Skip to content

Commit

Permalink
Merge pull request #626 from kimdv/kimdv/graphql-file-upload
Browse files Browse the repository at this point in the history
File upload concept - Taken from #116
  • Loading branch information
designatednerd authored Jul 18, 2019
2 parents aa472fe + 7521813 commit 1ff23e3
Show file tree
Hide file tree
Showing 10 changed files with 394 additions and 27 deletions.
52 changes: 45 additions & 7 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@
9FF90A6F1DDDEB420034C3B6 /* InputValueEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */; };
9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */; };
9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */; };
C304EBD722DDC8E600748F72 /* a.txt in Resources */ = {isa = PBXBuildFile; fileRef = C304EBD322DDC7B200748F72 /* a.txt */; };
C338DF1722DD9DE9006AF33E /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C338DF1622DD9DE9006AF33E /* MultipartFormDataTests.swift */; };
C35D43C222DDD4AC00BCBABE /* b.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BE22DDD3C100BCBABE /* b.txt */; };
C35D43C322DDD4AF00BCBABE /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BF22DDD3C100BCBABE /* c.txt */; };
C35D43C422DDD4D100BCBABE /* b.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BE22DDD3C100BCBABE /* b.txt */; };
C35D43C522DDD4D300BCBABE /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BF22DDD3C100BCBABE /* c.txt */; };
C35D43C622DDE28D00BCBABE /* a.txt in Resources */ = {isa = PBXBuildFile; fileRef = C304EBD322DDC7B200748F72 /* a.txt */; };
C377CCA922D798BD00572E03 /* GraphQLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377CCA822D798BD00572E03 /* GraphQLFile.swift */; };
C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377CCAA22D7992E00572E03 /* MultipartFormData.swift */; };
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86D8E03214B32DA0028EFE1 /* JSONTests.swift */; };
F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -361,6 +370,12 @@
9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputValueEncodingTests.swift; sourceTree = "<group>"; };
9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadFieldValueTests.swift; sourceTree = "<group>"; };
9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryResponseTests.swift; sourceTree = "<group>"; };
C304EBD322DDC7B200748F72 /* a.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = a.txt; sourceTree = "<group>"; };
C338DF1622DD9DE9006AF33E /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = "<group>"; };
C35D43BE22DDD3C100BCBABE /* b.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = b.txt; sourceTree = "<group>"; };
C35D43BF22DDD3C100BCBABE /* c.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = c.txt; sourceTree = "<group>"; };
C377CCA822D798BD00572E03 /* GraphQLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFile.swift; sourceTree = "<group>"; };
C377CCAA22D7992E00572E03 /* MultipartFormData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormData.swift; sourceTree = "<group>"; };
E86D8E03214B32DA0028EFE1 /* JSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONTests.swift; sourceTree = "<group>"; };
F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryFromJSONBuildingTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -568,17 +583,19 @@
isa = PBXGroup;
children = (
9FC750621D2A59F600458D91 /* ApolloClient.swift */,
9FC750601D2A59C300458D91 /* GraphQLOperation.swift */,
9FC9A9BE1E2C27FB0023C4D5 /* GraphQLResult.swift */,
9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */,
C377CCA822D798BD00572E03 /* GraphQLFile.swift */,
9FC750601D2A59C300458D91 /* GraphQLOperation.swift */,
9FCDFD281E33D0CE007519DC /* GraphQLQueryWatcher.swift */,
9BDE43D222C6658D00FD7C7F /* Protocols */,
9FC9A9CE1E2FD0CC0023C4D5 /* Network */,
9FC9A9CA1E2FD05C0023C4D5 /* Store */,
9FC9A9BE1E2C27FB0023C4D5 /* GraphQLResult.swift */,
C377CCAA22D7992E00572E03 /* MultipartFormData.swift */,
9F27D4601D40363A00715680 /* Execution */,
9FC4B9231D2BE4F00046A641 /* JSON */,
9FCDFD211E33A09F007519DC /* Utilities */,
9FC9A9CE1E2FD0CC0023C4D5 /* Network */,
9BDE43D222C6658D00FD7C7F /* Protocols */,
9FC9A9CA1E2FD05C0023C4D5 /* Store */,
9FE3F3961DADBD0D0072078F /* Supporting Files */,
9FCDFD211E33A09F007519DC /* Utilities */,
);
name = Apollo;
path = Sources/Apollo;
Expand All @@ -587,22 +604,26 @@
9FC750521D2A532D00458D91 /* ApolloTests */ = {
isa = PBXGroup;
children = (
9FC750551D2A532D00458D91 /* Info.plist */,
9F438D0B1E6C494C007BDC1A /* BatchedLoadTests.swift */,
9FC9A9C71E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift */,
9FADC8531E6B86D900C677E6 /* DataLoaderTests.swift */,
9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */,
9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */,
9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */,
9FC750551D2A532D00458D91 /* Info.plist */,
9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */,
E86D8E03214B32DA0028EFE1 /* JSONTests.swift */,
C338DF1622DD9DE9006AF33E /* MultipartFormDataTests.swift */,
9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */,
9F295E301E27534800A24949 /* NormalizeQueryResults.swift */,
9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */,
9FE1C6E61E634C8D00C02284 /* PromiseTests.swift */,
F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */,
9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */,
9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */,
C304EBD322DDC7B200748F72 /* a.txt */,
C35D43BE22DDD3C100BCBABE /* b.txt */,
C35D43BF22DDD3C100BCBABE /* c.txt */,
);
path = ApolloTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -850,6 +871,7 @@
buildPhases = (
9FC7504A1D2A532D00458D91 /* Sources */,
9FC7504B1D2A532D00458D91 /* Frameworks */,
C304EBD522DDC87800748F72 /* Resources */,
);
buildRules = (
);
Expand Down Expand Up @@ -992,8 +1014,11 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C35D43C522DDD4D300BCBABE /* c.txt in Resources */,
C304EBD722DDC8E600748F72 /* a.txt in Resources */,
9FD637EE1E6ACF88001EDBC8 /* LaunchScreen.storyboard in Resources */,
9FD637EB1E6ACF88001EDBC8 /* Assets.xcassets in Resources */,
C35D43C422DDD4D100BCBABE /* b.txt in Resources */,
9FD637E91E6ACF88001EDBC8 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -1005,6 +1030,16 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C304EBD522DDC87800748F72 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C35D43C622DDE28D00BCBABE /* a.txt in Resources */,
C35D43C322DDD4AF00BCBABE /* c.txt in Resources */,
C35D43C222DDD4AC00BCBABE /* b.txt in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
Expand Down Expand Up @@ -1103,6 +1138,7 @@
buildActionMask = 2147483647;
files = (
9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */,
C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */,
9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */,
9F8F334C229044A200C0E83B /* Decoding.swift in Sources */,
9FADC84A1E6B0B2300C677E6 /* Locking.swift in Sources */,
Expand All @@ -1122,6 +1158,7 @@
9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */,
9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */,
9BA1244A22D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift in Sources */,
C377CCA922D798BD00572E03 /* GraphQLFile.swift in Sources */,
9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */,
9FC4B9201D2A6F8D0046A641 /* JSON.swift in Sources */,
9FEC15B41E681DAD00D461B4 /* Collections.swift in Sources */,
Expand Down Expand Up @@ -1159,6 +1196,7 @@
9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */,
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */,
9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */,
C338DF1722DD9DE9006AF33E /* MultipartFormDataTests.swift in Sources */,
F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */,
9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */,
9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */,
Expand Down
43 changes: 43 additions & 0 deletions Sources/Apollo/GraphQLFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

public struct GraphQLFile {
let fieldName: String
let originalName: String
let mimeType: String
let inputStream: InputStream
let contentLength: UInt64

public init(fieldName: String, originalName: String, mimeType: String = "application/octet-stream", data: Data) {
self.init(fieldName: fieldName, originalName: originalName, mimeType: mimeType, inputStream: InputStream(data: data), contentLength: UInt64(data.count))
}

public init?(fieldName: String, originalName: String, mimeType: String = "application/octet-stream", fileURL: URL) {
guard let inputStream = InputStream(url: fileURL) else {
return nil
}

guard let contentLength = GraphQLFile.getFileSize(fileURL: fileURL) else {
return nil
}

self.init(fieldName: fieldName, originalName: originalName, mimeType: mimeType, inputStream: inputStream, contentLength: contentLength)
}

public init(fieldName: String, originalName: String, mimeType: String = "application/octet-stream", inputStream: InputStream, contentLength: UInt64) {
self.fieldName = fieldName
self.originalName = originalName
self.mimeType = mimeType

self.inputStream = inputStream
self.contentLength = contentLength
}

private static func getFileSize(fileURL: URL) -> UInt64? {
guard let fileSizeAttribute = try? FileManager.default.attributesOfItem(atPath: fileURL.path)[.size],
let fileSize = fileSizeAttribute as? NSNumber else {
return nil
}

return fileSize.uint64Value
}
}
69 changes: 51 additions & 18 deletions Sources/Apollo/HTTPNetworkTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public class HTTPNetworkTransport: NetworkTransport {
let serializationFormat = JSONSerializationFormat.self
let useGETForQueries: Bool
let delegate: HTTPNetworkTransportDelegate?

/// Creates a network transport with the specified server URL and session configuration.
///
/// - Parameters:
Expand Down Expand Up @@ -104,9 +104,13 @@ public class HTTPNetworkTransport: NetworkTransport {
/// - error: An error that indicates why a request failed, or `nil` if the request was succesful.
/// - Returns: An object that can be used to cancel an in progress request.
public func send<Operation>(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
return upload(operation: operation, completionHandler: completionHandler)
}

public func upload<Operation>(operation: Operation, files: [GraphQLFile]? = nil, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
let request: URLRequest
do {
request = try self.createRequest(for: operation)
request = try self.createRequest(for: operation, files: files)
} catch {
completionHandler(nil, error)
return EmptyCancellable()
Expand All @@ -117,7 +121,7 @@ public class HTTPNetworkTransport: NetworkTransport {
data: data,
response: response,
error: error)

if let receivedError = error {
self?.handleErrorOrRetry(operation: operation,
error: receivedError,
Expand All @@ -126,11 +130,11 @@ public class HTTPNetworkTransport: NetworkTransport {
completionHandler: completionHandler)
return
}

guard let httpResponse = response as? HTTPURLResponse else {
fatalError("Response should be an HTTPURLResponse")
}

guard httpResponse.isSuccessful else {
let unsuccessfulError = GraphQLHTTPResponseError(body: data,
response: httpResponse,
Expand All @@ -142,7 +146,7 @@ public class HTTPNetworkTransport: NetworkTransport {
completionHandler: completionHandler)
return
}

guard let data = data else {
let error = GraphQLHTTPResponseError(body: nil,
response: httpResponse,
Expand All @@ -154,7 +158,7 @@ public class HTTPNetworkTransport: NetworkTransport {
completionHandler: completionHandler)
return
}

do {
guard let body = try self?.serializationFormat.deserialize(data: data) as? JSONObject else {
throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
Expand All @@ -174,7 +178,7 @@ public class HTTPNetworkTransport: NetworkTransport {

return task
}

private let sendOperationIdentifiers: Bool

private func handleErrorOrRetry<Operation>(operation: Operation,
Expand All @@ -185,8 +189,8 @@ public class HTTPNetworkTransport: NetworkTransport {
guard
let delegate = self.delegate,
let retrier = delegate as? HTTPNetworkTransportRetryDelegate else {
completionHandler(nil, error)
return
completionHandler(nil, error)
return
}

retrier.networkTransport(
Expand All @@ -211,7 +215,7 @@ public class HTTPNetworkTransport: NetworkTransport {
guard
let delegate = self.delegate,
let taskDelegate = delegate as? HTTPNetworkTransportTaskCompletedDelegate else {
return
return
}

taskDelegate.networkTransport(self,
Expand All @@ -221,7 +225,7 @@ public class HTTPNetworkTransport: NetworkTransport {
error: error)
}

private func createRequest<Operation: GraphQLOperation>(for operation: Operation) throws -> URLRequest {
private func createRequest<Operation: GraphQLOperation>(for operation: Operation, files: [GraphQLFile]?) throws -> URLRequest {
let body = requestBody(for: operation)
var request = URLRequest(url: self.url)

Expand All @@ -235,7 +239,14 @@ public class HTTPNetworkTransport: NetworkTransport {
}
} else {
do {
request.httpBody = try serializationFormat.serialize(value: body)
if let files = files, !files.isEmpty {
let formData = try requestMultipartFormData(for: operation, files: files)
request.setValue("multipart/form-data; boundary=\(formData.boundary)", forHTTPHeaderField: "Content-Type")
request.httpBody = formData.encode()
} else {
request.httpBody = try serializationFormat.serialize(value: body)
}

request.httpMethod = GraphQLHTTPMethod.POST.rawValue
} catch {
throw GraphQLHTTPRequestError.serializedBodyMessageError
Expand All @@ -248,16 +259,16 @@ public class HTTPNetworkTransport: NetworkTransport {
if
let delegate = self.delegate,
let preflightDelegate = delegate as? HTTPNetworkTransportPreflightDelegate {
guard preflightDelegate.networkTransport(self, shouldSend: request) else {
throw GraphQLHTTPRequestError.cancelledByDelegate
}
guard preflightDelegate.networkTransport(self, shouldSend: request) else {
throw GraphQLHTTPRequestError.cancelledByDelegate
}

preflightDelegate.networkTransport(self, willSend: &request)
preflightDelegate.networkTransport(self, willSend: &request)
}

return request
}

private func requestBody<Operation: GraphQLOperation>(for operation: Operation) -> GraphQLMap {
if sendOperationIdentifiers {
guard let operationIdentifier = operation.operationIdentifier else {
Expand All @@ -269,4 +280,26 @@ public class HTTPNetworkTransport: NetworkTransport {

return ["query": operation.queryDocument, "variables": operation.variables]
}

private func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation, files: [GraphQLFile]) throws -> MultipartFormData {
let formData = MultipartFormData()

let fields = requestBody(for: operation)
for (name, data) in fields {
if let data = data as? GraphQLMap {
let data = try serializationFormat.serialize(value: data)
formData.appendPart(data: data, name: name)
} else if let data = data as? String {
formData.appendPart(string: data, name: name)
} else {
formData.appendPart(string: data.debugDescription, name: name)
}
}

files.forEach {
formData.appendPart(inputStream: $0.inputStream, contentLength: $0.contentLength, name: $0.fieldName, contentType: $0.mimeType, filename: $0.originalName)
}

return formData
}
}
Loading

0 comments on commit 1ff23e3

Please sign in to comment.