MultipartFormData.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. //
  2. // MultipartFormData.swift
  3. //
  4. // Copyright (c) 2014-2016 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. import Foundation
  25. #if os(iOS) || os(watchOS) || os(tvOS)
  26. import MobileCoreServices
  27. #elseif os(OSX)
  28. import CoreServices
  29. #endif
  30. /**
  31. Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode
  32. multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead
  33. to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the
  34. data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for
  35. larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset.
  36. For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well
  37. and the w3 form documentation.
  38. - https://www.ietf.org/rfc/rfc2388.txt
  39. - https://www.ietf.org/rfc/rfc2045.txt
  40. - https://www.w3.org/TR/html401/interact/forms.html#h-17.13
  41. */
  42. public class MultipartFormData {
  43. // MARK: - Helper Types
  44. struct EncodingCharacters {
  45. static let CRLF = "\r\n"
  46. }
  47. struct BoundaryGenerator {
  48. enum BoundaryType {
  49. case initial, encapsulated, final
  50. }
  51. static func randomBoundary() -> String {
  52. return String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random())
  53. }
  54. static func boundaryData(boundaryType: BoundaryType, boundary: String) -> Data {
  55. let boundaryText: String
  56. switch boundaryType {
  57. case .initial:
  58. boundaryText = "--\(boundary)\(EncodingCharacters.CRLF)"
  59. case .encapsulated:
  60. boundaryText = "\(EncodingCharacters.CRLF)--\(boundary)\(EncodingCharacters.CRLF)"
  61. case .final:
  62. boundaryText = "\(EncodingCharacters.CRLF)--\(boundary)--\(EncodingCharacters.CRLF)"
  63. }
  64. return boundaryText.data(using: String.Encoding.utf8, allowLossyConversion: false)!
  65. }
  66. }
  67. class BodyPart {
  68. let headers: [String: String]
  69. let bodyStream: InputStream
  70. let bodyContentLength: UInt64
  71. var hasInitialBoundary = false
  72. var hasFinalBoundary = false
  73. init(headers: [String: String], bodyStream: InputStream, bodyContentLength: UInt64) {
  74. self.headers = headers
  75. self.bodyStream = bodyStream
  76. self.bodyContentLength = bodyContentLength
  77. }
  78. }
  79. // MARK: - Properties
  80. /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
  81. public var contentType: String { return "multipart/form-data; boundary=\(boundary)" }
  82. /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries.
  83. public var contentLength: UInt64 { return bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
  84. /// The boundary used to separate the body parts in the encoded form data.
  85. public let boundary: String
  86. private var bodyParts: [BodyPart]
  87. private var bodyPartError: NSError?
  88. private let streamBufferSize: Int
  89. // MARK: - Lifecycle
  90. /**
  91. Creates a multipart form data object.
  92. - returns: The multipart form data object.
  93. */
  94. public init() {
  95. self.boundary = BoundaryGenerator.randomBoundary()
  96. self.bodyParts = []
  97. /**
  98. * The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more
  99. * information, please refer to the following article:
  100. * - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html
  101. */
  102. self.streamBufferSize = 1024
  103. }
  104. // MARK: - Body Parts
  105. /**
  106. Creates a body part from the data and appends it to the multipart form data object.
  107. The body part data will be encoded using the following format:
  108. - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
  109. - Encoded data
  110. - Multipart form boundary
  111. - parameter data: The data to encode into the multipart form data.
  112. - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header.
  113. */
  114. public func appendBodyPart(data: Data, name: String) {
  115. let headers = contentHeaders(name: name)
  116. let stream = InputStream(data: data)
  117. let length = UInt64(data.count)
  118. appendBodyPart(stream: stream, length: length, headers: headers)
  119. }
  120. /**
  121. Creates a body part from the data and appends it to the multipart form data object.
  122. The body part data will be encoded using the following format:
  123. - `Content-Disposition: form-data; name=#{name}` (HTTP Header)
  124. - `Content-Type: #{generated mimeType}` (HTTP Header)
  125. - Encoded data
  126. - Multipart form boundary
  127. - parameter data: The data to encode into the multipart form data.
  128. - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header.
  129. - parameter mimeType: The MIME type to associate with the data content type in the `Content-Type` HTTP header.
  130. */
  131. public func appendBodyPart(data: Data, name: String, mimeType: String) {
  132. let headers = contentHeaders(name: name, mimeType: mimeType)
  133. let stream = InputStream(data: data)
  134. let length = UInt64(data.count)
  135. appendBodyPart(stream: stream, length: length, headers: headers)
  136. }
  137. /**
  138. Creates a body part from the data and appends it to the multipart form data object.
  139. The body part data will be encoded using the following format:
  140. - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
  141. - `Content-Type: #{mimeType}` (HTTP Header)
  142. - Encoded file data
  143. - Multipart form boundary
  144. - parameter data: The data to encode into the multipart form data.
  145. - parameter name: The name to associate with the data in the `Content-Disposition` HTTP header.
  146. - parameter fileName: The filename to associate with the data in the `Content-Disposition` HTTP header.
  147. - parameter mimeType: The MIME type to associate with the data in the `Content-Type` HTTP header.
  148. */
  149. public func appendBodyPart(data: Data, name: String, fileName: String, mimeType: String) {
  150. let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
  151. let stream = InputStream(data: data)
  152. let length = UInt64(data.count)
  153. appendBodyPart(stream: stream, length: length, headers: headers)
  154. }
  155. /**
  156. Creates a body part from the file and appends it to the multipart form data object.
  157. The body part data will be encoded using the following format:
  158. - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header)
  159. - `Content-Type: #{generated mimeType}` (HTTP Header)
  160. - Encoded file data
  161. - Multipart form boundary
  162. The filename in the `Content-Disposition` HTTP header is generated from the last path component of the
  163. `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the
  164. system associated MIME type.
  165. - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data.
  166. - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header.
  167. */
  168. public func appendBodyPart(fileURL: URL, name: String) {
  169. if let fileName = fileURL.lastPathComponent,
  170. let pathExtension = fileURL.pathExtension
  171. {
  172. let mimeType = mimeTypeForPathExtension(pathExtension)
  173. appendBodyPart(fileURL: fileURL, name: name, fileName: fileName, mimeType: mimeType)
  174. } else {
  175. let failureReason = "Failed to extract the fileName of the provided URL: \(fileURL)"
  176. setBodyPartError(code: NSURLErrorBadURL, failureReason: failureReason)
  177. }
  178. }
  179. /**
  180. Creates a body part from the file and appends it to the multipart form data object.
  181. The body part data will be encoded using the following format:
  182. - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header)
  183. - Content-Type: #{mimeType} (HTTP Header)
  184. - Encoded file data
  185. - Multipart form boundary
  186. - parameter fileURL: The URL of the file whose content will be encoded into the multipart form data.
  187. - parameter name: The name to associate with the file content in the `Content-Disposition` HTTP header.
  188. - parameter fileName: The filename to associate with the file content in the `Content-Disposition` HTTP header.
  189. - parameter mimeType: The MIME type to associate with the file content in the `Content-Type` HTTP header.
  190. */
  191. public func appendBodyPart(fileURL: URL, name: String, fileName: String, mimeType: String) {
  192. let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
  193. //============================================================
  194. // Check 1 - is file URL?
  195. //============================================================
  196. guard fileURL.isFileURL else {
  197. let failureReason = "The file URL does not point to a file URL: \(fileURL)"
  198. setBodyPartError(code: NSURLErrorBadURL, failureReason: failureReason)
  199. return
  200. }
  201. //============================================================
  202. // Check 2 - is file URL reachable?
  203. //============================================================
  204. var isReachable = true
  205. if #available(OSX 10.10, *) {
  206. isReachable = (fileURL as NSURL).checkPromisedItemIsReachableAndReturnError(nil)
  207. }
  208. guard isReachable else {
  209. setBodyPartError(code: NSURLErrorBadURL, failureReason: "The file URL is not reachable: \(fileURL)")
  210. return
  211. }
  212. //============================================================
  213. // Check 3 - is file URL a directory?
  214. //============================================================
  215. var isDirectory: ObjCBool = false
  216. guard let path = fileURL.path,
  217. FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory else
  218. {
  219. let failureReason = "The file URL is a directory, not a file: \(fileURL)"
  220. setBodyPartError(code: NSURLErrorBadURL, failureReason: failureReason)
  221. return
  222. }
  223. //============================================================
  224. // Check 4 - can the file size be extracted?
  225. //============================================================
  226. var bodyContentLength: UInt64?
  227. do {
  228. if let path = fileURL.path,
  229. let fileSize = try FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber
  230. {
  231. bodyContentLength = fileSize.uint64Value
  232. }
  233. }
  234. catch {
  235. // No Op
  236. }
  237. guard let length = bodyContentLength else {
  238. let failureReason = "Could not fetch attributes from the file URL: \(fileURL)"
  239. setBodyPartError(code: NSURLErrorBadURL, failureReason: failureReason)
  240. return
  241. }
  242. //============================================================
  243. // Check 5 - can a stream be created from file URL?
  244. //============================================================
  245. guard let stream = InputStream(url: fileURL) else {
  246. let failureReason = "Failed to create an input stream from the file URL: \(fileURL)"
  247. setBodyPartError(code: NSURLErrorCannotOpenFile, failureReason: failureReason)
  248. return
  249. }
  250. appendBodyPart(stream: stream, length: length, headers: headers)
  251. }
  252. /**
  253. Creates a body part from the stream and appends it to the multipart form data object.
  254. The body part data will be encoded using the following format:
  255. - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
  256. - `Content-Type: #{mimeType}` (HTTP Header)
  257. - Encoded stream data
  258. - Multipart form boundary
  259. - parameter stream: The input stream to encode in the multipart form data.
  260. - parameter length: The content length of the stream.
  261. - parameter name: The name to associate with the stream content in the `Content-Disposition` HTTP header.
  262. - parameter fileName: The filename to associate with the stream content in the `Content-Disposition` HTTP header.
  263. - parameter mimeType: The MIME type to associate with the stream content in the `Content-Type` HTTP header.
  264. */
  265. public func appendBodyPart(
  266. stream: InputStream,
  267. length: UInt64,
  268. name: String,
  269. fileName: String,
  270. mimeType: String)
  271. {
  272. let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType)
  273. appendBodyPart(stream: stream, length: length, headers: headers)
  274. }
  275. /**
  276. Creates a body part with the headers, stream and length and appends it to the multipart form data object.
  277. The body part data will be encoded using the following format:
  278. - HTTP headers
  279. - Encoded stream data
  280. - Multipart form boundary
  281. - parameter stream: The input stream to encode in the multipart form data.
  282. - parameter length: The content length of the stream.
  283. - parameter headers: The HTTP headers for the body part.
  284. */
  285. public func appendBodyPart(stream: InputStream, length: UInt64, headers: [String: String]) {
  286. let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
  287. bodyParts.append(bodyPart)
  288. }
  289. // MARK: - Data Encoding
  290. /**
  291. Encodes all the appended body parts into a single `NSData` object.
  292. It is important to note that this method will load all the appended body parts into memory all at the same
  293. time. This method should only be used when the encoded data will have a small memory footprint. For large data
  294. cases, please use the `writeEncodedDataToDisk(fileURL:completionHandler:)` method.
  295. - throws: An `NSError` if encoding encounters an error.
  296. - returns: The encoded `NSData` if encoding is successful.
  297. */
  298. public func encode() throws -> Data {
  299. if let bodyPartError = bodyPartError {
  300. throw bodyPartError
  301. }
  302. let encoded = NSMutableData()
  303. bodyParts.first?.hasInitialBoundary = true
  304. bodyParts.last?.hasFinalBoundary = true
  305. for bodyPart in bodyParts {
  306. let encodedData = try encodeBodyPart(bodyPart)
  307. encoded.append(encodedData)
  308. }
  309. return encoded as Data
  310. }
  311. /**
  312. Writes the appended body parts into the given file URL.
  313. This process is facilitated by reading and writing with input and output streams, respectively. Thus,
  314. this approach is very memory efficient and should be used for large body part data.
  315. - parameter fileURL: The file URL to write the multipart form data into.
  316. - throws: An `NSError` if encoding encounters an error.
  317. */
  318. public func writeEncodedDataToDisk(_ fileURL: URL) throws {
  319. if let bodyPartError = bodyPartError {
  320. throw bodyPartError
  321. }
  322. if let path = fileURL.path, FileManager.default.fileExists(atPath: path) {
  323. let failureReason = "A file already exists at the given file URL: \(fileURL)"
  324. throw Error.error(domain: NSURLErrorDomain, code: NSURLErrorBadURL, failureReason: failureReason)
  325. } else if !fileURL.isFileURL {
  326. let failureReason = "The URL does not point to a valid file: \(fileURL)"
  327. throw Error.error(domain: NSURLErrorDomain, code: NSURLErrorBadURL, failureReason: failureReason)
  328. }
  329. let outputStream: NSOutputStream
  330. if let possibleOutputStream = NSOutputStream(url: fileURL, append: false) {
  331. outputStream = possibleOutputStream
  332. } else {
  333. let failureReason = "Failed to create an output stream with the given URL: \(fileURL)"
  334. throw Error.error(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, failureReason: failureReason)
  335. }
  336. outputStream.open()
  337. self.bodyParts.first?.hasInitialBoundary = true
  338. self.bodyParts.last?.hasFinalBoundary = true
  339. for bodyPart in self.bodyParts {
  340. try writeBodyPart(bodyPart, toOutputStream: outputStream)
  341. }
  342. outputStream.close()
  343. }
  344. // MARK: - Private - Body Part Encoding
  345. private func encodeBodyPart(_ bodyPart: BodyPart) throws -> Data {
  346. let encoded = NSMutableData()
  347. let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
  348. encoded.append(initialData)
  349. let headerData = encodeHeaderDataForBodyPart(bodyPart)
  350. encoded.append(headerData)
  351. let bodyStreamData = try encodeBodyStreamDataForBodyPart(bodyPart)
  352. encoded.append(bodyStreamData)
  353. if bodyPart.hasFinalBoundary {
  354. encoded.append(finalBoundaryData())
  355. }
  356. return encoded as Data
  357. }
  358. private func encodeHeaderDataForBodyPart(_ bodyPart: BodyPart) -> Data {
  359. var headerText = ""
  360. for (key, value) in bodyPart.headers {
  361. headerText += "\(key): \(value)\(EncodingCharacters.CRLF)"
  362. }
  363. headerText += EncodingCharacters.CRLF
  364. return headerText.data(using: String.Encoding.utf8, allowLossyConversion: false)!
  365. }
  366. private func encodeBodyStreamDataForBodyPart(_ bodyPart: BodyPart) throws -> Data {
  367. let inputStream = bodyPart.bodyStream
  368. inputStream.open()
  369. var error: NSError?
  370. let encoded = NSMutableData()
  371. while inputStream.hasBytesAvailable {
  372. var buffer = [UInt8](repeating: 0, count: streamBufferSize)
  373. let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
  374. if inputStream.streamError != nil {
  375. error = inputStream.streamError
  376. break
  377. }
  378. if bytesRead > 0 {
  379. encoded.append(buffer, length: bytesRead)
  380. } else if bytesRead < 0 {
  381. let failureReason = "Failed to read from input stream: \(inputStream)"
  382. error = Error.error(domain: NSURLErrorDomain, code: .inputStreamReadFailed, failureReason: failureReason)
  383. break
  384. } else {
  385. break
  386. }
  387. }
  388. inputStream.close()
  389. if let error = error {
  390. throw error
  391. }
  392. return encoded as Data
  393. }
  394. // MARK: - Private - Writing Body Part to Output Stream
  395. private func writeBodyPart(_ bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) throws {
  396. try writeInitialBoundaryDataForBodyPart(bodyPart, toOutputStream: outputStream)
  397. try writeHeaderDataForBodyPart(bodyPart, toOutputStream: outputStream)
  398. try writeBodyStreamForBodyPart(bodyPart, toOutputStream: outputStream)
  399. try writeFinalBoundaryDataForBodyPart(bodyPart, toOutputStream: outputStream)
  400. }
  401. private func writeInitialBoundaryDataForBodyPart(
  402. _ bodyPart: BodyPart,
  403. toOutputStream outputStream: NSOutputStream)
  404. throws
  405. {
  406. let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
  407. return try writeData(initialData, toOutputStream: outputStream)
  408. }
  409. private func writeHeaderDataForBodyPart(_ bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) throws {
  410. let headerData = encodeHeaderDataForBodyPart(bodyPart)
  411. return try writeData(headerData, toOutputStream: outputStream)
  412. }
  413. private func writeBodyStreamForBodyPart(_ bodyPart: BodyPart, toOutputStream outputStream: NSOutputStream) throws {
  414. let inputStream = bodyPart.bodyStream
  415. inputStream.open()
  416. while inputStream.hasBytesAvailable {
  417. var buffer = [UInt8](repeating: 0, count: streamBufferSize)
  418. let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
  419. if let streamError = inputStream.streamError {
  420. throw streamError
  421. }
  422. if bytesRead > 0 {
  423. if buffer.count != bytesRead {
  424. buffer = Array(buffer[0..<bytesRead])
  425. }
  426. try writeBuffer(&buffer, toOutputStream: outputStream)
  427. } else if bytesRead < 0 {
  428. let failureReason = "Failed to read from input stream: \(inputStream)"
  429. throw Error.error(domain: NSURLErrorDomain, code: .inputStreamReadFailed, failureReason: failureReason)
  430. } else {
  431. break
  432. }
  433. }
  434. inputStream.close()
  435. }
  436. private func writeFinalBoundaryDataForBodyPart(
  437. _ bodyPart: BodyPart,
  438. toOutputStream outputStream: NSOutputStream)
  439. throws
  440. {
  441. if bodyPart.hasFinalBoundary {
  442. return try writeData(finalBoundaryData(), toOutputStream: outputStream)
  443. }
  444. }
  445. // MARK: - Private - Writing Buffered Data to Output Stream
  446. private func writeData(_ data: Data, toOutputStream outputStream: NSOutputStream) throws {
  447. var buffer = [UInt8](repeating: 0, count: data.count)
  448. (data as NSData).getBytes(&buffer, length: data.count)
  449. return try writeBuffer(&buffer, toOutputStream: outputStream)
  450. }
  451. private func writeBuffer(_ buffer: inout [UInt8], toOutputStream outputStream: NSOutputStream) throws {
  452. var bytesToWrite = buffer.count
  453. while bytesToWrite > 0 {
  454. if outputStream.hasSpaceAvailable {
  455. let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)
  456. if let streamError = outputStream.streamError {
  457. throw streamError
  458. }
  459. if bytesWritten < 0 {
  460. let failureReason = "Failed to write to output stream: \(outputStream)"
  461. throw Error.error(domain: NSURLErrorDomain, code: .outputStreamWriteFailed, failureReason: failureReason)
  462. }
  463. bytesToWrite -= bytesWritten
  464. if bytesToWrite > 0 {
  465. buffer = Array(buffer[bytesWritten..<buffer.count])
  466. }
  467. } else if let streamError = outputStream.streamError {
  468. throw streamError
  469. }
  470. }
  471. }
  472. // MARK: - Private - Mime Type
  473. private func mimeTypeForPathExtension(_ pathExtension: String) -> String {
  474. if let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, nil)?.takeRetainedValue(),
  475. let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue()
  476. {
  477. return contentType as String
  478. }
  479. return "application/octet-stream"
  480. }
  481. // MARK: - Private - Content Headers
  482. private func contentHeaders(name: String) -> [String: String] {
  483. return ["Content-Disposition": "form-data; name=\"\(name)\""]
  484. }
  485. private func contentHeaders(name: String, mimeType: String) -> [String: String] {
  486. return [
  487. "Content-Disposition": "form-data; name=\"\(name)\"",
  488. "Content-Type": "\(mimeType)"
  489. ]
  490. }
  491. private func contentHeaders(name: String, fileName: String, mimeType: String) -> [String: String] {
  492. return [
  493. "Content-Disposition": "form-data; name=\"\(name)\"; filename=\"\(fileName)\"",
  494. "Content-Type": "\(mimeType)"
  495. ]
  496. }
  497. // MARK: - Private - Boundary Encoding
  498. private func initialBoundaryData() -> Data {
  499. return BoundaryGenerator.boundaryData(boundaryType: .initial, boundary: boundary)
  500. }
  501. private func encapsulatedBoundaryData() -> Data {
  502. return BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundary: boundary)
  503. }
  504. private func finalBoundaryData() -> Data {
  505. return BoundaryGenerator.boundaryData(boundaryType: .final, boundary: boundary)
  506. }
  507. // MARK: - Private - Errors
  508. private func setBodyPartError(code: Int, failureReason: String) {
  509. guard bodyPartError == nil else { return }
  510. bodyPartError = Error.error(domain: NSURLErrorDomain, code: code, failureReason: failureReason)
  511. }
  512. }