Skip to content

Commit 9e3743e

Browse files
authored
Merge pull request #9 from django-files/dev
Dev
2 parents bfcd158 + fabfc4f commit 9e3743e

File tree

3 files changed

+360
-16
lines changed

3 files changed

+360
-16
lines changed

Django Files/API/DFAPI.swift

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ struct DFAPI {
8383
}
8484
return responseBody
8585
}
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+
}
8697

8798
public func getStats(amount: Int? = nil) async -> DFStatsResponse?{
8899
do{
@@ -120,6 +131,20 @@ struct DFAPI {
120131
}
121132
}
122133

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+
123148
public func createShort(url: URL, short: String, maxViews: Int? = nil) async -> DFShortResponse?{
124149
let request = DFShortRequest(url: url.absoluteString, vanity: short, maxViews: maxViews ?? 0)
125150
do{
@@ -132,3 +157,317 @@ struct DFAPI {
132157
}
133158
}
134159
}
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

Comments
 (0)