@@ -83,6 +83,17 @@ struct DFAPI {
83
83
}
84
84
return responseBody
85
85
}
86
+ private func makeAPIRequestStreamed( path: String , parameters: [ String : String ] , method: HTTPRequest . Method = . get, expectedResponse: HTTPResponse . Status = . ok, headerFields: [ HTTPField . Name : String ] = [ : ] , taskDelegate: URLSessionStreamDelegate ) throws -> URLSessionUploadTask {
87
+ var request = HTTPRequest ( method: method, url: encodeParametersIntoURL ( path: path, parameters: parameters) )
88
+ request. headerFields [ . referer] = url. absoluteString
89
+ request. headerFields [ . authorization] = self . token
90
+ for kvp in headerFields {
91
+ request. headerFields [ kvp. key] = kvp. value
92
+ }
93
+
94
+ let session = URLSession ( configuration: . ephemeral, delegate: taskDelegate, delegateQueue: . main)
95
+ return session. uploadTask ( withStreamedRequest: URLRequest ( httpRequest: request) !)
96
+ }
86
97
87
98
public func getStats( amount: Int ? = nil ) async -> DFStatsResponse ? {
88
99
do {
@@ -120,6 +131,20 @@ struct DFAPI {
120
131
}
121
132
}
122
133
134
+ public func uploadFileStreamed( url: URL , fileName: String ? = nil , taskDelegate: URLSessionTaskDelegate ) async -> DjangoFilesUploadDelegate ? {
135
+ let boundary = UUID ( ) . uuidString
136
+
137
+ do {
138
+ let delegate = DjangoFilesUploadDelegate ( fileURL: url, boundary: boundary, originalDelegate: taskDelegate)
139
+ let task = try makeAPIRequestStreamed ( path: getAPIPath ( . upload) , parameters: [ : ] , method: . post, expectedResponse: . ok, headerFields: [ . contentType: " multipart/form-data; boundary= \( boundary) " ] , taskDelegate: delegate)
140
+ task. resume ( )
141
+ return delegate
142
+ } catch {
143
+ print ( " Request failed \( error) " )
144
+ return nil ;
145
+ }
146
+ }
147
+
123
148
public func createShort( url: URL , short: String , maxViews: Int ? = nil ) async -> DFShortResponse ? {
124
149
let request = DFShortRequest ( url: url. absoluteString, vanity: short, maxViews: maxViews ?? 0 )
125
150
do {
@@ -132,3 +157,317 @@ struct DFAPI {
132
157
}
133
158
}
134
159
}
160
+
161
+ class DjangoFilesUploadDelegate : NSObject , StreamDelegate , URLSessionDelegate , URLSessionDataDelegate , URLSessionTaskDelegate , URLSessionStreamDelegate {
162
+ enum States {
163
+ case invalid //Invalid/uninitialized state
164
+ case started //Upload started
165
+ case intro //Uploading intro to multipart data
166
+ case file //Uploading file
167
+ case outro //Uploading outro to multipart data
168
+ case complete //Upload complete
169
+ case error //Error state
170
+ }
171
+
172
+ //Upload buffer size
173
+ final let BUFFER_SIZE : Int = 1000000
174
+
175
+ //File read chunk size
176
+ final let CHUNK_SIZE : Int = 4096
177
+
178
+ public let originalDelegate : URLSessionTaskDelegate
179
+ public let boundary : String
180
+
181
+ public let fileName : String
182
+ public let fileURL : URL
183
+ var canWrite : Bool = false
184
+ var error : String ? = nil
185
+ var timer : Timer ?
186
+ var state : States = . invalid
187
+
188
+ var intro : Data
189
+ var fileStream : FileHandle
190
+ var dataProgress : Int64
191
+ var size : Int64
192
+ var outro : Data
193
+
194
+ var multipartProgress : Int = 0
195
+
196
+ var task : URLSessionTask ? = nil
197
+ var session : URLSession ? = nil
198
+
199
+ var response : DFUploadResponse ?
200
+
201
+ struct UploadStreams {
202
+ let input : InputStream
203
+ let output : OutputStream
204
+ var bytesAvailable : Int64 = 0
205
+ }
206
+ lazy var boundStreams : UploadStreams = {
207
+ var inputOrNil : InputStream ? = nil
208
+ var outputOrNil : OutputStream ? = nil
209
+ Stream . getBoundStreams ( withBufferSize: BUFFER_SIZE,
210
+ inputStream: & inputOrNil,
211
+ outputStream: & outputOrNil)
212
+ guard let input = inputOrNil, let output = outputOrNil else {
213
+ fatalError ( " On return of `getBoundStreams`, both `inputStream` and `outputStream` will contain non-nil streams. " )
214
+ }
215
+ DispatchQueue . main. async {
216
+ // configure and open output stream
217
+ output. delegate = self
218
+ output. schedule ( in: . current, forMode: . default)
219
+ output. open ( )
220
+ }
221
+
222
+ self . session = session
223
+ self . task = task
224
+ state = . started
225
+
226
+ return UploadStreams ( input: input, output: output)
227
+ } ( )
228
+
229
+ func urlSession( _ session: URLSession , task: URLSessionTask ,
230
+ needNewBodyStream completionHandler: @escaping ( InputStream ? ) -> Void ) {
231
+ completionHandler ( boundStreams. input)
232
+ }
233
+
234
+ func urlSession( _ session: URLSession , task: URLSessionTask , needNewBodyStreamFrom offset: Int64 , completionHandler: @escaping @Sendable ( InputStream ? ) -> Void ) {
235
+ completionHandler ( boundStreams. input)
236
+ }
237
+
238
+ func urlSession( _ session: URLSession , task: URLSessionTask , didCompleteWithError error: ( any Error ) ? ) {
239
+ self . error = error? . localizedDescription
240
+ if error != nil {
241
+ state = . error
242
+ }
243
+ }
244
+
245
+ func urlSession( _ session: URLSession , didBecomeInvalidWithError error: ( any Error ) ? ) {
246
+ self . error = error? . localizedDescription
247
+ if error != nil {
248
+ state = . error
249
+ }
250
+ }
251
+
252
+ func urlSession( _ session: URLSession , task: URLSessionTask , didSendBodyData bytesSent: Int64 , totalBytesSent: Int64 , totalBytesExpectedToSend: Int64 )
253
+ {
254
+ //Tell the original delegate how much data was sent
255
+ originalDelegate. urlSession ? ( session, task: task, didSendBodyData: bytesSent, totalBytesSent: self . dataProgress, totalBytesExpectedToSend: self . size)
256
+ }
257
+
258
+ func waitForComplete( ) async -> DFUploadResponse ? {
259
+ while task? . state != . completed{
260
+ do {
261
+ try await Task . sleep ( for: . milliseconds( 100 ) )
262
+ }
263
+ catch {
264
+ }
265
+ }
266
+ return response
267
+ }
268
+
269
+ func urlSession( _ session: URLSession , dataTask: URLSessionDataTask , didReceive: Data ) {
270
+ if dataTask. httpResponse != nil {
271
+ if dataTask. httpResponse!. status == 200 { }
272
+ else if dataTask. httpResponse!. status == 499 {
273
+ error = " Upload timeout. (499) "
274
+ state = . error
275
+ return
276
+ }
277
+ else {
278
+ error = " Response error. ( \( dataTask. httpResponse!. status) ) "
279
+ state = . error
280
+ return
281
+ }
282
+ }
283
+
284
+ do {
285
+ response = try JSONDecoder ( ) . decode ( DFUploadResponse . self, from: didReceive)
286
+ }
287
+ catch {
288
+ response = nil
289
+ state = . error
290
+ }
291
+ }
292
+
293
+ func urlSession( _ session: URLSession , didCreateTask task: URLSessionTask ) {
294
+ self . task = task
295
+ }
296
+
297
+ func stream( _ aStream: Stream , handle eventCode: Stream . Event ) {
298
+ guard aStream == boundStreams. output else {
299
+ return
300
+ }
301
+ if eventCode. contains ( . openCompleted) {
302
+ print ( " DFAPI: Stream opened. " )
303
+ }
304
+ if eventCode. contains ( . hasSpaceAvailable) {
305
+ canWrite = true
306
+ }
307
+ if eventCode. contains ( . endEncountered) {
308
+ print ( " DFAPI: Stream closed. " )
309
+ }
310
+ if eventCode. contains ( . errorOccurred) {
311
+ error = " Stream error "
312
+ state = . complete
313
+ }
314
+ }
315
+
316
+ func isComplete( ) -> Bool {
317
+ return state == . complete || state == . error
318
+ }
319
+
320
+ init ( fileURL: URL , boundary: String , originalDelegate: URLSessionTaskDelegate , fileName: String ? = nil ) {
321
+ self . boundary = boundary
322
+ self . originalDelegate = originalDelegate
323
+ self . fileURL = fileURL
324
+
325
+ self . fileName = fileName ?? ( fileURL. absoluteString as NSString ) . lastPathComponent
326
+
327
+ self . intro = Data ( )
328
+ self . outro = Data ( )
329
+
330
+ self . dataProgress = 0
331
+
332
+ do {
333
+ let attributes = try FileManager . default. attributesOfItem ( atPath: fileURL. path ( percentEncoded: false ) )
334
+ self . size = attributes [ FileAttributeKey . size] as! Int64
335
+ self . fileStream = try FileHandle ( forReadingFrom: fileURL)
336
+ }
337
+ catch {
338
+ state = . complete
339
+ self . error = " Could not read file: \( error) "
340
+ self . fileStream = FileHandle ( )
341
+ self . size = 0
342
+ super. init ( )
343
+ return
344
+ }
345
+
346
+ super. init ( )
347
+
348
+ //Create the multipart form data
349
+ self . intro. append ( " \r \n -- \( self . boundary) \r \n " . data ( using: . utf8) !)
350
+ self . intro. append ( " Content-Disposition: form-data; name= \" file \" ; filename= \" \( self . fileName) \" \r \n " . data ( using: . utf8) !)
351
+ self . intro. append ( " Content-Type: application/octet-stream \r \n \r \n " . data ( using: . utf8) !)
352
+
353
+ self . outro. append ( " \r \n -- \( self . boundary) -- \r \n " . data ( using: . utf8) !)
354
+
355
+ DispatchQueue . main. async {
356
+ self . timer = Timer . scheduledTimer ( withTimeInterval: 0.0 , repeats: true ) {
357
+ [ weak self] timer in
358
+ guard let self = self else { return }
359
+
360
+ if self . canWrite {
361
+ let message : Data
362
+ switch state{
363
+ case . intro:
364
+ //Send as much of the multipart form data as possible
365
+ message = intro. subdata ( in: multipartProgress..< intro. count)
366
+ break
367
+ case . outro:
368
+ //Send as much of the multipart form data as possible
369
+ message = outro. subdata ( in: multipartProgress..< outro. count)
370
+ case . file:
371
+ do {
372
+ //Send as much of the file as possible
373
+ message = try fileStream. read ( upToCount: CHUNK_SIZE) !
374
+ }
375
+ catch {
376
+ self . state = . error
377
+ return
378
+ }
379
+ case . invalid:
380
+ return
381
+ case . started:
382
+ print ( " DFAPI: Timer started. " )
383
+ multipartProgress = 0
384
+ state = . intro
385
+ return
386
+ case . error:
387
+ print ( " DFAPI: File upload state error. " )
388
+ self . boundStreams. output. close ( )
389
+ timer. invalidate ( )
390
+ return
391
+ case . complete:
392
+ print ( " DFAPI: File upload complete. " )
393
+ self . boundStreams. output. close ( )
394
+ timer. invalidate ( )
395
+ return
396
+ }
397
+ let messageData = message
398
+ let messageCount = messageData. count
399
+ do {
400
+ let bytesWritten : Int = messageData. withUnsafeBytes {
401
+ self . boundStreams. output. write ( $0. bindMemory ( to: UInt8 . self) . baseAddress!, maxLength: messageData. count)
402
+ }
403
+ if self . state == . file {
404
+ //If uploading file, save the amount of data written to stream
405
+ dataProgress += Int64 ( bytesWritten)
406
+ }
407
+ if bytesWritten < messageCount {
408
+ if self . state == . file {
409
+ do {
410
+ //Not all data was written, seek back to where we left off
411
+ try fileStream. seek ( toOffset: fileStream. offset ( ) - UInt64( messageCount - bytesWritten) )
412
+ }
413
+ catch {
414
+ state = . error
415
+ self . error = " Stream overflowed and could not seek back. "
416
+ timer. invalidate ( )
417
+ }
418
+ }
419
+ }
420
+ if self . state != . file{
421
+ //Not all data was written, store how much was
422
+ self . multipartProgress += bytesWritten
423
+ }
424
+ }
425
+ canWrite = false
426
+
427
+ switch state{
428
+ case . intro:
429
+ if multipartProgress == intro. count{
430
+ print ( " DFAPI: Intro sent. " )
431
+ state = . file
432
+ multipartProgress = 0
433
+ }
434
+ break
435
+ case . outro:
436
+ if multipartProgress == outro. count{
437
+ print ( " DFAPI: Outro sent. " )
438
+ state = . complete
439
+ multipartProgress = 0
440
+ }
441
+ case . file:
442
+ do {
443
+ if try fileStream. offset ( ) == self . size {
444
+ print ( " DFAPI: File sent. " )
445
+ state = . outro
446
+ multipartProgress = 0
447
+ }
448
+ }
449
+ catch {
450
+ print ( " DFAPI: File upload read error. " )
451
+ self . error = " Read error "
452
+ state = . error
453
+ }
454
+ case . invalid:
455
+ return
456
+ case . started:
457
+ return
458
+ case . error:
459
+ print ( " DFAPI: File upload state error. " )
460
+ self . boundStreams. output. close ( )
461
+ timer. invalidate ( )
462
+ return
463
+ case . complete:
464
+ print ( " DFAPI: File upload complete. " )
465
+ self . boundStreams. output. close ( )
466
+ timer. invalidate ( )
467
+ return
468
+ }
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
0 commit comments