From 8241b719025e43c1f7c86509eda3524a1527f33e Mon Sep 17 00:00:00 2001 From: dondi Date: Mon, 18 Jun 2018 23:23:26 -0700 Subject: [PATCH 1/3] Transcribe helper code into request creation extension file. --- Source/Siesta/Request/RequestCreation.swift | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/Source/Siesta/Request/RequestCreation.swift b/Source/Siesta/Request/RequestCreation.swift index e7cb3207..35efbace 100644 --- a/Source/Siesta/Request/RequestCreation.swift +++ b/Source/Siesta/Request/RequestCreation.swift @@ -155,6 +155,67 @@ public extension Resource allowedChars.remove(charactersIn: charsToEscape) return allowedChars }() + + /** + Convenience method to initiate a request using multipart encoding in the message body. + + Based on code suggested by @Alex293 in https://github.com/bustoutsolutions/siesta/issues/190 + + This convenience method just structures @Alex293’s example in a way that parallels the other convenience + methods in this extension. + + The parameters have the following meanings: + - values: [String:String] listing the names of various parts and their corresponding values + - files: optional [String:FilePart] listing the the names of _files_ to upload, with the files represented via a helper FilePart struct (defined at the bottom of this source file) + - order: optional [String] containing the keys from `values` and `files`—this comes into play if the server or service that accepts multipart requests also requires the parts in a particular order (e.g., S3 wants the `key` part first). The `order` array specifies the order to how the parts are sent. If `order` is not given, then the parts are enumerated in the order that Swift enumerates the keys of the `values` and `files` dictionaries (`values` enumerated first, then `files`) + - requestMutation: same closure as in the other convenience methods + */ + public func request( + _ method: RequestMethod, + multipart values: [String:String], + files: [String:FilePart]?, + order: [String]?, + requestMutation: @escaping RequestMutation = { _ in }) -> Request { + + func getNames() -> [String] { + if let givenOrder = order { + return givenOrder + } + + var names = Array(values.keys) + if files != nil { + names.append(contentsOf: files!.keys) + } + + return names + } + + func append(_ body: NSMutableData, _ line: String) { + body.append(line.data(using: .utf8)!) + } + + // Derived from https://github.com/bustoutsolutions/siesta/issues/190#issuecomment-294267686 + let boundary = "Boundary-\(NSUUID().uuidString)" + let contentType = "multipart/form-data; boundary=\(boundary)" + let body = NSMutableData() + let names = getNames() + names.forEach { name in + append(body, "--\(boundary)\r\n") + if values.keys.contains(name), let value = values[name] { + append(body, "Content-Disposition:form-data; name=\"\(name)\"\r\n\r\n") + append(body, "\(value)\r\n") + } else if let givenFiles = files, givenFiles.keys.contains(name), let filePart = givenFiles[name] { + append(body, "Content-Disposition:form-data; name=\"\(name)\"; filename=\"\(filePart.filename)\"\r\n") + append(body, "Content-Type: \(filePart.type)\r\n\r\n") + body.append(filePart.data) + append(body, "\r\n") + } + } + + append(body, "--\(boundary)--\r\n") + return request(method, data: body as Data, contentType: contentType, requestMutation: requestMutation) + } + } /// Dictionaries and arrays can both be passed to `Resource.request(_:json:contentType:requestMutation:)`. @@ -163,3 +224,16 @@ extension NSDictionary: JSONConvertible { } extension NSArray: JSONConvertible { } extension Dictionary: JSONConvertible { } extension Array: JSONConvertible { } + +/// Helper struct for specifying a file to upload for `Resource.request(_:multipart:files:order:requestMutation:)`. +public struct FilePart { + let filename: String + let type: String + let data: Data + + init(filename: String, type: String, data: Data) { + self.filename = filename + self.type = type + self.data = data + } +} From a85500b7ef1321591b2e904be661a971fceca680 Mon Sep 17 00:00:00 2001 From: dondi Date: Mon, 18 Jun 2018 23:28:38 -0700 Subject: [PATCH 2/3] Match formatting conventions as closely as possible. --- Source/Siesta/Request/RequestCreation.swift | 30 ++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Source/Siesta/Request/RequestCreation.swift b/Source/Siesta/Request/RequestCreation.swift index 35efbace..3ff7768d 100644 --- a/Source/Siesta/Request/RequestCreation.swift +++ b/Source/Siesta/Request/RequestCreation.swift @@ -175,9 +175,11 @@ public extension Resource multipart values: [String:String], files: [String:FilePart]?, order: [String]?, - requestMutation: @escaping RequestMutation = { _ in }) -> Request { - - func getNames() -> [String] { + requestMutation: @escaping RequestMutation = { _ in }) + -> Request + { + func getNames() -> [String] + { if let givenOrder = order { return givenOrder } @@ -188,34 +190,38 @@ public extension Resource } return names - } + } - func append(_ body: NSMutableData, _ line: String) { + func append(_ body: NSMutableData, _ line: String) + { body.append(line.data(using: .utf8)!) - } + } // Derived from https://github.com/bustoutsolutions/siesta/issues/190#issuecomment-294267686 let boundary = "Boundary-\(NSUUID().uuidString)" let contentType = "multipart/form-data; boundary=\(boundary)" let body = NSMutableData() let names = getNames() - names.forEach { name in + names.forEach + { name in append(body, "--\(boundary)\r\n") - if values.keys.contains(name), let value = values[name] { + if values.keys.contains(name), let value = values[name] + { append(body, "Content-Disposition:form-data; name=\"\(name)\"\r\n\r\n") append(body, "\(value)\r\n") - } else if let givenFiles = files, givenFiles.keys.contains(name), let filePart = givenFiles[name] { + } + else if let givenFiles = files, givenFiles.keys.contains(name), let filePart = givenFiles[name] + { append(body, "Content-Disposition:form-data; name=\"\(name)\"; filename=\"\(filePart.filename)\"\r\n") append(body, "Content-Type: \(filePart.type)\r\n\r\n") body.append(filePart.data) append(body, "\r\n") + } } - } append(body, "--\(boundary)--\r\n") return request(method, data: body as Data, contentType: contentType, requestMutation: requestMutation) - } - + } } /// Dictionaries and arrays can both be passed to `Resource.request(_:json:contentType:requestMutation:)`. From f116a866d85e4518b425044cd4b26c3a8e3d46f6 Mon Sep 17 00:00:00 2001 From: dondi Date: Tue, 19 Jun 2018 00:03:21 -0700 Subject: [PATCH 3/3] Reformat FilePart too. --- Source/Siesta/Request/RequestCreation.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/Siesta/Request/RequestCreation.swift b/Source/Siesta/Request/RequestCreation.swift index 3ff7768d..a2558abe 100644 --- a/Source/Siesta/Request/RequestCreation.swift +++ b/Source/Siesta/Request/RequestCreation.swift @@ -232,14 +232,16 @@ extension Dictionary: JSONConvertible { } extension Array: JSONConvertible { } /// Helper struct for specifying a file to upload for `Resource.request(_:multipart:files:order:requestMutation:)`. -public struct FilePart { +public struct FilePart + { let filename: String let type: String let data: Data - init(filename: String, type: String, data: Data) { + init(filename: String, type: String, data: Data) + { self.filename = filename self.type = type self.data = data + } } -}