浏览代码

Added a multipart form data class to the project.

Christian Noon 10 年之前
父节点
当前提交
f3d04f314b
共有 3 个文件被更改,包括 616 次插入0 次删除
  1. 6 0
      Alamofire.xcodeproj/project.pbxproj
  2. 2 0
      Source/Alamofire.swift
  3. 608 0
      Source/MultipartFormData.swift

+ 6 - 0
Alamofire.xcodeproj/project.pbxproj

@@ -7,6 +7,8 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		4C23EB431B327C5B0090E0BC /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */; };
+		4C23EB441B327C5B0090E0BC /* MultipartFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */; };
 		4C256A531B096C770065714F /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C256A501B096C2C0065714F /* BaseTestCase.swift */; };
 		4C256A541B096C770065714F /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C256A501B096C2C0065714F /* BaseTestCase.swift */; };
 		4C341BBA1B1A865A00C1B34D /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; };
@@ -70,6 +72,7 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
+		4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormData.swift; sourceTree = "<group>"; };
 		4C256A501B096C2C0065714F /* BaseTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestCase.swift; sourceTree = "<group>"; };
 		4C341BB91B1A865A00C1B34D /* CacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheTests.swift; sourceTree = "<group>"; };
 		4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLProtocolTests.swift; sourceTree = "<group>"; };
@@ -171,6 +174,7 @@
 			isa = PBXGroup;
 			children = (
 				4CDE2C3C1AF89D4900BABAE5 /* Download.swift */,
+				4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */,
 				4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */,
 				4CDE2C3F1AF89E0700BABAE5 /* Upload.swift */,
 				4CDE2C421AF89F0900BABAE5 /* Validation.swift */,
@@ -416,6 +420,7 @@
 				4CDE2C471AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */,
 				4CDE2C381AF8932A00BABAE5 /* Manager.swift in Sources */,
 				4DD67C251A5C590000ED2280 /* Alamofire.swift in Sources */,
+				4C23EB441B327C5B0090E0BC /* MultipartFormData.swift in Sources */,
 				4CDE2C3E1AF89D4900BABAE5 /* Download.swift in Sources */,
 				4CDE2C441AF89F0900BABAE5 /* Validation.swift in Sources */,
 			);
@@ -431,6 +436,7 @@
 				4CDE2C461AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */,
 				4CDE2C371AF8932A00BABAE5 /* Manager.swift in Sources */,
 				F897FF4119AA800700AB5182 /* Alamofire.swift in Sources */,
+				4C23EB431B327C5B0090E0BC /* MultipartFormData.swift in Sources */,
 				4CDE2C3D1AF89D4900BABAE5 /* Download.swift in Sources */,
 				4CDE2C431AF89F0900BABAE5 /* Validation.swift in Sources */,
 			);

+ 2 - 0
Source/Alamofire.swift

@@ -24,6 +24,8 @@ import Foundation
 
 /// Alamofire errors
 public let AlamofireErrorDomain = "com.alamofire.error"
+public let AlamofireInputStreamReadFailed = -6000
+public let AlamofireOutputStreamWriteFailed = -6001
 
 // MARK: - URLStringConvertible
 

+ 608 - 0
Source/MultipartFormData.swift

@@ -0,0 +1,608 @@
+// MultipartFormData.swift
+//
+// Copyright (c) 2014–2015 Alamofire Software Foundation (http://alamofire.org/)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Foundation
+
+#if os(iOS)
+import MobileCoreServices
+#elseif os(OSX)
+import CoreServices
+#endif
+
+/**
+    Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode 
+    multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead 
+    to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the 
+    data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for 
+    larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset.
+
+    For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well
+    and the w3 form documentation.
+
+    - http://www.ietf.org/rfc/rfc2388.txt
+    - http://www.ietf.org/rfc/rfc2045.txt
+    - http://www.w3.org/TR/html401/interact/forms.html#h-17.13
+*/
+public class MultipartFormData {
+
+    // MARK: - Helper Types
+
+    /**
+        Used to specify whether encoding was successful.
+    */
+    public enum EncodingResult {
+        case Success(NSData)
+        case Failure(NSError)
+    }
+
+    struct EncodingCharacters {
+        static let CRLF = "\r\n"
+    }
+
+    struct BoundaryGenerator {
+        enum BoundaryType {
+            case Initial, Encapsulated, Final
+        }
+
+        static func randomBoundaryKey() -> String {
+            return String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random())
+        }
+
+        static func boundaryData(#boundaryType: BoundaryType, boundaryKey: String, stringEncoding: NSStringEncoding) -> NSData {
+            let boundary: String
+
+            switch boundaryType {
+            case .Initial:
+                boundary = "--\(boundaryKey)\(EncodingCharacters.CRLF)"
+            case .Encapsulated:
+                boundary = "\(EncodingCharacters.CRLF)--\(boundaryKey)\(EncodingCharacters.CRLF)"
+            case .Final:
+                boundary = "\(EncodingCharacters.CRLF)--\(boundaryKey)--\(EncodingCharacters.CRLF)"
+            }
+
+            return boundary.dataUsingEncoding(stringEncoding, allowLossyConversion: false)!
+        }
+    }
+
+    class BodyPart {
+        let headers: [String: String]
+        let bodyStream: NSInputStream
+        var hasInitialBoundary = false
+        var hasFinalBoundary = false
+
+        init(headers: [String: String], bodyStream: NSInputStream) {
+            self.headers = headers
+            self.bodyStream = bodyStream
+        }
+    }
+
+    // MARK: - Properties
+
+    /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
+    public var contentType: String { return "multipart/form-data;boundary=\(self.boundaryKey)" }
+
+    private let stringEncoding: NSStringEncoding
+    private let boundaryKey: String
+    private var bodyParts: [BodyPart]
+    private let streamBufferSize: Int
+
+    // MARK: - Lifecycle
+
+    /**
+        Creates a multipart form data object with the given string encoding.
+
+        :param: stringEncoding The string encoding used to encode the data.
+
+        :returns: The multipart form data object.
+    */
+    public init(stringEncoding: NSStringEncoding) {
+        self.stringEncoding = stringEncoding
+        self.boundaryKey = BoundaryGenerator.randomBoundaryKey()
+        self.bodyParts = []
+
+        /**
+         *  The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more 
+         *  information, please refer to the following article:
+         *    - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html
+         */
+
+        self.streamBufferSize = 1024
+    }
+
+    // MARK: - Body Parts
+
+    /**
+        Creates a body part from the file 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=#{generated filename}` (HTTP Header)
+        - `Content-Type: #{generated mimeType}` (HTTP Header)
+        - Encoded file data
+        - Multipart form boundary
+
+        The filename in the `Content-Disposition` HTTP header is generated from the last path component of the
+        `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.
+    */
+    public func appendBodyPart(fileURL URL: NSURL, name: String) -> NSError? {
+        let fileName: String
+        let mimeType: String
+
+        if let lastPathComponent = URL.lastPathComponent {
+            fileName = lastPathComponent
+        } else {
+            let failureReason = "Failed to extract the fileName of the provided URL: \(URL)"
+            let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
+            return NSError(domain: AlamofireErrorDomain, code: NSURLErrorBadURL, userInfo: userInfo)
+        }
+
+        if let pathExtension = URL.pathExtension {
+            mimeType = mimeTypeForPathExtension(pathExtension)
+        } else {
+            let failureReason = "Failed to extract the file extension of the provided URL: \(URL)"
+            let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
+            return NSError(domain: AlamofireErrorDomain, code: NSURLErrorBadURL, userInfo: userInfo)
+        }
+
+        return appendBodyPart(fileURL: URL, name: name, fileName: fileName, mimeType: mimeType)
+    }
+
+    /**
+        Creates a body part from the file 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: 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.
+        :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? {
+        let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
+
+        var reachableError: NSError?
+
+        if !URL.fileURL {
+            return errorWithCode(NSURLErrorBadURL, failureReason: "The URL does not point to a valid file: \(URL)")
+        } else if !URL.checkResourceIsReachableAndReturnError(&reachableError) {
+            return errorWithCode(NSURLErrorBadURL, failureReason: "The URL is not reachable: \(URL)")
+        }
+
+        if let bodyStream = NSInputStream(URL: URL) {
+            let bodyPart = BodyPart(headers: headers, bodyStream: bodyStream)
+            self.bodyParts.append(bodyPart)
+        } else {
+            let failureReason = "Failed to create an input stream from the URL: \(URL)"
+            return errorWithCode(NSURLErrorCannotOpenFile, failureReason: failureReason)
+        }
+
+        return nil
+    }
+
+    /**
+        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(fileData data: NSData, 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)
+
+        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)
+
+        self.bodyParts.append(bodyPart)
+    }
+
+    /**
+        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
+        - 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.
+    */
+    public func appendBodyPart(#stream: NSInputStream, name: String, fileName: String, mimeType: String) {
+        let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
+        let bodyPart = BodyPart(headers: headers, bodyStream: stream)
+
+        self.bodyParts.append(bodyPart)
+    }
+
+    // MARK: - Data Extraction
+
+    /**
+        Encodes all the appended body parts into a single `NSData` object.
+
+        It is important to note that this method will load all the appended body parts into memory all at the same 
+        time. This method should only be used when the encoded data will have a small memory footprint. For large data 
+        cases, please use the `writeEncodedDataToDisk(fileURL:completionHandler:)` method.
+
+        :returns: EncodingResult containing an `NSData` object if the encoding succeeded, an `NSError` otherwise.
+    */
+    public func encode() -> EncodingResult {
+        var encoded = NSMutableData()
+
+        self.bodyParts.first?.hasInitialBoundary = true
+        self.bodyParts.last?.hasFinalBoundary = true
+
+        for bodyPart in self.bodyParts {
+            let encodedDataResult = encodeBodyPart(bodyPart)
+
+            switch encodedDataResult {
+            case .Failure:
+                return encodedDataResult
+            case let .Success(data):
+                encoded.appendData(data)
+            }
+        }
+
+        return .Success(encoded)
+    }
+
+    /**
+        Writes the appended body parts into the given file URL asynchronously and calls the `completionHandler`
+        when finished.
+
+        This process is facilitated by reading and writing with input and output streams, respectively. Thus,
+        this approach is very memory efficient and should be used for large body part data.
+
+        :param: fileURL           The file URL to write the multipart form data into.
+        :param: completionHandler A closure to be executed when writing is finished.
+    */
+    public func writeEncodedDataToDisk(fileURL: NSURL, completionHandler: (NSError?) -> Void) {
+        if !fileURL.fileURL {
+            let failureReason = "The URL does not point to a valid file: \(fileURL)"
+            let error = errorWithCode(NSURLErrorBadURL, failureReason: failureReason)
+
+            completionHandler(error)
+            return
+        }
+
+        let outputStream: NSOutputStream
+
+        if let possibleOutputStream = NSOutputStream(URL: fileURL, append: false) {
+            outputStream = possibleOutputStream
+        } else {
+            let failureReason = "Failed to create an output stream with the given URL: \(fileURL)"
+            let error = errorWithCode(NSURLErrorCannotOpenFile, failureReason: failureReason)
+
+            completionHandler(error)
+            return
+        }
+
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
+            outputStream.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
+            outputStream.open()
+
+            self.bodyParts.first?.hasInitialBoundary = true
+            self.bodyParts.last?.hasFinalBoundary = true
+
+            var error: NSError?
+
+            for bodyPart in self.bodyParts {
+                if let writeError = self.writeBodyPart(bodyPart, toOutputStream: outputStream) {
+                    error = writeError
+                    break
+                }
+            }
+
+            outputStream.close()
+            outputStream.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
+
+            dispatch_async(dispatch_get_main_queue()) {
+                completionHandler(error)
+            }
+        }
+    }
+
+    // MARK: - Private - Body Part Encoding
+
+    private func encodeBodyPart(bodyPart: BodyPart) -> EncodingResult {
+        let encoded = NSMutableData()
+
+        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
+        encoded.appendData(initialData)
+
+        let headerData = encodeHeaderDataForBodyPart(bodyPart)
+        encoded.appendData(headerData)
+
+        let bodyStreamResult = encodeBodyStreamDataForBodyPart(bodyPart)
+
+        switch bodyStreamResult {
+        case .Failure:
+            return bodyStreamResult
+        case let .Success(data):
+            encoded.appendData(data)
+        }
+
+        if bodyPart.hasFinalBoundary {
+            encoded.appendData(finalBoundaryData())
+        }
+
+        return .Success(encoded)
+    }
+
+    private func encodeHeaderDataForBodyPart(bodyPart: BodyPart) -> NSData {
+        var headerText = ""
+
+        for (key, value) in bodyPart.headers {
+            headerText += "\(key): \(value)\(EncodingCharacters.CRLF)"
+        }
+        headerText += EncodingCharacters.CRLF
+
+        return headerText.dataUsingEncoding(self.stringEncoding, allowLossyConversion: false)!
+    }
+
+    private func encodeBodyStreamDataForBodyPart(bodyPart: BodyPart) -> EncodingResult {
+        let inputStream = bodyPart.bodyStream
+        inputStream.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
+        inputStream.open()
+
+        var error: NSError?
+        let encoded = NSMutableData()
+
+        while inputStream.hasBytesAvailable {
+            var buffer = [UInt8](count: self.streamBufferSize, repeatedValue: 0)
+            let bytesRead = inputStream.read(&buffer, maxLength: self.streamBufferSize)
+
+            if inputStream.streamError != nil {
+                error = inputStream.streamError
+                break
+            }
+
+            if bytesRead < 0 {
+                let failureReason = "Failed to read from input stream: \(inputStream)"
+                error = errorWithCode(AlamofireInputStreamReadFailed, failureReason: failureReason)
+                break
+            }
+
+            encoded.appendBytes(buffer, length: bytesRead)
+        }
+
+        inputStream.close()
+        inputStream.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
+
+        if let error = error {
+            return .Failure(error)
+        }
+
+        return .Success(encoded)
+    }
+
+    // MARK: - Private - Writing Body Part to Output Stream
+
+    private func writeBodyPart(bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) -> NSError? {
+        if let error = writeInitialBoundaryDataForBodyPart(bodyPart, toOutputStream: outputStream) {
+            return error
+        }
+
+        if let error = writeHeaderDataForBodyPart(bodyPart, toOutputStream: outputStream) {
+            return error
+        }
+
+        if let error = writeBodyStreamForBodyPart(bodyPart, toOutputStream: outputStream) {
+            return error
+        }
+
+        if let error = writeFinalBoundaryDataForBodyPart(bodyPart, toOutputStream: outputStream) {
+            return error
+        }
+
+        return nil
+    }
+
+    private func writeInitialBoundaryDataForBodyPart(bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) -> NSError? {
+        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
+        return writeData(initialData, toOutputStream: outputStream)
+    }
+
+    private func writeHeaderDataForBodyPart(bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) -> NSError? {
+        let headerData = encodeHeaderDataForBodyPart(bodyPart)
+        return writeData(headerData, toOutputStream: outputStream)
+    }
+
+    private func writeBodyStreamForBodyPart(bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) -> NSError? {
+        var error: NSError?
+
+        let inputStream = bodyPart.bodyStream
+        inputStream.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
+        inputStream.open()
+
+        while inputStream.hasBytesAvailable {
+            var buffer = [UInt8](count: self.streamBufferSize, repeatedValue: 0)
+            let bytesRead = inputStream.read(&buffer, maxLength: self.streamBufferSize)
+
+            if inputStream.streamError != nil {
+                error = inputStream.streamError
+                break
+            }
+
+            if bytesRead < 0 {
+                let failureReason = "Failed to read from input stream: \(inputStream)"
+                error = errorWithCode(AlamofireInputStreamReadFailed, failureReason: failureReason)
+                break
+            }
+
+            if buffer.count != bytesRead {
+                buffer = Array(buffer[0..<bytesRead])
+            }
+
+            if let writeError = writeBuffer(&buffer, toOutputStream: outputStream) {
+                error = writeError
+                break
+            }
+        }
+
+        inputStream.close()
+        inputStream.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
+
+        return error
+    }
+
+    private func writeFinalBoundaryDataForBodyPart(bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) -> NSError? {
+        if bodyPart.hasFinalBoundary {
+            return writeData(finalBoundaryData(), toOutputStream: outputStream)
+        }
+
+        return nil
+    }
+
+    // MARK: - Private - Writing Buffered Data to Output Stream
+
+    private func writeData(data: NSData, toOutputStream outputStream: NSOutputStream) -> NSError? {
+        var buffer = [UInt8](count: data.length, repeatedValue: 0)
+        data.getBytes(&buffer, length: data.length)
+
+        return writeBuffer(&buffer, toOutputStream: outputStream)
+    }
+
+    private func writeBuffer(inout buffer: [UInt8], toOutputStream outputStream: NSOutputStream) -> NSError? {
+        var error: NSError?
+
+        var bytesToWrite = buffer.count
+
+        while bytesToWrite > 0 {
+            if outputStream.hasSpaceAvailable {
+                let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)
+
+                if outputStream.streamError != nil {
+                    error = outputStream.streamError
+                    break
+                }
+
+                if bytesWritten < 0 {
+                    let failureReason = "Failed to write to output stream: \(outputStream)"
+                    error = errorWithCode(AlamofireOutputStreamWriteFailed, failureReason: failureReason)
+                    break
+                }
+
+                bytesToWrite -= bytesWritten
+
+                if bytesToWrite > 0 {
+                    buffer = Array(buffer[bytesWritten..<buffer.count])
+                }
+            } else if outputStream.streamError != nil {
+                error = outputStream.streamError
+                break
+            }
+        }
+
+        return error
+    }
+
+    // MARK: - Private - Mime Type
+
+    private func mimeTypeForPathExtension(pathExtension: String) -> String {
+        let identifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, nil).takeRetainedValue()
+
+        if let contentType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType) {
+            return contentType.takeRetainedValue() as String
+        }
+
+        return "application/octet-stream"
+    }
+
+    // MARK: - Private - Content Headers
+
+    private func contentHeaders(#name: String) -> [String: String] {
+        return ["Content-Disposition": "form-data; name=\"\(name)\""]
+    }
+
+    private func contentHeaders(#name: String, fileName: String, mimeType: String) -> [String: String] {
+        return [
+            "Content-Disposition": "form-data; name=\"\(name)\"; filename=\"\(fileName)\"",
+            "Content-Type": "\(mimeType)"
+        ]
+    }
+
+    // MARK: - Private - Boundary Encoding
+
+    private func initialBoundaryData() -> NSData {
+        return BoundaryGenerator.boundaryData(boundaryType: .Initial, boundaryKey: self.boundaryKey, stringEncoding: self.stringEncoding)
+    }
+
+    private func encapsulatedBoundaryData() -> NSData {
+        return BoundaryGenerator.boundaryData(boundaryType: .Encapsulated, boundaryKey: self.boundaryKey, stringEncoding: self.stringEncoding)
+    }
+
+    private func finalBoundaryData() -> NSData {
+        return BoundaryGenerator.boundaryData(boundaryType: .Final, boundaryKey: self.boundaryKey, stringEncoding: self.stringEncoding)
+    }
+
+    // MARK: - Private - Errors
+
+    private func errorWithCode(code: Int, failureReason: String) -> NSError {
+        let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
+        return NSError(domain: AlamofireErrorDomain, code: code, userInfo: userInfo)
+    }
+}