2
0
Эх сурвалжийг харах

Merge pull request #596 from Alamofire/feature/multipart_form_data

Christian Noon 10 жил өмнө
parent
commit
13eac9f245

+ 147 - 81
Source/MultipartFormData.swift

@@ -109,6 +109,7 @@ public class MultipartFormData {
     public let boundary: String
 
     private var bodyParts: [BodyPart]
+    private var bodyPartError: NSError?
     private let streamBufferSize: Int
 
     // MARK: - Lifecycle
@@ -133,6 +134,71 @@ public class MultipartFormData {
 
     // MARK: - Body Parts
 
+    /**
+        Creates a body part from the data and appends it to the multipart form data object.
+
+        The body part data will be encoded using the following format:
+
+        - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
+        - Encoded data
+        - Multipart form boundary
+
+        :param: data The data to encode into the multipart form data.
+        :param: name The name to associate with the data in the `Content-Disposition` HTTP header.
+    */
+    public func appendBodyPart(#data: NSData, name: String) {
+        let headers = contentHeaders(name: name)
+        let stream = NSInputStream(data: data)
+        let length = UInt64(data.length)
+
+        appendBodyPart(stream: stream, length: length, headers: headers)
+    }
+
+    /**
+        Creates a body part from the data and appends it to the multipart form data object.
+
+        The body part data will be encoded using the following format:
+
+        - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
+        - `Content-Type: #{generated mimeType}` (HTTP Header)
+        - Encoded data
+        - Multipart form boundary
+
+        :param: data The data to encode into the multipart form data.
+        :param: name The name to associate with the data in the `Content-Disposition` HTTP header.
+        :param: mimeType The MIME type to associate with the data content type in the `Content-Type` HTTP header.
+    */
+    public func appendBodyPart(#data: NSData, name: String, mimeType: String) {
+        let headers = contentHeaders(name: name, mimeType: mimeType)
+        let stream = NSInputStream(data: data)
+        let length = UInt64(data.length)
+
+        appendBodyPart(stream: stream, length: length, headers: headers)
+    }
+
+    /**
+        Creates a body part from the data and appends it to the multipart form data object.
+
+        The body part data will be encoded using the following format:
+
+        - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
+        - `Content-Type: #{mimeType}` (HTTP Header)
+        - Encoded file data
+        - Multipart form boundary
+
+        :param: data     The data to encode into the multipart form data.
+        :param: name     The name to associate with the data in the `Content-Disposition` HTTP header.
+        :param: fileName The filename to associate with the data in the `Content-Disposition` HTTP header.
+        :param: mimeType The MIME type to associate with the data in the `Content-Type` HTTP header.
+    */
+    public func appendBodyPart(#data: NSData, name: String, fileName: String, mimeType: String) {
+        let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
+        let stream = NSInputStream(data: data)
+        let length = UInt64(data.length)
+
+        appendBodyPart(stream: stream, length: length, headers: headers)
+    }
+
     /**
         Creates a body part from the file and appends it to the multipart form data object.
 
@@ -147,24 +213,23 @@ public class MultipartFormData {
         `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the
         system associated MIME type.
 
-        :param: URL  The URL of the file whose content will be encoded into the multipart form data.
-        :param: name The name to associate with the file content in the `Content-Disposition` HTTP header.
-
-        :returns: An `NSError` if an error occurred, `nil` otherwise.
+        :param: fileURL The URL of the file whose content will be encoded into the multipart form data.
+        :param: name    The name to associate with the file content in the `Content-Disposition` HTTP header.
     */
-    public func appendBodyPart(fileURL URL: NSURL, name: String) -> NSError? {
+    public func appendBodyPart(#fileURL: NSURL, name: String) {
         if let
-            fileName = URL.lastPathComponent,
-            pathExtension = URL.pathExtension
+            fileName = fileURL.lastPathComponent,
+            pathExtension = fileURL.pathExtension
         {
             let mimeType = mimeTypeForPathExtension(pathExtension)
-            return appendBodyPart(fileURL: URL, name: name, fileName: fileName, mimeType: mimeType)
-        }
-
-        let failureReason = "Failed to extract the fileName of the provided URL: \(URL)"
-        let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
+            appendBodyPart(fileURL: fileURL, name: name, fileName: fileName, mimeType: mimeType)
+        } else {
+            let failureReason = "Failed to extract the fileName of the provided URL: \(fileURL)"
+            let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
+            let error = NSError(domain: AlamofireErrorDomain, code: NSURLErrorBadURL, userInfo: userInfo)
 
-        return NSError(domain: AlamofireErrorDomain, code: NSURLErrorBadURL, userInfo: userInfo)
+            setBodyPartError(error)
+        }
     }
 
     /**
@@ -177,117 +242,96 @@ public class MultipartFormData {
         - Encoded file data
         - Multipart form boundary
 
-        :param: URL      The URL of the file whose content will be encoded into the multipart form data.
+        :param: fileURL  The URL of the file whose content will be encoded into the multipart form data.
         :param: name     The name to associate with the file content in the `Content-Disposition` HTTP header.
         :param: fileName The filename to associate with the file content in the `Content-Disposition` HTTP header.
         :param: mimeType The MIME type to associate with the file content in the `Content-Type` HTTP header.
-
-        :returns: An `NSError` if an error occurred, `nil` otherwise.
     */
-    public func appendBodyPart(fileURL URL: NSURL, name: String, fileName: String, mimeType: String) -> NSError? {
+    public func appendBodyPart(#fileURL: NSURL, name: String, fileName: String, mimeType: String) {
         let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
         var isDirectory: ObjCBool = false
+        var error: NSError?
 
-        if !URL.fileURL {
-            return errorWithCode(NSURLErrorBadURL, failureReason: "The URL does not point to a file URL: \(URL)")
-        } else if !URL.checkResourceIsReachableAndReturnError(nil) {
-            return errorWithCode(NSURLErrorBadURL, failureReason: "The URL is not reachable: \(URL)")
-        } else if NSFileManager.defaultManager().fileExistsAtPath(URL.path!, isDirectory: &isDirectory) && isDirectory {
-            return errorWithCode(NSURLErrorBadURL, failureReason: "The URL is a directory, not a file: \(URL)")
+        if !fileURL.fileURL {
+            error = errorWithCode(NSURLErrorBadURL, failureReason: "The URL does not point to a file URL: \(fileURL)")
+        } else if !fileURL.checkResourceIsReachableAndReturnError(nil) {
+            error = errorWithCode(NSURLErrorBadURL, failureReason: "The URL is not reachable: \(fileURL)")
+        } else if NSFileManager.defaultManager().fileExistsAtPath(fileURL.path!, isDirectory: &isDirectory) && isDirectory {
+            error = errorWithCode(NSURLErrorBadURL, failureReason: "The URL is a directory, not a file: \(fileURL)")
         }
 
-        let bodyContentLength: UInt64
-        var fileAttributesError: NSError?
+        if let error = error {
+            setBodyPartError(error)
+            return
+        }
+
+        let length: UInt64
 
         if let
-            path = URL.path,
-            attributes = NSFileManager.defaultManager().attributesOfItemAtPath(path, error: &fileAttributesError),
+            path = fileURL.path,
+            attributes = NSFileManager.defaultManager().attributesOfItemAtPath(path, error: &error),
             fileSize = (attributes[NSFileSize] as? NSNumber)?.unsignedLongLongValue
         {
-            bodyContentLength = fileSize
+            length = fileSize
         } else {
-            return errorWithCode(NSURLErrorBadURL, failureReason: "Could not fetch attributes from the URL: \(URL)")
+            let failureReason = "Could not fetch attributes from the URL: \(fileURL)"
+            let error = errorWithCode(NSURLErrorBadURL, failureReason: failureReason)
+
+            setBodyPartError(error)
+
+            return
         }
 
-        if let bodyStream = NSInputStream(URL: URL) {
-            let bodyPart = BodyPart(headers: headers, bodyStream: bodyStream, bodyContentLength: bodyContentLength)
-            self.bodyParts.append(bodyPart)
+        if let stream = NSInputStream(URL: fileURL) {
+            appendBodyPart(stream: stream, length: length, headers: headers)
         } else {
-            let failureReason = "Failed to create an input stream from the URL: \(URL)"
-            return errorWithCode(NSURLErrorCannotOpenFile, failureReason: failureReason)
-        }
+            let failureReason = "Failed to create an input stream from the URL: \(fileURL)"
+            let error = errorWithCode(NSURLErrorCannotOpenFile, failureReason: failureReason)
 
-        return nil
+            setBodyPartError(error)
+        }
     }
 
     /**
-        Creates a body part from the data and appends it to the multipart form data object.
+        Creates a body part from the stream and appends it to the multipart form data object.
 
         The body part data will be encoded using the following format:
 
         - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
         - `Content-Type: #{mimeType}` (HTTP Header)
-        - Encoded file data
+        - Encoded stream data
         - Multipart form boundary
 
-        :param: data     The data to encode into the multipart form data.
-        :param: name     The name to associate with the data in the `Content-Disposition` HTTP header.
-        :param: fileName The filename to associate with the data in the `Content-Disposition` HTTP header.
-        :param: mimeType The MIME type to associate with the data in the `Content-Type` HTTP header.
+        :param: stream   The input stream to encode in the multipart form data.
+        :param: length   The content length of the stream.
+        :param: name     The name to associate with the stream content in the `Content-Disposition` HTTP header.
+        :param: fileName The filename to associate with the stream content in the `Content-Disposition` HTTP header.
+        :param: mimeType The MIME type to associate with the stream content in the `Content-Type` HTTP header.
     */
-    public func appendBodyPart(fileData data: NSData, name: String, fileName: String, mimeType: String) {
+    public func appendBodyPart(#stream: NSInputStream, length: UInt64, name: String, fileName: String, mimeType: String) {
         let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
-        let bodyStream = NSInputStream(data: data)
-        let bodyContentLength = UInt64(data.length)
-        let bodyPart = BodyPart(headers: headers, bodyStream: bodyStream, bodyContentLength: bodyContentLength)
-
-        self.bodyParts.append(bodyPart)
-    }
-
-    /**
-        Creates a body part from the data and appends it to the multipart form data object.
-
-        The body part data will be encoded using the following format:
-
-        - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
-        - Encoded file data
-        - Multipart form boundary
-
-        :param: data The data to encode into the multipart form data.
-        :param: name The name to associate with the data in the `Content-Disposition` HTTP header.
-    */
-    public func appendBodyPart(#data: NSData, name: String) {
-        let headers = contentHeaders(name: name)
-        let bodyStream = NSInputStream(data: data)
-        let bodyContentLength = UInt64(data.length)
-        let bodyPart = BodyPart(headers: headers, bodyStream: bodyStream, bodyContentLength: bodyContentLength)
-
-        self.bodyParts.append(bodyPart)
+        appendBodyPart(stream: stream, length: length, headers: headers)
     }
 
     /**
-        Creates a body part from the stream and appends it to the multipart form data object.
+        Creates a body part with the headers, stream and length and appends it to the multipart form data object.
 
         The body part data will be encoded using the following format:
 
-        - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
-        - `Content-Type: #{mimeType}` (HTTP Header)
-        - Encoded file data
+        - HTTP headers
+        - Encoded stream data
         - Multipart form boundary
 
-        :param: stream   The input stream to encode in the multipart form data.
-        :param: name     The name to associate with the stream content in the `Content-Disposition` HTTP header.
-        :param: fileName The filename to associate with the stream content in the `Content-Disposition` HTTP header.
-        :param: mimeType The MIME type to associate with the stream content in the `Content-Type` HTTP header.
+        :param: stream  The input stream to encode in the multipart form data.
+        :param: length  The content length of the stream.
+        :param: headers The HTTP headers for the body part.
     */
-    public func appendBodyPart(#stream: NSInputStream, name: String, fileName: String, length: UInt64, mimeType: String) {
-        let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
+    public func appendBodyPart(#stream: NSInputStream, length: UInt64, headers: [String: String]) {
         let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
-
         self.bodyParts.append(bodyPart)
     }
 
-    // MARK: - Data Extraction
+    // MARK: - Data Encoding
 
     /**
         Encodes all the appended body parts into a single `NSData` object.
@@ -299,6 +343,10 @@ public class MultipartFormData {
         :returns: EncodingResult containing an `NSData` object if the encoding succeeded, an `NSError` otherwise.
     */
     public func encode() -> EncodingResult {
+        if let bodyPartError = self.bodyPartError {
+            return .Failure(bodyPartError)
+        }
+
         var encoded = NSMutableData()
 
         self.bodyParts.first?.hasInitialBoundary = true
@@ -329,6 +377,11 @@ public class MultipartFormData {
         :param: completionHandler A closure to be executed when writing is finished.
     */
     public func writeEncodedDataToDisk(fileURL: NSURL, completionHandler: (NSError?) -> Void) {
+        if let bodyPartError = self.bodyPartError {
+            completionHandler(bodyPartError)
+            return
+        }
+
         var error: NSError?
 
         if let path = fileURL.path where NSFileManager.defaultManager().fileExistsAtPath(path) {
@@ -598,6 +651,13 @@ public class MultipartFormData {
         return ["Content-Disposition": "form-data; name=\"\(name)\""]
     }
 
+    private func contentHeaders(#name: String, mimeType: String) -> [String: String] {
+        return [
+            "Content-Disposition": "form-data; name=\"\(name)\"",
+            "Content-Type": "\(mimeType)"
+        ]
+    }
+
     private func contentHeaders(#name: String, fileName: String, mimeType: String) -> [String: String] {
         return [
             "Content-Disposition": "form-data; name=\"\(name)\"; filename=\"\(fileName)\"",
@@ -621,6 +681,12 @@ public class MultipartFormData {
 
     // MARK: - Private - Errors
 
+    private func setBodyPartError(error: NSError) {
+        if self.bodyPartError == nil {
+            self.bodyPartError = error
+        }
+    }
+
     private func errorWithCode(code: Int, failureReason: String) -> NSError {
         let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
         return NSError(domain: AlamofireErrorDomain, code: code, userInfo: userInfo)

+ 54 - 16
Tests/MultipartFormDataTests.swift

@@ -141,8 +141,8 @@ class MultipartFormDataEncodingTestCase: BaseTestCase {
 
         // When
         multipartFormData.appendBodyPart(data: french, name: "french")
-        multipartFormData.appendBodyPart(data: japanese, name: "japanese")
-        multipartFormData.appendBodyPart(data: emoji, name: "emoji")
+        multipartFormData.appendBodyPart(data: japanese, name: "japanese", mimeType: "text/plain")
+        multipartFormData.appendBodyPart(data: emoji, name: "emoji", mimeType: "text/plain")
         let encodingResult = multipartFormData.encode()
 
         // Then
@@ -155,10 +155,12 @@ class MultipartFormDataEncodingTestCase: BaseTestCase {
                 "Content-Disposition: form-data; name=\"french\"\(self.CRLF)\(self.CRLF)" +
                 "français" +
                 BoundaryGenerator.boundary(boundaryType: .Encapsulated, boundaryKey: boundary) +
-                "Content-Disposition: form-data; name=\"japanese\"\(self.CRLF)\(self.CRLF)" +
+                "Content-Disposition: form-data; name=\"japanese\"\(self.CRLF)" +
+                "Content-Type: text/plain\(self.CRLF)\(self.CRLF)" +
                 "日本語" +
                 BoundaryGenerator.boundary(boundaryType: .Encapsulated, boundaryKey: boundary) +
-                "Content-Disposition: form-data; name=\"emoji\"\(self.CRLF)\(self.CRLF)" +
+                "Content-Disposition: form-data; name=\"emoji\"\(self.CRLF)" +
+                "Content-Type: text/plain\(self.CRLF)\(self.CRLF)" +
                 "😃👍🏻🍻🎉" +
                 BoundaryGenerator.boundary(boundaryType: .Final, boundaryKey: boundary)
             ).dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
@@ -248,9 +250,9 @@ class MultipartFormDataEncodingTestCase: BaseTestCase {
         // When
         multipartFormData.appendBodyPart(
             stream: unicornStream,
+            length: unicornDataLength,
             name: "unicorn",
             fileName: "unicorn.png",
-            length: unicornDataLength,
             mimeType: "image/png"
         )
         let encodingResult = multipartFormData.encode()
@@ -291,16 +293,16 @@ class MultipartFormDataEncodingTestCase: BaseTestCase {
         // When
         multipartFormData.appendBodyPart(
             stream: unicornStream,
+            length: unicornDataLength,
             name: "unicorn",
             fileName: "unicorn.png",
-            length: unicornDataLength,
             mimeType: "image/png"
         )
         multipartFormData.appendBodyPart(
             stream: rainbowStream,
+            length: rainbowDataLength,
             name: "rainbow",
             fileName: "rainbow.jpg",
-            length: rainbowDataLength,
             mimeType: "image/jpeg"
         )
         let encodingResult = multipartFormData.encode()
@@ -350,9 +352,9 @@ class MultipartFormDataEncodingTestCase: BaseTestCase {
         multipartFormData.appendBodyPart(fileURL: unicornImageURL, name: "unicorn")
         multipartFormData.appendBodyPart(
             stream: rainbowStream,
+            length: rainbowDataLength,
             name: "rainbow",
             fileName: "rainbow.jpg",
-            length: rainbowDataLength,
             mimeType: "image/jpeg"
         )
         let encodingResult = multipartFormData.encode()
@@ -591,9 +593,9 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
         // When
         multipartFormData.appendBodyPart(
             stream: unicornStream,
+            length: unicornDataLength,
             name: "unicorn",
             fileName: "unicorn.png",
-            length: unicornDataLength,
             mimeType: "image/png"
         )
 
@@ -646,16 +648,16 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
         // When
         multipartFormData.appendBodyPart(
             stream: unicornStream,
+            length: unicornDataLength,
             name: "unicorn",
             fileName: "unicorn.png",
-            length: unicornDataLength,
             mimeType: "image/png"
         )
         multipartFormData.appendBodyPart(
             stream: rainbowStream,
+            length: rainbowDataLength,
             name: "rainbow",
             fileName: "rainbow.jpg",
-            length: rainbowDataLength,
             mimeType: "image/jpeg"
         )
 
@@ -717,9 +719,9 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
         multipartFormData.appendBodyPart(fileURL: unicornImageURL, name: "unicorn")
         multipartFormData.appendBodyPart(
             stream: rainbowStream,
+            length: rainbowDataLength,
             name: "rainbow",
             fileName: "rainbow.jpg",
-            length: rainbowDataLength,
             mimeType: "image/jpeg"
         )
 
@@ -774,8 +776,17 @@ class MultipartFormDataFailureTestCase: BaseTestCase {
         let fileURL = NSURL(string: "")!
         let multipartFormData = MultipartFormData()
 
+        var error: NSError?
+
         // When
-        let error = multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data")
+        multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data")
+
+        switch multipartFormData.encode() {
+        case .Failure(let encodingError):
+            error = encodingError
+        default:
+            break
+        }
 
         // Then
         XCTAssertNotNil(error, "error should not be nil")
@@ -798,8 +809,17 @@ class MultipartFormDataFailureTestCase: BaseTestCase {
         let fileURL = NSURL(string: "http://example.com/image.jpg")!
         let multipartFormData = MultipartFormData()
 
+        var error: NSError?
+
         // When
-        let error = multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data")
+        multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data")
+
+        switch multipartFormData.encode() {
+        case .Failure(let encodingError):
+            error = encodingError
+        default:
+            break
+        }
 
         // Then
         XCTAssertNotNil(error, "error should not be nil")
@@ -822,8 +842,17 @@ class MultipartFormDataFailureTestCase: BaseTestCase {
         let fileURL = NSURL(fileURLWithPath: NSTemporaryDirectory().stringByAppendingPathComponent("does_not_exist.jpg"))!
         let multipartFormData = MultipartFormData()
 
+        var error: NSError?
+
         // When
-        let error = multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data")
+        multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data")
+
+        switch multipartFormData.encode() {
+        case .Failure(let encodingError):
+            error = encodingError
+        default:
+            break
+        }
 
         // Then
         XCTAssertNotNil(error, "error should not be nil")
@@ -846,8 +875,17 @@ class MultipartFormDataFailureTestCase: BaseTestCase {
         let directoryURL = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)!
         let multipartFormData = MultipartFormData()
 
+        var error: NSError?
+
         // When
-        let error = multipartFormData.appendBodyPart(fileURL: directoryURL, name: "empty_data")
+        multipartFormData.appendBodyPart(fileURL: directoryURL, name: "empty_data")
+
+        switch multipartFormData.encode() {
+        case .Failure(let encodingError):
+            error = encodingError
+        default:
+            break
+        }
 
         // Then
         XCTAssertNotNil(error, "error should not be nil")