stream-chat-react-native 9.0.2-beta.2 → 9.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +5 -4
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +20 -4
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadFileRequestBody.kt +25 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadModels.kt +39 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadProgress.kt +80 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadRequestParser.kt +110 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadSourceResolver.kt +99 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploader.kt +138 -0
- package/android/src/newarch/com/streamchatreactnative/StreamMultipartUploaderModule.kt +122 -0
- package/ios/shared/StreamMultipartUploadBodyStream.swift +254 -0
- package/ios/shared/StreamMultipartUploadManager.swift +462 -0
- package/ios/shared/StreamMultipartUploadModels.swift +69 -0
- package/ios/shared/StreamMultipartUploadProgress.swift +48 -0
- package/ios/shared/StreamMultipartUploadSourceResolver.swift +391 -0
- package/ios/shared/StreamMultipartUploader.h +16 -0
- package/ios/shared/StreamMultipartUploader.mm +109 -0
- package/ios/shared/StreamMultipartUploaderBridge.swift +145 -0
- package/ios/shared/StreamShimmerView.swift +180 -77
- package/ios/shared/StreamVideoThumbnailGenerator.swift +13 -2
- package/package.json +3 -2
- package/src/handlers/index.ts +1 -0
- package/src/handlers/multipartUpload.ts +9 -0
- package/src/index.js +2 -1
- package/src/native/NativeStreamMultipartUploader.ts +52 -0
- package/src/native/multipartUploader.ts +5 -0
- package/src/optionalDependencies/__tests__/pickDocument.test.ts +86 -0
- package/src/optionalDependencies/pickDocument.ts +24 -6
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
private enum StreamMultipartBodyElement {
|
|
4
|
+
case data(Data)
|
|
5
|
+
case file(URL)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
final class StreamMultipartUploadBodyStreamFactory {
|
|
9
|
+
let boundary: String
|
|
10
|
+
let contentLength: Int64?
|
|
11
|
+
|
|
12
|
+
private let elements: [StreamMultipartBodyElement]
|
|
13
|
+
|
|
14
|
+
private init(
|
|
15
|
+
boundary: String,
|
|
16
|
+
contentLength: Int64?,
|
|
17
|
+
elements: [StreamMultipartBodyElement]
|
|
18
|
+
) {
|
|
19
|
+
self.boundary = boundary
|
|
20
|
+
self.contentLength = contentLength
|
|
21
|
+
self.elements = elements
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static func create(parts: [StreamMultipartUploadPart]) async throws -> StreamMultipartUploadBodyStreamFactory {
|
|
25
|
+
let boundary = "stream-upload-\(UUID().uuidString)"
|
|
26
|
+
var elements = [StreamMultipartBodyElement]()
|
|
27
|
+
var totalLength: Int64 = 0
|
|
28
|
+
var canComputeLength = true
|
|
29
|
+
|
|
30
|
+
for part in parts {
|
|
31
|
+
switch part {
|
|
32
|
+
case .text(let textPart):
|
|
33
|
+
let data = multipartTextData(boundary: boundary, part: textPart)
|
|
34
|
+
elements.append(.data(data))
|
|
35
|
+
totalLength += Int64(data.count)
|
|
36
|
+
case .file(let filePart):
|
|
37
|
+
let resolvedPart = try await StreamMultipartUploadSourceResolver.resolve(filePart)
|
|
38
|
+
let headerData = multipartFileHeaderData(boundary: boundary, part: resolvedPart)
|
|
39
|
+
let footerData = "\r\n".data(using: .utf8) ?? Data()
|
|
40
|
+
|
|
41
|
+
elements.append(.data(headerData))
|
|
42
|
+
elements.append(.file(resolvedPart.fileURL))
|
|
43
|
+
elements.append(.data(footerData))
|
|
44
|
+
|
|
45
|
+
totalLength += Int64(headerData.count) + Int64(footerData.count)
|
|
46
|
+
if let size = resolvedPart.size {
|
|
47
|
+
totalLength += size
|
|
48
|
+
} else {
|
|
49
|
+
canComputeLength = false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let closingBoundary = "--\(boundary)--\r\n".data(using: .utf8) ?? Data()
|
|
55
|
+
elements.append(.data(closingBoundary))
|
|
56
|
+
totalLength += Int64(closingBoundary.count)
|
|
57
|
+
|
|
58
|
+
return StreamMultipartUploadBodyStreamFactory(
|
|
59
|
+
boundary: boundary,
|
|
60
|
+
contentLength: canComputeLength ? totalLength : nil,
|
|
61
|
+
elements: elements
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func makeStream() -> InputStream {
|
|
66
|
+
StreamMultipartSequentialInputStream(elements: elements)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static func multipartTextData(boundary: String, part: StreamMultipartTextPart) -> Data {
|
|
70
|
+
let payload = [
|
|
71
|
+
"--\(boundary)",
|
|
72
|
+
"Content-Disposition: form-data; name=\(multipartQuotedParameter(part.fieldName))",
|
|
73
|
+
"",
|
|
74
|
+
part.value,
|
|
75
|
+
"",
|
|
76
|
+
].joined(separator: "\r\n")
|
|
77
|
+
|
|
78
|
+
return payload.data(using: .utf8) ?? Data()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private static func multipartFileHeaderData(
|
|
82
|
+
boundary: String,
|
|
83
|
+
part: StreamMultipartResolvedFilePart
|
|
84
|
+
) -> Data {
|
|
85
|
+
let payload = [
|
|
86
|
+
"--\(boundary)",
|
|
87
|
+
"Content-Disposition: form-data; name=\(multipartQuotedParameter(part.fieldName)); filename=\(multipartQuotedParameter(part.fileName))",
|
|
88
|
+
"Content-Type: \(part.mimeType)",
|
|
89
|
+
"",
|
|
90
|
+
].joined(separator: "\r\n") + "\r\n"
|
|
91
|
+
|
|
92
|
+
return payload.data(using: .utf8) ?? Data()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static func multipartQuotedParameter(_ value: String) -> String {
|
|
96
|
+
let escaped = value
|
|
97
|
+
.replacingOccurrences(of: "\r", with: "%0D")
|
|
98
|
+
.replacingOccurrences(of: "\n", with: "%0A")
|
|
99
|
+
.replacingOccurrences(of: "\"", with: "%22")
|
|
100
|
+
|
|
101
|
+
return "\"\(escaped)\""
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private final class StreamMultipartSequentialInputStream: InputStream {
|
|
106
|
+
private let elements: [StreamMultipartBodyElement]
|
|
107
|
+
private var currentIndex = 0
|
|
108
|
+
private var currentStream: InputStream?
|
|
109
|
+
private weak var internalDelegate: StreamDelegate?
|
|
110
|
+
private var internalStatus: Stream.Status = .notOpen
|
|
111
|
+
private var internalError: Error?
|
|
112
|
+
private var scheduledRunLoops: [(runLoop: RunLoop, mode: RunLoop.Mode)] = []
|
|
113
|
+
|
|
114
|
+
init(elements: [StreamMultipartBodyElement]) {
|
|
115
|
+
self.elements = elements
|
|
116
|
+
super.init(data: Data())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
override var delegate: StreamDelegate? {
|
|
120
|
+
get {
|
|
121
|
+
internalDelegate
|
|
122
|
+
}
|
|
123
|
+
set {
|
|
124
|
+
internalDelegate = newValue
|
|
125
|
+
currentStream?.delegate = newValue
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override var hasBytesAvailable: Bool {
|
|
130
|
+
guard internalStatus != .closed, internalStatus != .error else {
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if let currentStream, currentStream.hasBytesAvailable {
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return currentIndex < elements.count
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override var streamError: Error? {
|
|
142
|
+
internalError
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override var streamStatus: Stream.Status {
|
|
146
|
+
internalStatus
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override func open() {
|
|
150
|
+
guard internalStatus == .notOpen else {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
internalStatus = .opening
|
|
155
|
+
advanceStreamIfNeeded()
|
|
156
|
+
if internalStatus == .error {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
internalStatus = currentStream == nil ? .atEnd : .open
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
override func close() {
|
|
163
|
+
currentStream?.close()
|
|
164
|
+
currentStream = nil
|
|
165
|
+
internalStatus = .closed
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
|
|
169
|
+
scheduledRunLoops.append((runLoop: aRunLoop, mode: mode))
|
|
170
|
+
currentStream?.schedule(in: aRunLoop, forMode: mode)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
override func remove(from aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
|
|
174
|
+
scheduledRunLoops.removeAll { $0.runLoop == aRunLoop && $0.mode == mode }
|
|
175
|
+
currentStream?.remove(from: aRunLoop, forMode: mode)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength len: Int) -> Int {
|
|
179
|
+
guard internalStatus != .closed else {
|
|
180
|
+
return 0
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if internalStatus == .notOpen {
|
|
184
|
+
open()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
while true {
|
|
188
|
+
guard let currentStream else {
|
|
189
|
+
if internalStatus == .error {
|
|
190
|
+
return -1
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
internalStatus = .atEnd
|
|
194
|
+
return 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let bytesRead = currentStream.read(buffer, maxLength: len)
|
|
198
|
+
|
|
199
|
+
if bytesRead > 0 {
|
|
200
|
+
internalStatus = .open
|
|
201
|
+
return bytesRead
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if bytesRead < 0 {
|
|
205
|
+
internalError = currentStream.streamError
|
|
206
|
+
internalStatus = .error
|
|
207
|
+
return -1
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
currentStream.close()
|
|
211
|
+
self.currentStream = nil
|
|
212
|
+
advanceStreamIfNeeded()
|
|
213
|
+
|
|
214
|
+
if self.currentStream == nil {
|
|
215
|
+
internalStatus = .atEnd
|
|
216
|
+
return 0
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private func advanceStreamIfNeeded() {
|
|
222
|
+
guard currentStream == nil else {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
while currentIndex < elements.count {
|
|
227
|
+
let nextElement = elements[currentIndex]
|
|
228
|
+
currentIndex += 1
|
|
229
|
+
|
|
230
|
+
let nextStream: InputStream?
|
|
231
|
+
switch nextElement {
|
|
232
|
+
case .data(let data):
|
|
233
|
+
nextStream = InputStream(data: data)
|
|
234
|
+
case .file(let url):
|
|
235
|
+
nextStream = InputStream(url: url)
|
|
236
|
+
if nextStream == nil {
|
|
237
|
+
internalError = StreamMultipartUploadError.unreadableFile(url.path)
|
|
238
|
+
internalStatus = .error
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if let nextStream {
|
|
244
|
+
nextStream.delegate = internalDelegate
|
|
245
|
+
for scheduled in scheduledRunLoops {
|
|
246
|
+
nextStream.schedule(in: scheduled.runLoop, forMode: scheduled.mode)
|
|
247
|
+
}
|
|
248
|
+
nextStream.open()
|
|
249
|
+
currentStream = nextStream
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
private actor StreamMultipartUploadConcurrencyLimiter {
|
|
4
|
+
private var activeUploads = 0
|
|
5
|
+
private let maxConcurrentUploads: Int
|
|
6
|
+
private var waiterOrder = [UUID]()
|
|
7
|
+
private var waiters = [UUID: CheckedContinuation<Void, Error>]()
|
|
8
|
+
|
|
9
|
+
init(maxConcurrentUploads: Int) {
|
|
10
|
+
self.maxConcurrentUploads = max(1, maxConcurrentUploads)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func acquire() async throws {
|
|
14
|
+
if activeUploads < maxConcurrentUploads {
|
|
15
|
+
activeUploads += 1
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let waiterId = UUID()
|
|
20
|
+
|
|
21
|
+
try await withTaskCancellationHandler {
|
|
22
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
23
|
+
if activeUploads < maxConcurrentUploads {
|
|
24
|
+
activeUploads += 1
|
|
25
|
+
continuation.resume()
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
waiterOrder.append(waiterId)
|
|
30
|
+
waiters[waiterId] = continuation
|
|
31
|
+
}
|
|
32
|
+
} onCancel: {
|
|
33
|
+
Task {
|
|
34
|
+
await self.cancelWaiter(id: waiterId)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func release() {
|
|
40
|
+
while !waiterOrder.isEmpty {
|
|
41
|
+
let waiterId = waiterOrder.removeFirst()
|
|
42
|
+
|
|
43
|
+
guard let continuation = waiters.removeValue(forKey: waiterId) else {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
continuation.resume()
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
activeUploads = max(0, activeUploads - 1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private func cancelWaiter(id: UUID) {
|
|
55
|
+
waiterOrder.removeAll { $0 == id }
|
|
56
|
+
waiters.removeValue(forKey: id)?.resume(throwing: StreamMultipartUploadError.cancelled)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private final class StreamMultipartUploadTaskState {
|
|
61
|
+
let bodyFactory: StreamMultipartUploadBodyStreamFactory
|
|
62
|
+
let progressThrottler: StreamMultipartUploadProgressThrottler
|
|
63
|
+
let task: URLSessionUploadTask
|
|
64
|
+
let uploadId: String
|
|
65
|
+
var completion:
|
|
66
|
+
((Result<StreamMultipartUploadResponse, Error>) -> Void)?
|
|
67
|
+
var response: HTTPURLResponse?
|
|
68
|
+
var responseData = Data()
|
|
69
|
+
var responseDataError: Error?
|
|
70
|
+
|
|
71
|
+
init(
|
|
72
|
+
bodyFactory: StreamMultipartUploadBodyStreamFactory,
|
|
73
|
+
progressThrottler: StreamMultipartUploadProgressThrottler,
|
|
74
|
+
task: URLSessionUploadTask,
|
|
75
|
+
uploadId: String,
|
|
76
|
+
completion: @escaping (Result<StreamMultipartUploadResponse, Error>) -> Void
|
|
77
|
+
) {
|
|
78
|
+
self.bodyFactory = bodyFactory
|
|
79
|
+
self.progressThrottler = progressThrottler
|
|
80
|
+
self.task = task
|
|
81
|
+
self.uploadId = uploadId
|
|
82
|
+
self.completion = completion
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
final class StreamMultipartUploadManager: NSObject {
|
|
87
|
+
static let shared = StreamMultipartUploadManager()
|
|
88
|
+
private let maxResponseBodyBytes = 1_048_576
|
|
89
|
+
private let maxConcurrentUploads = min(max(ProcessInfo.processInfo.activeProcessorCount, 2), 4)
|
|
90
|
+
|
|
91
|
+
private lazy var session: URLSession = {
|
|
92
|
+
let delegateQueue = OperationQueue()
|
|
93
|
+
delegateQueue.maxConcurrentOperationCount = 1
|
|
94
|
+
delegateQueue.qualityOfService = .userInitiated
|
|
95
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
96
|
+
configuration.httpMaximumConnectionsPerHost = maxConcurrentUploads
|
|
97
|
+
configuration.waitsForConnectivity = false
|
|
98
|
+
return URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
|
|
99
|
+
}()
|
|
100
|
+
private lazy var uploadLimiter = StreamMultipartUploadConcurrencyLimiter(
|
|
101
|
+
maxConcurrentUploads: maxConcurrentUploads
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
private let lock = NSLock()
|
|
105
|
+
private var cancelledUploadIds = Set<String>()
|
|
106
|
+
private var statesByTaskIdentifier = [Int: StreamMultipartUploadTaskState]()
|
|
107
|
+
private var taskIdentifiersByUploadId = [String: Int]()
|
|
108
|
+
|
|
109
|
+
func cancel(uploadId: String) {
|
|
110
|
+
cancel(uploadId: uploadId, recordCancellation: true)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func cancelInFlight(uploadId: String) {
|
|
114
|
+
cancel(uploadId: uploadId, recordCancellation: false)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func cancel(uploadId: String, recordCancellation: Bool) {
|
|
118
|
+
lock.lock()
|
|
119
|
+
if recordCancellation {
|
|
120
|
+
cancelledUploadIds.insert(uploadId)
|
|
121
|
+
}
|
|
122
|
+
let taskIdentifier = taskIdentifiersByUploadId[uploadId]
|
|
123
|
+
let task: URLSessionUploadTask?
|
|
124
|
+
if let taskIdentifier {
|
|
125
|
+
task = statesByTaskIdentifier[taskIdentifier]?.task
|
|
126
|
+
} else {
|
|
127
|
+
task = nil
|
|
128
|
+
}
|
|
129
|
+
lock.unlock()
|
|
130
|
+
|
|
131
|
+
task?.cancel()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func uploadMultipart(
|
|
135
|
+
uploadId: String,
|
|
136
|
+
url: String,
|
|
137
|
+
method: String,
|
|
138
|
+
headers: [String: String],
|
|
139
|
+
parts: [[String: Any]],
|
|
140
|
+
progress: [String: Any]?,
|
|
141
|
+
timeoutMs: TimeInterval?,
|
|
142
|
+
onProgress: @escaping (Int64, Int64?) -> Void
|
|
143
|
+
) async throws -> StreamMultipartUploadResponse {
|
|
144
|
+
let request = try parseRequest(
|
|
145
|
+
uploadId: uploadId,
|
|
146
|
+
url: url,
|
|
147
|
+
method: method,
|
|
148
|
+
headers: headers,
|
|
149
|
+
parts: parts,
|
|
150
|
+
progress: progress,
|
|
151
|
+
timeoutMs: timeoutMs
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try throwIfCancelled(uploadId: uploadId)
|
|
155
|
+
let bodyFactory = try await StreamMultipartUploadBodyStreamFactory.create(parts: request.parts)
|
|
156
|
+
try throwIfCancelled(uploadId: uploadId)
|
|
157
|
+
var urlRequest = URLRequest(url: request.url)
|
|
158
|
+
urlRequest.httpMethod = request.method
|
|
159
|
+
if let timeoutMs = request.timeoutMs, timeoutMs > 0 {
|
|
160
|
+
urlRequest.timeoutInterval = timeoutMs / 1_000
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
request.headers.forEach { key, value in
|
|
164
|
+
if
|
|
165
|
+
key.caseInsensitiveCompare("Content-Type") == .orderedSame ||
|
|
166
|
+
key.caseInsensitiveCompare("Content-Length") == .orderedSame
|
|
167
|
+
{
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
urlRequest.setValue(value, forHTTPHeaderField: key)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
urlRequest.setValue(
|
|
174
|
+
"multipart/form-data; boundary=\(bodyFactory.boundary)",
|
|
175
|
+
forHTTPHeaderField: "Content-Type"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if let contentLength = bodyFactory.contentLength {
|
|
179
|
+
urlRequest.setValue(String(contentLength), forHTTPHeaderField: "Content-Length")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let progressThrottler =
|
|
183
|
+
StreamMultipartUploadProgressThrottler(options: request.progress, onProgress: onProgress)
|
|
184
|
+
try await uploadLimiter.acquire()
|
|
185
|
+
|
|
186
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
187
|
+
let task = session.uploadTask(withStreamedRequest: urlRequest)
|
|
188
|
+
let state = StreamMultipartUploadTaskState(
|
|
189
|
+
bodyFactory: bodyFactory,
|
|
190
|
+
progressThrottler: progressThrottler,
|
|
191
|
+
task: task,
|
|
192
|
+
uploadId: uploadId
|
|
193
|
+
) { result in
|
|
194
|
+
Task {
|
|
195
|
+
await self.uploadLimiter.release()
|
|
196
|
+
}
|
|
197
|
+
continuation.resume(with: result)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
guard register(state) else {
|
|
201
|
+
task.cancel()
|
|
202
|
+
Task {
|
|
203
|
+
await self.uploadLimiter.release()
|
|
204
|
+
}
|
|
205
|
+
continuation.resume(throwing: StreamMultipartUploadError.cancelled)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
task.resume()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func parseRequest(
|
|
214
|
+
uploadId: String,
|
|
215
|
+
url: String,
|
|
216
|
+
method: String,
|
|
217
|
+
headers: [String: String],
|
|
218
|
+
parts: [[String: Any]],
|
|
219
|
+
progress: [String: Any]?,
|
|
220
|
+
timeoutMs: TimeInterval?
|
|
221
|
+
) throws -> StreamMultipartUploadRequest {
|
|
222
|
+
guard let parsedURL = URL(string: url) else {
|
|
223
|
+
throw StreamMultipartUploadError.invalidURL(url)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let uploadParts = try parts.enumerated().map { index, rawPart -> StreamMultipartUploadPart in
|
|
227
|
+
guard let fieldName = rawPart["fieldName"] as? String else {
|
|
228
|
+
throw StreamMultipartUploadError.invalidRequest(
|
|
229
|
+
"Multipart part \(index) is missing fieldName"
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
guard let kind = rawPart["kind"] as? String else {
|
|
234
|
+
throw StreamMultipartUploadError.invalidRequest("Multipart part \(index) is missing kind")
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
switch kind {
|
|
238
|
+
case "text":
|
|
239
|
+
guard let value = rawPart["value"] as? String else {
|
|
240
|
+
throw StreamMultipartUploadError.invalidRequest(
|
|
241
|
+
"Multipart text part \(index) is missing value"
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
return .text(
|
|
245
|
+
StreamMultipartTextPart(fieldName: fieldName, value: value)
|
|
246
|
+
)
|
|
247
|
+
case "file":
|
|
248
|
+
guard let uri = rawPart["uri"] as? String else {
|
|
249
|
+
throw StreamMultipartUploadError.invalidRequest(
|
|
250
|
+
"Multipart file part \(index) is missing uri"
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
guard let fileName = rawPart["fileName"] as? String else {
|
|
254
|
+
throw StreamMultipartUploadError.invalidRequest(
|
|
255
|
+
"Multipart file part \(index) is missing fileName"
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
return .file(
|
|
259
|
+
StreamMultipartFilePart(
|
|
260
|
+
fieldName: fieldName,
|
|
261
|
+
fileName: fileName,
|
|
262
|
+
mimeType: rawPart["mimeType"] as? String,
|
|
263
|
+
uri: uri
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
default:
|
|
267
|
+
throw StreamMultipartUploadError.invalidRequest("Unsupported multipart kind: \(kind)")
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if !uploadParts.contains(where: {
|
|
272
|
+
if case .file = $0 {
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
275
|
+
return false
|
|
276
|
+
}) {
|
|
277
|
+
throw StreamMultipartUploadError.invalidRequest(
|
|
278
|
+
"Multipart upload must contain at least one file part"
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let progressOptions = StreamMultipartUploadProgressOptions(
|
|
283
|
+
count: progress?["count"] as? Int ?? (progress?["count"] as? NSNumber)?.intValue,
|
|
284
|
+
intervalMs: progress?["intervalMs"] as? Double ?? (progress?["intervalMs"] as? NSNumber)?.doubleValue
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
let parsedTimeoutMs = timeoutMs.flatMap { $0 > 0 ? $0 : nil }
|
|
288
|
+
|
|
289
|
+
return StreamMultipartUploadRequest(
|
|
290
|
+
headers: headers,
|
|
291
|
+
method: method,
|
|
292
|
+
parts: uploadParts,
|
|
293
|
+
progress: progress == nil ? nil : progressOptions,
|
|
294
|
+
timeoutMs: parsedTimeoutMs,
|
|
295
|
+
uploadId: uploadId,
|
|
296
|
+
url: parsedURL
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private func throwIfCancelled(uploadId: String) throws {
|
|
301
|
+
lock.lock()
|
|
302
|
+
let wasCancelled = cancelledUploadIds.remove(uploadId) != nil
|
|
303
|
+
lock.unlock()
|
|
304
|
+
|
|
305
|
+
if wasCancelled {
|
|
306
|
+
throw StreamMultipartUploadError.cancelled
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private func register(_ state: StreamMultipartUploadTaskState) -> Bool {
|
|
311
|
+
lock.lock()
|
|
312
|
+
if cancelledUploadIds.remove(state.uploadId) != nil {
|
|
313
|
+
lock.unlock()
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
statesByTaskIdentifier[state.task.taskIdentifier] = state
|
|
318
|
+
taskIdentifiersByUploadId[state.uploadId] = state.task.taskIdentifier
|
|
319
|
+
lock.unlock()
|
|
320
|
+
return true
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private func removeState(taskIdentifier: Int) -> StreamMultipartUploadTaskState? {
|
|
324
|
+
lock.lock()
|
|
325
|
+
let state = statesByTaskIdentifier.removeValue(forKey: taskIdentifier)
|
|
326
|
+
if let uploadId = state?.uploadId {
|
|
327
|
+
if taskIdentifiersByUploadId[uploadId] == taskIdentifier {
|
|
328
|
+
taskIdentifiersByUploadId.removeValue(forKey: uploadId)
|
|
329
|
+
}
|
|
330
|
+
cancelledUploadIds.remove(uploadId)
|
|
331
|
+
}
|
|
332
|
+
lock.unlock()
|
|
333
|
+
return state
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private func state(taskIdentifier: Int) -> StreamMultipartUploadTaskState? {
|
|
337
|
+
lock.lock()
|
|
338
|
+
let state = statesByTaskIdentifier[taskIdentifier]
|
|
339
|
+
lock.unlock()
|
|
340
|
+
return state
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
extension StreamMultipartUploadManager: URLSessionDataDelegate, URLSessionTaskDelegate {
|
|
345
|
+
func urlSession(
|
|
346
|
+
_ session: URLSession,
|
|
347
|
+
dataTask: URLSessionDataTask,
|
|
348
|
+
didReceive data: Data
|
|
349
|
+
) {
|
|
350
|
+
guard let state = state(taskIdentifier: dataTask.taskIdentifier) else {
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if state.responseData.count + data.count > maxResponseBodyBytes {
|
|
355
|
+
state.responseDataError = StreamMultipartUploadError.responseBodyTooLarge(maxResponseBodyBytes)
|
|
356
|
+
dataTask.cancel()
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
state.responseData.append(data)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
func urlSession(
|
|
364
|
+
_ session: URLSession,
|
|
365
|
+
dataTask: URLSessionDataTask,
|
|
366
|
+
didReceive response: URLResponse,
|
|
367
|
+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
|
368
|
+
) {
|
|
369
|
+
state(taskIdentifier: dataTask.taskIdentifier)?.response = response as? HTTPURLResponse
|
|
370
|
+
completionHandler(.allow)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func urlSession(
|
|
374
|
+
_ session: URLSession,
|
|
375
|
+
task: URLSessionTask,
|
|
376
|
+
didCompleteWithError error: Error?
|
|
377
|
+
) {
|
|
378
|
+
guard let state = removeState(taskIdentifier: task.taskIdentifier) else {
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if let error {
|
|
383
|
+
if let responseDataError = state.responseDataError {
|
|
384
|
+
state.completion?(.failure(responseDataError))
|
|
385
|
+
state.completion = nil
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let nsError = error as NSError
|
|
390
|
+
|
|
391
|
+
if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorCancelled {
|
|
392
|
+
state.completion?(.failure(StreamMultipartUploadError.cancelled))
|
|
393
|
+
} else {
|
|
394
|
+
state.completion?(.failure(nsError))
|
|
395
|
+
}
|
|
396
|
+
state.completion = nil
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
guard let response = state.response else {
|
|
401
|
+
state.completion?(.failure(StreamMultipartUploadError.missingHTTPResponse))
|
|
402
|
+
state.completion = nil
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let headers =
|
|
407
|
+
response.allHeaderFields.reduce(into: [String: String]()) { partialResult, entry in
|
|
408
|
+
guard let key = entry.key as? String else {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let value = String(describing: entry.value)
|
|
413
|
+
if let existingValue = partialResult[key] {
|
|
414
|
+
partialResult[key] = "\(existingValue), \(value)"
|
|
415
|
+
} else {
|
|
416
|
+
partialResult[key] = value
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let body = String(decoding: state.responseData, as: UTF8.self)
|
|
421
|
+
|
|
422
|
+
state.completion?(
|
|
423
|
+
.success(
|
|
424
|
+
StreamMultipartUploadResponse(
|
|
425
|
+
body: body,
|
|
426
|
+
headers: headers,
|
|
427
|
+
status: response.statusCode,
|
|
428
|
+
statusText: HTTPURLResponse.localizedString(forStatusCode: response.statusCode)
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
state.completion = nil
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
func urlSession(
|
|
436
|
+
_ session: URLSession,
|
|
437
|
+
task: URLSessionTask,
|
|
438
|
+
didSendBodyData bytesSent: Int64,
|
|
439
|
+
totalBytesSent: Int64,
|
|
440
|
+
totalBytesExpectedToSend: Int64
|
|
441
|
+
) {
|
|
442
|
+
let total: Int64?
|
|
443
|
+
if totalBytesExpectedToSend > 0 {
|
|
444
|
+
total = totalBytesExpectedToSend
|
|
445
|
+
} else {
|
|
446
|
+
total = nil
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
state(taskIdentifier: task.taskIdentifier)?.progressThrottler.dispatch(
|
|
450
|
+
loaded: totalBytesSent,
|
|
451
|
+
total: total
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
func urlSession(
|
|
456
|
+
_ session: URLSession,
|
|
457
|
+
task: URLSessionTask,
|
|
458
|
+
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void
|
|
459
|
+
) {
|
|
460
|
+
completionHandler(state(taskIdentifier: task.taskIdentifier)?.bodyFactory.makeStream())
|
|
461
|
+
}
|
|
462
|
+
}
|