|
| 1 | +import Foundation |
| 2 | + |
| 3 | +struct MultipartFormDataEncoder { |
| 4 | + |
| 5 | + let boundary: String |
| 6 | + private var bodyParts: [BodyPart] |
| 7 | + |
| 8 | + // |
| 9 | + // The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more |
| 10 | + // information, please refer to the following article: |
| 11 | + // - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html |
| 12 | + // |
| 13 | + private let streamBufferSize = 1024 |
| 14 | + |
| 15 | + public init(body: MultipartFormData) { |
| 16 | + self.boundary = body.boundary |
| 17 | + self.bodyParts = body.bodyParts |
| 18 | + } |
| 19 | + |
| 20 | + mutating func encode() throws -> Data { |
| 21 | + var encoded = Data() |
| 22 | + |
| 23 | + if var first = bodyParts.first { |
| 24 | + first.hasInitialBoundary = true |
| 25 | + bodyParts[0] = first |
| 26 | + } |
| 27 | + |
| 28 | + if var last = bodyParts.last { |
| 29 | + last.hasFinalBoundary = true |
| 30 | + bodyParts[bodyParts.count - 1] = last |
| 31 | + } |
| 32 | + |
| 33 | + for bodyPart in bodyParts { |
| 34 | + encoded.append(try encodeBodyPart(bodyPart)) |
| 35 | + } |
| 36 | + |
| 37 | + return encoded |
| 38 | + } |
| 39 | + |
| 40 | + private func encodeBodyPart(_ bodyPart: BodyPart) throws -> Data { |
| 41 | + var encoded = Data() |
| 42 | + |
| 43 | + if bodyPart.hasInitialBoundary { |
| 44 | + encoded.append(Boundary.data(for: .initial, boundary: boundary)) |
| 45 | + } else { |
| 46 | + encoded.append(Boundary.data(for: .encapsulated, boundary: boundary)) |
| 47 | + } |
| 48 | + |
| 49 | + encoded.append(try encodeBodyPart(headers: bodyPart.headers)) |
| 50 | + encoded.append(try encodeBodyPart(stream: bodyPart.stream, length: bodyPart.length)) |
| 51 | + |
| 52 | + if bodyPart.hasFinalBoundary { |
| 53 | + encoded.append(Boundary.data(for: .final, boundary: boundary)) |
| 54 | + } |
| 55 | + |
| 56 | + return encoded |
| 57 | + } |
| 58 | + |
| 59 | + private func encodeBodyPart(headers: [Header]) throws -> Data { |
| 60 | + let headerText = headers.map { "\($0.name.key): \($0.value)\(EncodingCharacters.crlf)" } |
| 61 | + .joined() |
| 62 | + + EncodingCharacters.crlf |
| 63 | + |
| 64 | + return Data(headerText.utf8) |
| 65 | + } |
| 66 | + |
| 67 | + private func encodeBodyPart(stream: InputStream, length: Int) throws -> Data { |
| 68 | + var encoded = Data() |
| 69 | + |
| 70 | + stream.open() |
| 71 | + defer { stream.close() } |
| 72 | + |
| 73 | + while stream.hasBytesAvailable { |
| 74 | + var buffer = [UInt8](repeating: 0, count: streamBufferSize) |
| 75 | + let bytesRead = stream.read(&buffer, maxLength: streamBufferSize) |
| 76 | + |
| 77 | + if let error = stream.streamError { |
| 78 | + throw BodyPart.Error.inputStreamReadFailed(error.localizedDescription) |
| 79 | + } |
| 80 | + |
| 81 | + if bytesRead > 0 { |
| 82 | + encoded.append(buffer, count: bytesRead) |
| 83 | + } else { |
| 84 | + break |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + guard encoded.count == length else { |
| 89 | + throw BodyPart.Error.unexpectedInputStreamLength(expected: length, bytesRead: encoded.count) |
| 90 | + } |
| 91 | + |
| 92 | + return encoded |
| 93 | + } |
| 94 | + |
| 95 | +} |
| 96 | + |
| 97 | +extension BodyPart { |
| 98 | + |
| 99 | + enum Error: Swift.Error { |
| 100 | + case inputStreamReadFailed(String) |
| 101 | + case unexpectedInputStreamLength(expected: Int, bytesRead: Int) |
| 102 | + } |
| 103 | + |
| 104 | +} |
0 commit comments