Skip to content

Commit 1ff23e3

Browse files
Merge pull request #626 from kimdv/kimdv/graphql-file-upload
File upload concept - Taken from #116
2 parents aa472fe + 7521813 commit 1ff23e3

10 files changed

Lines changed: 394 additions & 27 deletions

File tree

Apollo.xcodeproj/project.pbxproj

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@
103103
9FF90A6F1DDDEB420034C3B6 /* InputValueEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */; };
104104
9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */; };
105105
9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */; };
106+
C304EBD722DDC8E600748F72 /* a.txt in Resources */ = {isa = PBXBuildFile; fileRef = C304EBD322DDC7B200748F72 /* a.txt */; };
107+
C338DF1722DD9DE9006AF33E /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C338DF1622DD9DE9006AF33E /* MultipartFormDataTests.swift */; };
108+
C35D43C222DDD4AC00BCBABE /* b.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BE22DDD3C100BCBABE /* b.txt */; };
109+
C35D43C322DDD4AF00BCBABE /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BF22DDD3C100BCBABE /* c.txt */; };
110+
C35D43C422DDD4D100BCBABE /* b.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BE22DDD3C100BCBABE /* b.txt */; };
111+
C35D43C522DDD4D300BCBABE /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = C35D43BF22DDD3C100BCBABE /* c.txt */; };
112+
C35D43C622DDE28D00BCBABE /* a.txt in Resources */ = {isa = PBXBuildFile; fileRef = C304EBD322DDC7B200748F72 /* a.txt */; };
113+
C377CCA922D798BD00572E03 /* GraphQLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377CCA822D798BD00572E03 /* GraphQLFile.swift */; };
114+
C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377CCAA22D7992E00572E03 /* MultipartFormData.swift */; };
106115
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86D8E03214B32DA0028EFE1 /* JSONTests.swift */; };
107116
F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */; };
108117
/* End PBXBuildFile section */
@@ -361,6 +370,12 @@
361370
9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputValueEncodingTests.swift; sourceTree = "<group>"; };
362371
9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadFieldValueTests.swift; sourceTree = "<group>"; };
363372
9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryResponseTests.swift; sourceTree = "<group>"; };
373+
C304EBD322DDC7B200748F72 /* a.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = a.txt; sourceTree = "<group>"; };
374+
C338DF1622DD9DE9006AF33E /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = "<group>"; };
375+
C35D43BE22DDD3C100BCBABE /* b.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = b.txt; sourceTree = "<group>"; };
376+
C35D43BF22DDD3C100BCBABE /* c.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = c.txt; sourceTree = "<group>"; };
377+
C377CCA822D798BD00572E03 /* GraphQLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLFile.swift; sourceTree = "<group>"; };
378+
C377CCAA22D7992E00572E03 /* MultipartFormData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormData.swift; sourceTree = "<group>"; };
364379
E86D8E03214B32DA0028EFE1 /* JSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONTests.swift; sourceTree = "<group>"; };
365380
F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryFromJSONBuildingTests.swift; sourceTree = "<group>"; };
366381
/* End PBXFileReference section */
@@ -568,17 +583,19 @@
568583
isa = PBXGroup;
569584
children = (
570585
9FC750621D2A59F600458D91 /* ApolloClient.swift */,
571-
9FC750601D2A59C300458D91 /* GraphQLOperation.swift */,
572-
9FC9A9BE1E2C27FB0023C4D5 /* GraphQLResult.swift */,
573586
9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */,
587+
C377CCA822D798BD00572E03 /* GraphQLFile.swift */,
588+
9FC750601D2A59C300458D91 /* GraphQLOperation.swift */,
574589
9FCDFD281E33D0CE007519DC /* GraphQLQueryWatcher.swift */,
575-
9BDE43D222C6658D00FD7C7F /* Protocols */,
576-
9FC9A9CE1E2FD0CC0023C4D5 /* Network */,
577-
9FC9A9CA1E2FD05C0023C4D5 /* Store */,
590+
9FC9A9BE1E2C27FB0023C4D5 /* GraphQLResult.swift */,
591+
C377CCAA22D7992E00572E03 /* MultipartFormData.swift */,
578592
9F27D4601D40363A00715680 /* Execution */,
579593
9FC4B9231D2BE4F00046A641 /* JSON */,
580-
9FCDFD211E33A09F007519DC /* Utilities */,
594+
9FC9A9CE1E2FD0CC0023C4D5 /* Network */,
595+
9BDE43D222C6658D00FD7C7F /* Protocols */,
596+
9FC9A9CA1E2FD05C0023C4D5 /* Store */,
581597
9FE3F3961DADBD0D0072078F /* Supporting Files */,
598+
9FCDFD211E33A09F007519DC /* Utilities */,
582599
);
583600
name = Apollo;
584601
path = Sources/Apollo;
@@ -587,22 +604,26 @@
587604
9FC750521D2A532D00458D91 /* ApolloTests */ = {
588605
isa = PBXGroup;
589606
children = (
607+
9FC750551D2A532D00458D91 /* Info.plist */,
590608
9F438D0B1E6C494C007BDC1A /* BatchedLoadTests.swift */,
591609
9FC9A9C71E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift */,
592610
9FADC8531E6B86D900C677E6 /* DataLoaderTests.swift */,
593611
9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */,
594612
9B95EDBF22CAA0AF00702BB2 /* GETTransformerTests.swift */,
595613
9BF1A94C22CA54F9005292C2 /* HTTPTransportTests.swift */,
596-
9FC750551D2A532D00458D91 /* Info.plist */,
597614
9FF90A6A1DDDEB420034C3B6 /* InputValueEncodingTests.swift */,
598615
E86D8E03214B32DA0028EFE1 /* JSONTests.swift */,
616+
C338DF1622DD9DE9006AF33E /* MultipartFormDataTests.swift */,
599617
9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */,
600618
9F295E301E27534800A24949 /* NormalizeQueryResults.swift */,
601619
9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */,
602620
9FE1C6E61E634C8D00C02284 /* PromiseTests.swift */,
603621
F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */,
604622
9FF90A6B1DDDEB420034C3B6 /* ReadFieldValueTests.swift */,
605623
9F19D8451EED8D3B00C57247 /* ResultOrPromiseTests.swift */,
624+
C304EBD322DDC7B200748F72 /* a.txt */,
625+
C35D43BE22DDD3C100BCBABE /* b.txt */,
626+
C35D43BF22DDD3C100BCBABE /* c.txt */,
606627
);
607628
path = ApolloTests;
608629
sourceTree = "<group>";
@@ -850,6 +871,7 @@
850871
buildPhases = (
851872
9FC7504A1D2A532D00458D91 /* Sources */,
852873
9FC7504B1D2A532D00458D91 /* Frameworks */,
874+
C304EBD522DDC87800748F72 /* Resources */,
853875
);
854876
buildRules = (
855877
);
@@ -992,8 +1014,11 @@
9921014
isa = PBXResourcesBuildPhase;
9931015
buildActionMask = 2147483647;
9941016
files = (
1017+
C35D43C522DDD4D300BCBABE /* c.txt in Resources */,
1018+
C304EBD722DDC8E600748F72 /* a.txt in Resources */,
9951019
9FD637EE1E6ACF88001EDBC8 /* LaunchScreen.storyboard in Resources */,
9961020
9FD637EB1E6ACF88001EDBC8 /* Assets.xcassets in Resources */,
1021+
C35D43C422DDD4D100BCBABE /* b.txt in Resources */,
9971022
9FD637E91E6ACF88001EDBC8 /* Main.storyboard in Resources */,
9981023
);
9991024
runOnlyForDeploymentPostprocessing = 0;
@@ -1005,6 +1030,16 @@
10051030
);
10061031
runOnlyForDeploymentPostprocessing = 0;
10071032
};
1033+
C304EBD522DDC87800748F72 /* Resources */ = {
1034+
isa = PBXResourcesBuildPhase;
1035+
buildActionMask = 2147483647;
1036+
files = (
1037+
C35D43C622DDE28D00BCBABE /* a.txt in Resources */,
1038+
C35D43C322DDD4AF00BCBABE /* c.txt in Resources */,
1039+
C35D43C222DDD4AC00BCBABE /* b.txt in Resources */,
1040+
);
1041+
runOnlyForDeploymentPostprocessing = 0;
1042+
};
10081043
/* End PBXResourcesBuildPhase section */
10091044

10101045
/* Begin PBXShellScriptBuildPhase section */
@@ -1103,6 +1138,7 @@
11031138
buildActionMask = 2147483647;
11041139
files = (
11051140
9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */,
1141+
C377CCAB22D7992E00572E03 /* MultipartFormData.swift in Sources */,
11061142
9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */,
11071143
9F8F334C229044A200C0E83B /* Decoding.swift in Sources */,
11081144
9FADC84A1E6B0B2300C677E6 /* Locking.swift in Sources */,
@@ -1122,6 +1158,7 @@
11221158
9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */,
11231159
9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */,
11241160
9BA1244A22D8A8EA00BF1D24 /* JSONSerialziation+Sorting.swift in Sources */,
1161+
C377CCA922D798BD00572E03 /* GraphQLFile.swift in Sources */,
11251162
9FC9A9CC1E2FD0760023C4D5 /* Record.swift in Sources */,
11261163
9FC4B9201D2A6F8D0046A641 /* JSON.swift in Sources */,
11271164
9FEC15B41E681DAD00D461B4 /* Collections.swift in Sources */,
@@ -1159,6 +1196,7 @@
11591196
9FADC8541E6B86D900C677E6 /* DataLoaderTests.swift in Sources */,
11601197
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */,
11611198
9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */,
1199+
C338DF1722DD9DE9006AF33E /* MultipartFormDataTests.swift in Sources */,
11621200
F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */,
11631201
9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */,
11641202
9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */,

Sources/Apollo/GraphQLFile.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
3+
public struct GraphQLFile {
4+
let fieldName: String
5+
let originalName: String
6+
let mimeType: String
7+
let inputStream: InputStream
8+
let contentLength: UInt64
9+
10+
public init(fieldName: String, originalName: String, mimeType: String = "application/octet-stream", data: Data) {
11+
self.init(fieldName: fieldName, originalName: originalName, mimeType: mimeType, inputStream: InputStream(data: data), contentLength: UInt64(data.count))
12+
}
13+
14+
public init?(fieldName: String, originalName: String, mimeType: String = "application/octet-stream", fileURL: URL) {
15+
guard let inputStream = InputStream(url: fileURL) else {
16+
return nil
17+
}
18+
19+
guard let contentLength = GraphQLFile.getFileSize(fileURL: fileURL) else {
20+
return nil
21+
}
22+
23+
self.init(fieldName: fieldName, originalName: originalName, mimeType: mimeType, inputStream: inputStream, contentLength: contentLength)
24+
}
25+
26+
public init(fieldName: String, originalName: String, mimeType: String = "application/octet-stream", inputStream: InputStream, contentLength: UInt64) {
27+
self.fieldName = fieldName
28+
self.originalName = originalName
29+
self.mimeType = mimeType
30+
31+
self.inputStream = inputStream
32+
self.contentLength = contentLength
33+
}
34+
35+
private static func getFileSize(fileURL: URL) -> UInt64? {
36+
guard let fileSizeAttribute = try? FileManager.default.attributesOfItem(atPath: fileURL.path)[.size],
37+
let fileSize = fileSizeAttribute as? NSNumber else {
38+
return nil
39+
}
40+
41+
return fileSize.uint64Value
42+
}
43+
}

Sources/Apollo/HTTPNetworkTransport.swift

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public class HTTPNetworkTransport: NetworkTransport {
7474
let serializationFormat = JSONSerializationFormat.self
7575
let useGETForQueries: Bool
7676
let delegate: HTTPNetworkTransportDelegate?
77-
77+
7878
/// Creates a network transport with the specified server URL and session configuration.
7979
///
8080
/// - Parameters:
@@ -104,9 +104,13 @@ public class HTTPNetworkTransport: NetworkTransport {
104104
/// - error: An error that indicates why a request failed, or `nil` if the request was succesful.
105105
/// - Returns: An object that can be used to cancel an in progress request.
106106
public func send<Operation>(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
107+
return upload(operation: operation, completionHandler: completionHandler)
108+
}
109+
110+
public func upload<Operation>(operation: Operation, files: [GraphQLFile]? = nil, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
107111
let request: URLRequest
108112
do {
109-
request = try self.createRequest(for: operation)
113+
request = try self.createRequest(for: operation, files: files)
110114
} catch {
111115
completionHandler(nil, error)
112116
return EmptyCancellable()
@@ -117,7 +121,7 @@ public class HTTPNetworkTransport: NetworkTransport {
117121
data: data,
118122
response: response,
119123
error: error)
120-
124+
121125
if let receivedError = error {
122126
self?.handleErrorOrRetry(operation: operation,
123127
error: receivedError,
@@ -126,11 +130,11 @@ public class HTTPNetworkTransport: NetworkTransport {
126130
completionHandler: completionHandler)
127131
return
128132
}
129-
133+
130134
guard let httpResponse = response as? HTTPURLResponse else {
131135
fatalError("Response should be an HTTPURLResponse")
132136
}
133-
137+
134138
guard httpResponse.isSuccessful else {
135139
let unsuccessfulError = GraphQLHTTPResponseError(body: data,
136140
response: httpResponse,
@@ -142,7 +146,7 @@ public class HTTPNetworkTransport: NetworkTransport {
142146
completionHandler: completionHandler)
143147
return
144148
}
145-
149+
146150
guard let data = data else {
147151
let error = GraphQLHTTPResponseError(body: nil,
148152
response: httpResponse,
@@ -154,7 +158,7 @@ public class HTTPNetworkTransport: NetworkTransport {
154158
completionHandler: completionHandler)
155159
return
156160
}
157-
161+
158162
do {
159163
guard let body = try self?.serializationFormat.deserialize(data: data) as? JSONObject else {
160164
throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
@@ -174,7 +178,7 @@ public class HTTPNetworkTransport: NetworkTransport {
174178

175179
return task
176180
}
177-
181+
178182
private let sendOperationIdentifiers: Bool
179183

180184
private func handleErrorOrRetry<Operation>(operation: Operation,
@@ -185,8 +189,8 @@ public class HTTPNetworkTransport: NetworkTransport {
185189
guard
186190
let delegate = self.delegate,
187191
let retrier = delegate as? HTTPNetworkTransportRetryDelegate else {
188-
completionHandler(nil, error)
189-
return
192+
completionHandler(nil, error)
193+
return
190194
}
191195

192196
retrier.networkTransport(
@@ -211,7 +215,7 @@ public class HTTPNetworkTransport: NetworkTransport {
211215
guard
212216
let delegate = self.delegate,
213217
let taskDelegate = delegate as? HTTPNetworkTransportTaskCompletedDelegate else {
214-
return
218+
return
215219
}
216220

217221
taskDelegate.networkTransport(self,
@@ -221,7 +225,7 @@ public class HTTPNetworkTransport: NetworkTransport {
221225
error: error)
222226
}
223227

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

@@ -235,7 +239,14 @@ public class HTTPNetworkTransport: NetworkTransport {
235239
}
236240
} else {
237241
do {
238-
request.httpBody = try serializationFormat.serialize(value: body)
242+
if let files = files, !files.isEmpty {
243+
let formData = try requestMultipartFormData(for: operation, files: files)
244+
request.setValue("multipart/form-data; boundary=\(formData.boundary)", forHTTPHeaderField: "Content-Type")
245+
request.httpBody = formData.encode()
246+
} else {
247+
request.httpBody = try serializationFormat.serialize(value: body)
248+
}
249+
239250
request.httpMethod = GraphQLHTTPMethod.POST.rawValue
240251
} catch {
241252
throw GraphQLHTTPRequestError.serializedBodyMessageError
@@ -248,16 +259,16 @@ public class HTTPNetworkTransport: NetworkTransport {
248259
if
249260
let delegate = self.delegate,
250261
let preflightDelegate = delegate as? HTTPNetworkTransportPreflightDelegate {
251-
guard preflightDelegate.networkTransport(self, shouldSend: request) else {
252-
throw GraphQLHTTPRequestError.cancelledByDelegate
253-
}
262+
guard preflightDelegate.networkTransport(self, shouldSend: request) else {
263+
throw GraphQLHTTPRequestError.cancelledByDelegate
264+
}
254265

255-
preflightDelegate.networkTransport(self, willSend: &request)
266+
preflightDelegate.networkTransport(self, willSend: &request)
256267
}
257268

258269
return request
259270
}
260-
271+
261272
private func requestBody<Operation: GraphQLOperation>(for operation: Operation) -> GraphQLMap {
262273
if sendOperationIdentifiers {
263274
guard let operationIdentifier = operation.operationIdentifier else {
@@ -269,4 +280,26 @@ public class HTTPNetworkTransport: NetworkTransport {
269280

270281
return ["query": operation.queryDocument, "variables": operation.variables]
271282
}
283+
284+
private func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation, files: [GraphQLFile]) throws -> MultipartFormData {
285+
let formData = MultipartFormData()
286+
287+
let fields = requestBody(for: operation)
288+
for (name, data) in fields {
289+
if let data = data as? GraphQLMap {
290+
let data = try serializationFormat.serialize(value: data)
291+
formData.appendPart(data: data, name: name)
292+
} else if let data = data as? String {
293+
formData.appendPart(string: data, name: name)
294+
} else {
295+
formData.appendPart(string: data.debugDescription, name: name)
296+
}
297+
}
298+
299+
files.forEach {
300+
formData.appendPart(inputStream: $0.inputStream, contentLength: $0.contentLength, name: $0.fieldName, contentType: $0.mimeType, filename: $0.originalName)
301+
}
302+
303+
return formData
304+
}
272305
}

0 commit comments

Comments
 (0)