Skip to content

Commit

Permalink
Merge pull request #16 from thefuntasty/feature/uploading-using-streams
Browse files Browse the repository at this point in the history
Multipart requests using streams
  • Loading branch information
mkj-is authored Mar 11, 2019
2 parents a6f59c5 + 06f7a85 commit df7e707
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 91 deletions.
50 changes: 38 additions & 12 deletions FTAPIKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@
52D6D9871BEFF229002C0205 /* FTAPIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* FTAPIKit.framework */; };
DD7502881C68FEDE006590AF /* FTAPIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* FTAPIKit.framework */; };
DD7502921C690C7A006590AF /* FTAPIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* FTAPIKit.framework */; };
E7197F77223510FF0059A54A /* MultipartBodyPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7197F76223510FF0059A54A /* MultipartBodyPart.swift */; };
E7197F78223510FF0059A54A /* MultipartBodyPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7197F76223510FF0059A54A /* MultipartBodyPart.swift */; };
E7197F79223510FF0059A54A /* MultipartBodyPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7197F76223510FF0059A54A /* MultipartBodyPart.swift */; };
E7197F7A223510FF0059A54A /* MultipartBodyPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7197F76223510FF0059A54A /* MultipartBodyPart.swift */; };
E7197F802235266A0059A54A /* MockupBodyPart.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E7197F7B223523A40059A54A /* MockupBodyPart.jpg */; };
E7197F812235266A0059A54A /* MockupBodyPart.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E7197F7B223523A40059A54A /* MockupBodyPart.jpg */; };
E7197F822235266B0059A54A /* MockupBodyPart.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E7197F7B223523A40059A54A /* MockupBodyPart.jpg */; };
E7692824222431CE0031137D /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7692823222431CE0031137D /* MultipartFormData.swift */; };
E7692825222431CE0031137D /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7692823222431CE0031137D /* MultipartFormData.swift */; };
E7692826222431CE0031137D /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7692823222431CE0031137D /* MultipartFormData.swift */; };
E7692827222431CE0031137D /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7692823222431CE0031137D /* MultipartFormData.swift */; };
E769282922269D4C0031137D /* OutputStream+Write.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769282822269D4C0031137D /* OutputStream+Write.swift */; };
E769282A22269D4C0031137D /* OutputStream+Write.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769282822269D4C0031137D /* OutputStream+Write.swift */; };
E769282B22269D4C0031137D /* OutputStream+Write.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769282822269D4C0031137D /* OutputStream+Write.swift */; };
E769282C22269D4C0031137D /* OutputStream+Write.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769282822269D4C0031137D /* OutputStream+Write.swift */; };
E792629D219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; };
E792629E219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; };
E792629F219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; };
Expand All @@ -36,10 +51,6 @@
E7B774202152752A006E7585 /* URL+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774152152752A006E7585 /* URL+APIAdapter.swift */; };
E7B774212152752A006E7585 /* URL+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774152152752A006E7585 /* URL+APIAdapter.swift */; };
E7B774222152752A006E7585 /* URL+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774152152752A006E7585 /* URL+APIAdapter.swift */; };
E7B774232152752A006E7585 /* Data+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774162152752A006E7585 /* Data+APIAdapter.swift */; };
E7B774242152752A006E7585 /* Data+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774162152752A006E7585 /* Data+APIAdapter.swift */; };
E7B774252152752A006E7585 /* Data+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774162152752A006E7585 /* Data+APIAdapter.swift */; };
E7B774262152752A006E7585 /* Data+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774162152752A006E7585 /* Data+APIAdapter.swift */; };
E7B774272152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */; };
E7B774282152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */; };
E7B774292152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */; };
Expand Down Expand Up @@ -98,10 +109,13 @@
AD2FAA281CD0B6E100659CF4 /* FTAPIKitTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = FTAPIKitTests.plist; sourceTree = "<group>"; };
DD75027A1C68FCFC006590AF /* FTAPIKit-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FTAPIKit-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
DD75028D1C690C7A006590AF /* FTAPIKit-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FTAPIKit-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
E7197F76223510FF0059A54A /* MultipartBodyPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartBodyPart.swift; sourceTree = "<group>"; };
E7197F7B223523A40059A54A /* MockupBodyPart.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = MockupBodyPart.jpg; sourceTree = "<group>"; };
E7692823222431CE0031137D /* MultipartFormData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormData.swift; sourceTree = "<group>"; };
E769282822269D4C0031137D /* OutputStream+Write.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OutputStream+Write.swift"; sourceTree = "<group>"; };
E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionAPIAdapter.swift; sourceTree = "<group>"; };
E7B774142152752A006E7585 /* APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIAdapter.swift; sourceTree = "<group>"; };
E7B774152152752A006E7585 /* URL+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+APIAdapter.swift"; sourceTree = "<group>"; };
E7B774162152752A006E7585 /* Data+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+APIAdapter.swift"; sourceTree = "<group>"; };
E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+APIAdapter.swift"; sourceTree = "<group>"; };
E7B774182152752A006E7585 /* AnyEncodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
E7B774192152752A006E7585 /* APIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIEndpoint.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -207,9 +221,11 @@
E7B7741A2152752A006E7585 /* APIAdapter+Types.swift */,
E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */,
E7B774182152752A006E7585 /* AnyEncodable.swift */,
E7B774162152752A006E7585 /* Data+APIAdapter.swift */,
E7B774152152752A006E7585 /* URL+APIAdapter.swift */,
E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */,
E7692823222431CE0031137D /* MultipartFormData.swift */,
E769282822269D4C0031137D /* OutputStream+Write.swift */,
E7197F76223510FF0059A54A /* MultipartBodyPart.swift */,
);
path = Sources;
sourceTree = "<group>";
Expand Down Expand Up @@ -243,6 +259,7 @@
E7B7743F21527886006E7585 /* Mockups */ = {
isa = PBXGroup;
children = (
E7197F7B223523A40059A54A /* MockupBodyPart.jpg */,
E7B7743B21527883006E7585 /* MockupAPIAdapterDelegate.swift */,
);
path = Mockups;
Expand Down Expand Up @@ -488,6 +505,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E7197F802235266A0059A54A /* MockupBodyPart.jpg in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -516,13 +534,15 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E7197F812235266A0059A54A /* MockupBodyPart.jpg in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
DD75028B1C690C7A006590AF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E7197F822235266B0059A54A /* MockupBodyPart.jpg in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -553,13 +573,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E769282922269D4C0031137D /* OutputStream+Write.swift in Sources */,
E7B7742F2152752A006E7585 /* APIEndpoint.swift in Sources */,
E7B774232152752A006E7585 /* Data+APIAdapter.swift in Sources */,
E7B7741F2152752A006E7585 /* URL+APIAdapter.swift in Sources */,
E7B774272152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */,
E7B774332152752A006E7585 /* APIAdapter+Types.swift in Sources */,
E7B7741B2152752A006E7585 /* APIAdapter.swift in Sources */,
E7B7742B2152752A006E7585 /* AnyEncodable.swift in Sources */,
E7197F77223510FF0059A54A /* MultipartBodyPart.swift in Sources */,
E7692824222431CE0031137D /* MultipartFormData.swift in Sources */,
E792629D219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -577,13 +599,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E769282B22269D4C0031137D /* OutputStream+Write.swift in Sources */,
E7B774312152752A006E7585 /* APIEndpoint.swift in Sources */,
E7B774252152752A006E7585 /* Data+APIAdapter.swift in Sources */,
E7B774212152752A006E7585 /* URL+APIAdapter.swift in Sources */,
E7B774292152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */,
E7B774352152752A006E7585 /* APIAdapter+Types.swift in Sources */,
E7B7741D2152752A006E7585 /* APIAdapter.swift in Sources */,
E7B7742D2152752A006E7585 /* AnyEncodable.swift in Sources */,
E7197F79223510FF0059A54A /* MultipartBodyPart.swift in Sources */,
E7692826222431CE0031137D /* MultipartFormData.swift in Sources */,
E792629F219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -592,13 +616,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E769282C22269D4C0031137D /* OutputStream+Write.swift in Sources */,
E7B774322152752A006E7585 /* APIEndpoint.swift in Sources */,
E7B774262152752A006E7585 /* Data+APIAdapter.swift in Sources */,
E7B774222152752A006E7585 /* URL+APIAdapter.swift in Sources */,
E7B7742A2152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */,
E7B774362152752A006E7585 /* APIAdapter+Types.swift in Sources */,
E7B7741E2152752A006E7585 /* APIAdapter.swift in Sources */,
E7B7742E2152752A006E7585 /* AnyEncodable.swift in Sources */,
E7197F7A223510FF0059A54A /* MultipartBodyPart.swift in Sources */,
E7692827222431CE0031137D /* MultipartFormData.swift in Sources */,
E79262A0219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -607,13 +633,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E769282A22269D4C0031137D /* OutputStream+Write.swift in Sources */,
E7B774302152752A006E7585 /* APIEndpoint.swift in Sources */,
E7B774242152752A006E7585 /* Data+APIAdapter.swift in Sources */,
E7B774202152752A006E7585 /* URL+APIAdapter.swift in Sources */,
E7B774282152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */,
E7B774342152752A006E7585 /* APIAdapter+Types.swift in Sources */,
E7B7741C2152752A006E7585 /* APIAdapter.swift in Sources */,
E7B7742C2152752A006E7585 /* AnyEncodable.swift in Sources */,
E7197F78223510FF0059A54A /* MultipartBodyPart.swift in Sources */,
E7692825222431CE0031137D /* MultipartFormData.swift in Sources */,
E792629E219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -781,7 +809,6 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Configs/FTAPIKit.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
ONLY_ACTIVE_ARCH = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.FTAPIKit.FTAPIKit-iOS";
Expand All @@ -804,7 +831,6 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Configs/FTAPIKit.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.FTAPIKit.FTAPIKit-iOS";
PRODUCT_NAME = FTAPIKit;
Expand Down
27 changes: 5 additions & 22 deletions Sources/APIAdapter+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved.
//

import Foundation
import struct Foundation.Data

/// Standard API error returned in `APIResult` when no custom error
/// was parsed in the `APIAdapter` first and the response from server
Expand All @@ -21,6 +21,9 @@ public enum APIError: Error {
/// Error code returned by `APIAdapter`. Thrown when request fails
/// with return code larger or equal to 400.
case errorCode(Int, Data?)
/// Multipart body part error, when the stream for the part
/// or the temporary request body stream cannot be opened.
case multipartStreamCannotBeOpened
}

/// Generic result type for API responses.
Expand Down Expand Up @@ -83,28 +86,8 @@ public enum RequestType {
case jsonBody(Encodable)
/// All the parameters will be sent as multipart
/// and files too.
case multipart([MultipartFile])
case multipart([MultipartBodyPart])
/// The parameters will be encoded using Base64 encoding
/// and sent in request body.
case base64Upload
}

/// Multipart file model for multipart request types.
public struct MultipartFile: Hashable {
let name, filename, mimeType: String
let data: Data

/// Public initializer for multipart files.
///
/// - Parameters:
/// - name: Part name.
/// - filename: File name with extension.
/// - mimeType: MIME type of the file.
/// - data: File content.
public init(name: String, filename: String, mimeType: String, data: Data) {
self.name = name
self.filename = filename
self.mimeType = mimeType
self.data = data
}
}
22 changes: 0 additions & 22 deletions Sources/Data+APIAdapter.swift

This file was deleted.

70 changes: 70 additions & 0 deletions Sources/MultipartBodyPart.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// MultipartBodyPart.swift
// FTAPIKit
//
// Created by Matěj Kašpar Jirásek on 10/03/2019.
// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved.
//

import struct Foundation.Data
import struct Foundation.URL
import class Foundation.InputStream

/// Structure representing part in `multipart/form-data` request.
/// These parts must have valid headers according
/// to [RFC-7578](https://tools.ietf.org/html/rfc7578).
/// Everything passed to it is converted to `InputStream`
/// in order to limit memory usage when sending files to a server.
public struct MultipartBodyPart: Hashable {
let headers: [String: String]
let inputStream: InputStream

/// Creates a new instance with custom headers and any input stream.
///
/// - Parameters:
/// - headers: HTTP headers specific for the part, these are not validated locally and must be correct according to [RFC-7578](https://tools.ietf.org/html/rfc7578).
/// - inputStream: Any byte stream.
public init(headers: [String: String], inputStream: InputStream) {
self.headers = headers
self.inputStream = inputStream
}

/// Creates a new instance from key-value or HTTP parameter.
///
/// - Parameters:
/// - name: Name of the parameter used in `Content-Disposition` header.
/// - value: String value of the parameter set as a body.
public init(name: String, value: String) {
let headers = [
"Content-Disposition": "form-data; name=\(name)"
]
self.init(headers: headers, data: Data(value.utf8))
}

/// Creates a new instance with custom headers and data as body.
///
/// - Parameters:
/// - headers: HTTP headers specific for the part, these are not validated locally and must be correct according to [RFC-7578](https://tools.ietf.org/html/rfc7578).
/// - data: Bytes sent as a part body.
public init(headers: [String: String], data: Data) {
self.headers = headers
self.inputStream = InputStream(data: data)
}

/// Creates a new instance with file URL used to be converted to body.
///
/// - Parameters:
/// - name: Name of the parameter used in `Content-Disposition` header.
/// - url: URL to a local file.
/// - Throws: `APIError.multipartStreamCannotBeOpened` if stream was not created from the file.
public init(name: String, url: URL) throws {
guard let inputStream = InputStream(url: url) else {
throw APIError.multipartStreamCannotBeOpened
}
self.headers = [
"Content-Type": url.mimeType,
"Content-Disposition": "form-data; name=\(name); filename=\"\(url.lastPathComponent)\""
]
self.inputStream = inputStream
}
}
Loading

0 comments on commit df7e707

Please sign in to comment.