Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
//
2
//  MultipartFormData.swift
3
//
4
//  Copyright (c) 2014-2018 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
 
25
import Foundation
26
 
27
#if os(iOS) || os(watchOS) || os(tvOS)
28
import MobileCoreServices
29
#elseif os(macOS)
30
import CoreServices
31
#endif
32
 
33
/// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode
34
/// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead
35
/// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the
36
/// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for
37
/// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset.
38
///
39
/// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well
40
/// and the w3 form documentation.
41
///
42
/// - https://www.ietf.org/rfc/rfc2388.txt
43
/// - https://www.ietf.org/rfc/rfc2045.txt
44
/// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13
45
open class MultipartFormData {
46
    // MARK: - Helper Types
47
 
48
    enum EncodingCharacters {
49
        static let crlf = "\r\n"
50
    }
51
 
52
    enum BoundaryGenerator {
53
        enum BoundaryType {
54
            case initial, encapsulated, final
55
        }
56
 
57
        static func randomBoundary() -> String {
58
            let first = UInt32.random(in: UInt32.min...UInt32.max)
59
            let second = UInt32.random(in: UInt32.min...UInt32.max)
60
 
61
            return String(format: "alamofire.boundary.%08x%08x", first, second)
62
        }
63
 
64
        static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
65
            let boundaryText: String
66
 
67
            switch boundaryType {
68
            case .initial:
69
                boundaryText = "--\(boundary)\(EncodingCharacters.crlf)"
70
            case .encapsulated:
71
                boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
72
            case .final:
73
                boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
74
            }
75
 
76
            return Data(boundaryText.utf8)
77
        }
78
    }
79
 
80
    class BodyPart {
81
        let headers: HTTPHeaders
82
        let bodyStream: InputStream
83
        let bodyContentLength: UInt64
84
        var hasInitialBoundary = false
85
        var hasFinalBoundary = false
86
 
87
        init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) {
88
            self.headers = headers
89
            self.bodyStream = bodyStream
90
            self.bodyContentLength = bodyContentLength
91
        }
92
    }
93
 
94
    // MARK: - Properties
95
 
96
    /// Default memory threshold used when encoding `MultipartFormData`, in bytes.
97
    public static let encodingMemoryThreshold: UInt64 = 10_000_000
98
 
99
    /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
100
    open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
101
 
102
    /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries.
103
    public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
104
 
105
    /// The boundary used to separate the body parts in the encoded form data.
106
    public let boundary: String
107
 
108
    let fileManager: FileManager
109
 
110
    private var bodyParts: [BodyPart]
111
    private var bodyPartError: AFError?
112
    private let streamBufferSize: Int
113
 
114
    // MARK: - Lifecycle
115
 
116
    /// Creates an instance.
117
    ///
118
    /// - Parameters:
119
    ///   - fileManager: `FileManager` to use for file operations, if needed.
120
    ///   - boundary: Boundary `String` used to separate body parts.
121
    public init(fileManager: FileManager = .default, boundary: String? = nil) {
122
        self.fileManager = fileManager
123
        self.boundary = boundary ?? BoundaryGenerator.randomBoundary()
124
        bodyParts = []
125
 
126
        //
127
        // The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more
128
        // information, please refer to the following article:
129
        //   - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html
130
        //
131
        streamBufferSize = 1024
132
    }
133
 
134
    // MARK: - Body Parts
135
 
136
    /// Creates a body part from the data and appends it to the instance.
137
    ///
138
    /// The body part data will be encoded using the following format:
139
    ///
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
    ///
145
    /// - Parameters:
146
    ///   - data:     `Data` to encoding into the instance.
147
    ///   - name:     Name to associate with the `Data` in the `Content-Disposition` HTTP header.
148
    ///   - fileName: Filename to associate with the `Data` in the `Content-Disposition` HTTP header.
149
    ///   - mimeType: MIME type to associate with the data in the `Content-Type` HTTP header.
150
    public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil) {
151
        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
152
        let stream = InputStream(data: data)
153
        let length = UInt64(data.count)
154
 
155
        append(stream, withLength: length, headers: headers)
156
    }
157
 
158
    /// Creates a body part from the file and appends it to the instance.
159
    ///
160
    /// The body part data will be encoded using the following format:
161
    ///
162
    /// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header)
163
    /// - `Content-Type: #{generated mimeType}` (HTTP Header)
164
    /// - Encoded file data
165
    /// - Multipart form boundary
166
    ///
167
    /// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the
168
    /// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the
169
    /// system associated MIME type.
170
    ///
171
    /// - Parameters:
172
    ///   - fileURL: `URL` of the file whose content will be encoded into the instance.
173
    ///   - name:    Name to associate with the file content in the `Content-Disposition` HTTP header.
174
    public func append(_ fileURL: URL, withName name: String) {
175
        let fileName = fileURL.lastPathComponent
176
        let pathExtension = fileURL.pathExtension
177
 
178
        if !fileName.isEmpty && !pathExtension.isEmpty {
179
            let mime = mimeType(forPathExtension: pathExtension)
180
            append(fileURL, withName: name, fileName: fileName, mimeType: mime)
181
        } else {
182
            setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL))
183
        }
184
    }
185
 
186
    /// Creates a body part from the file and appends it to the instance.
187
    ///
188
    /// The body part data will be encoded using the following format:
189
    ///
190
    /// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header)
191
    /// - Content-Type: #{mimeType} (HTTP Header)
192
    /// - Encoded file data
193
    /// - Multipart form boundary
194
    ///
195
    /// - Parameters:
196
    ///   - fileURL:  `URL` of the file whose content will be encoded into the instance.
197
    ///   - name:     Name to associate with the file content in the `Content-Disposition` HTTP header.
198
    ///   - fileName: Filename to associate with the file content in the `Content-Disposition` HTTP header.
199
    ///   - mimeType: MIME type to associate with the file content in the `Content-Type` HTTP header.
200
    public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) {
201
        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
202
 
203
        //============================================================
204
        //                 Check 1 - is file URL?
205
        //============================================================
206
 
207
        guard fileURL.isFileURL else {
208
            setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL))
209
            return
210
        }
211
 
212
        //============================================================
213
        //              Check 2 - is file URL reachable?
214
        //============================================================
215
 
216
        #if !(os(Linux) || os(Windows))
217
        do {
218
            let isReachable = try fileURL.checkPromisedItemIsReachable()
219
            guard isReachable else {
220
                setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL))
221
                return
222
            }
223
        } catch {
224
            setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
225
            return
226
        }
227
        #endif
228
 
229
        //============================================================
230
        //            Check 3 - is file URL a directory?
231
        //============================================================
232
 
233
        var isDirectory: ObjCBool = false
234
        let path = fileURL.path
235
 
236
        guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else {
237
            setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL))
238
            return
239
        }
240
 
241
        //============================================================
242
        //          Check 4 - can the file size be extracted?
243
        //============================================================
244
 
245
        let bodyContentLength: UInt64
246
 
247
        do {
248
            guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else {
249
                setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL))
250
                return
251
            }
252
 
253
            bodyContentLength = fileSize.uint64Value
254
        } catch {
255
            setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error))
256
            return
257
        }
258
 
259
        //============================================================
260
        //       Check 5 - can a stream be created from file URL?
261
        //============================================================
262
 
263
        guard let stream = InputStream(url: fileURL) else {
264
            setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL))
265
            return
266
        }
267
 
268
        append(stream, withLength: bodyContentLength, headers: headers)
269
    }
270
 
271
    /// Creates a body part from the stream and appends it to the instance.
272
    ///
273
    /// The body part data will be encoded using the following format:
274
    ///
275
    /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
276
    /// - `Content-Type: #{mimeType}` (HTTP Header)
277
    /// - Encoded stream data
278
    /// - Multipart form boundary
279
    ///
280
    /// - Parameters:
281
    ///   - stream:   `InputStream` to encode into the instance.
282
    ///   - length:   Length, in bytes, of the stream.
283
    ///   - name:     Name to associate with the stream content in the `Content-Disposition` HTTP header.
284
    ///   - fileName: Filename to associate with the stream content in the `Content-Disposition` HTTP header.
285
    ///   - mimeType: MIME type to associate with the stream content in the `Content-Type` HTTP header.
286
    public func append(_ stream: InputStream,
287
                       withLength length: UInt64,
288
                       name: String,
289
                       fileName: String,
290
                       mimeType: String) {
291
        let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
292
        append(stream, withLength: length, headers: headers)
293
    }
294
 
295
    /// Creates a body part with the stream, length, and headers and appends it to the instance.
296
    ///
297
    /// The body part data will be encoded using the following format:
298
    ///
299
    /// - HTTP headers
300
    /// - Encoded stream data
301
    /// - Multipart form boundary
302
    ///
303
    /// - Parameters:
304
    ///   - stream:  `InputStream` to encode into the instance.
305
    ///   - length:  Length, in bytes, of the stream.
306
    ///   - headers: `HTTPHeaders` for the body part.
307
    public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) {
308
        let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
309
        bodyParts.append(bodyPart)
310
    }
311
 
312
    // MARK: - Data Encoding
313
 
314
    /// Encodes all appended body parts into a single `Data` value.
315
    ///
316
    /// - Note: This method will load all the appended body parts into memory all at the same time. This method should
317
    ///         only be used when the encoded data will have a small memory footprint. For large data cases, please use
318
    ///         the `writeEncodedData(to:))` method.
319
    ///
320
    /// - Returns: The encoded `Data`, if encoding is successful.
321
    /// - Throws:  An `AFError` if encoding encounters an error.
322
    public func encode() throws -> Data {
323
        if let bodyPartError = bodyPartError {
324
            throw bodyPartError
325
        }
326
 
327
        var encoded = Data()
328
 
329
        bodyParts.first?.hasInitialBoundary = true
330
        bodyParts.last?.hasFinalBoundary = true
331
 
332
        for bodyPart in bodyParts {
333
            let encodedData = try encode(bodyPart)
334
            encoded.append(encodedData)
335
        }
336
 
337
        return encoded
338
    }
339
 
340
    /// Writes all appended body parts to the given file `URL`.
341
    ///
342
    /// This process is facilitated by reading and writing with input and output streams, respectively. Thus,
343
    /// this approach is very memory efficient and should be used for large body part data.
344
    ///
345
    /// - Parameter fileURL: File `URL` to which to write the form data.
346
    /// - Throws:            An `AFError` if encoding encounters an error.
347
    public func writeEncodedData(to fileURL: URL) throws {
348
        if let bodyPartError = bodyPartError {
349
            throw bodyPartError
350
        }
351
 
352
        if fileManager.fileExists(atPath: fileURL.path) {
353
            throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
354
        } else if !fileURL.isFileURL {
355
            throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
356
        }
357
 
358
        guard let outputStream = OutputStream(url: fileURL, append: false) else {
359
            throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
360
        }
361
 
362
        outputStream.open()
363
        defer { outputStream.close() }
364
 
365
        bodyParts.first?.hasInitialBoundary = true
366
        bodyParts.last?.hasFinalBoundary = true
367
 
368
        for bodyPart in bodyParts {
369
            try write(bodyPart, to: outputStream)
370
        }
371
    }
372
 
373
    // MARK: - Private - Body Part Encoding
374
 
375
    private func encode(_ bodyPart: BodyPart) throws -> Data {
376
        var encoded = Data()
377
 
378
        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
379
        encoded.append(initialData)
380
 
381
        let headerData = encodeHeaders(for: bodyPart)
382
        encoded.append(headerData)
383
 
384
        let bodyStreamData = try encodeBodyStream(for: bodyPart)
385
        encoded.append(bodyStreamData)
386
 
387
        if bodyPart.hasFinalBoundary {
388
            encoded.append(finalBoundaryData())
389
        }
390
 
391
        return encoded
392
    }
393
 
394
    private func encodeHeaders(for bodyPart: BodyPart) -> Data {
395
        let headerText = bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" }
396
            .joined()
397
            + EncodingCharacters.crlf
398
 
399
        return Data(headerText.utf8)
400
    }
401
 
402
    private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data {
403
        let inputStream = bodyPart.bodyStream
404
        inputStream.open()
405
        defer { inputStream.close() }
406
 
407
        var encoded = Data()
408
 
409
        while inputStream.hasBytesAvailable {
410
            var buffer = [UInt8](repeating: 0, count: streamBufferSize)
411
            let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
412
 
413
            if let error = inputStream.streamError {
414
                throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error))
415
            }
416
 
417
            if bytesRead > 0 {
418
                encoded.append(buffer, count: bytesRead)
419
            } else {
420
                break
421
            }
422
        }
423
 
424
        guard UInt64(encoded.count) == bodyPart.bodyContentLength else {
425
            let error = AFError.UnexpectedInputStreamLength(bytesExpected: bodyPart.bodyContentLength,
426
                                                            bytesRead: UInt64(encoded.count))
427
            throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error))
428
        }
429
 
430
        return encoded
431
    }
432
 
433
    // MARK: - Private - Writing Body Part to Output Stream
434
 
435
    private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws {
436
        try writeInitialBoundaryData(for: bodyPart, to: outputStream)
437
        try writeHeaderData(for: bodyPart, to: outputStream)
438
        try writeBodyStream(for: bodyPart, to: outputStream)
439
        try writeFinalBoundaryData(for: bodyPart, to: outputStream)
440
    }
441
 
442
    private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
443
        let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
444
        return try write(initialData, to: outputStream)
445
    }
446
 
447
    private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
448
        let headerData = encodeHeaders(for: bodyPart)
449
        return try write(headerData, to: outputStream)
450
    }
451
 
452
    private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
453
        let inputStream = bodyPart.bodyStream
454
 
455
        inputStream.open()
456
        defer { inputStream.close() }
457
 
458
        while inputStream.hasBytesAvailable {
459
            var buffer = [UInt8](repeating: 0, count: streamBufferSize)
460
            let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
461
 
462
            if let streamError = inputStream.streamError {
463
                throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError))
464
            }
465
 
466
            if bytesRead > 0 {
467
                if buffer.count != bytesRead {
468
                    buffer = Array(buffer[0..<bytesRead])
469
                }
470
 
471
                try write(&buffer, to: outputStream)
472
            } else {
473
                break
474
            }
475
        }
476
    }
477
 
478
    private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
479
        if bodyPart.hasFinalBoundary {
480
            return try write(finalBoundaryData(), to: outputStream)
481
        }
482
    }
483
 
484
    // MARK: - Private - Writing Buffered Data to Output Stream
485
 
486
    private func write(_ data: Data, to outputStream: OutputStream) throws {
487
        var buffer = [UInt8](repeating: 0, count: data.count)
488
        data.copyBytes(to: &buffer, count: data.count)
489
 
490
        return try write(&buffer, to: outputStream)
491
    }
492
 
493
    private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws {
494
        var bytesToWrite = buffer.count
495
 
496
        while bytesToWrite > 0, outputStream.hasSpaceAvailable {
497
            let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)
498
 
499
            if let error = outputStream.streamError {
500
                throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error))
501
            }
502
 
503
            bytesToWrite -= bytesWritten
504
 
505
            if bytesToWrite > 0 {
506
                buffer = Array(buffer[bytesWritten..<buffer.count])
507
            }
508
        }
509
    }
510
 
511
    // MARK: - Private - Content Headers
512
 
513
    private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> HTTPHeaders {
514
        var disposition = "form-data; name=\"\(name)\""
515
        if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" }
516
 
517
        var headers: HTTPHeaders = [.contentDisposition(disposition)]
518
        if let mimeType = mimeType { headers.add(.contentType(mimeType)) }
519
 
520
        return headers
521
    }
522
 
523
    // MARK: - Private - Boundary Encoding
524
 
525
    private func initialBoundaryData() -> Data {
526
        BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary)
527
    }
528
 
529
    private func encapsulatedBoundaryData() -> Data {
530
        BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary)
531
    }
532
 
533
    private func finalBoundaryData() -> Data {
534
        BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary)
535
    }
536
 
537
    // MARK: - Private - Errors
538
 
539
    private func setBodyPartError(withReason reason: AFError.MultipartEncodingFailureReason) {
540
        guard bodyPartError == nil else { return }
541
        bodyPartError = AFError.multipartEncodingFailed(reason: reason)
542
    }
543
}
544
 
545
#if canImport(UniformTypeIdentifiers)
546
import UniformTypeIdentifiers
547
 
548
extension MultipartFormData {
549
    // MARK: - Private - Mime Type
550
 
551
    private func mimeType(forPathExtension pathExtension: String) -> String {
552
        if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) {
553
            return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
554
        } else {
555
            if
556
                let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
557
                let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() {
558
                return contentType as String
559
            }
560
 
561
            return "application/octet-stream"
562
        }
563
    }
564
}
565
 
566
#else
567
 
568
extension MultipartFormData {
569
    // MARK: - Private - Mime Type
570
 
571
    private func mimeType(forPathExtension pathExtension: String) -> String {
572
        #if !(os(Linux) || os(Windows))
573
        if
574
            let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
575
            let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() {
576
            return contentType as String
577
        }
578
        #endif
579
 
580
        return "application/octet-stream"
581
    }
582
}
583
 
584
#endif