@@ -13,6 +13,7 @@ final class NitroFSFileUploader: NSObject, URLSessionDataDelegate {
1313 private var uploadSessions : [ String : URLSession ] = [ : ]
1414 private var progressCallbacks : [ String : ( ( Double , Double ) -> Void ) ] = [ : ]
1515 private var taskToJobId : [ Int : String ] = [ : ]
16+ private var uploadContinuations : [ String : CheckedContinuation < Void , Error > ] = [ : ]
1617 private let taskQueue = DispatchQueue ( label: " com.nitrofs.uploader " )
1718
1819 init ( fileManager: FileManager ) {
@@ -48,34 +49,28 @@ final class NitroFSFileUploader: NSObject, URLSessionDataDelegate {
4849
4950 let session = URLSession ( configuration: . default, delegate: self , delegateQueue: nil )
5051
51- let task = session. uploadTask ( with: request, fromFile: multipartFile) { _, response, error in
52- defer {
53- try ? FileManager . default. removeItem ( at: multipartFile)
54- session. finishTasksAndInvalidate ( )
55- self . taskQueue. async {
56- self . uploadTasks. removeValue ( forKey: jobId)
57- self . uploadSessions. removeValue ( forKey: jobId)
58- self . progressCallbacks. removeValue ( forKey: jobId)
59- self . taskToJobId. removeValue ( forKey: task. taskIdentifier)
60- }
61- }
62-
63- // Note: Errors are silently ignored here since the jobId is returned immediately
64- // The user can check progress via onProgress callback or cancel if needed
65- }
52+ let task = session. uploadTask ( with: request, fromFile: multipartFile)
6653
6754 // Use sync to ensure state is initialized before task.resume() is called
6855 // This prevents race condition where delegate callbacks fire before state is set up
69- taskQueue. sync {
70- self . uploadTasks [ jobId] = task
71- self . uploadSessions [ jobId] = session
72- self . taskToJobId [ task. taskIdentifier] = jobId
73- if let onProgress = onProgress {
74- self . progressCallbacks [ jobId] = onProgress
56+ try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , Error > ) in
57+ taskQueue. sync {
58+ self . uploadTasks [ jobId] = task
59+ self . uploadSessions [ jobId] = session
60+ self . taskToJobId [ task. taskIdentifier] = jobId
61+ self . uploadContinuations [ jobId] = continuation
62+ if let onProgress = onProgress {
63+ self . progressCallbacks [ jobId] = onProgress
64+ }
7565 }
66+ task. resume ( )
67+ }
68+
69+ // Cleanup multipart file after completion
70+ defer {
71+ try ? FileManager . default. removeItem ( at: multipartFile)
7672 }
7773
78- task. resume ( )
7974 return jobId
8075 }
8176
@@ -85,6 +80,11 @@ final class NitroFSFileUploader: NSObject, URLSessionDataDelegate {
8580 guard let self = self else { return }
8681 if let task = self . uploadTasks [ jobId] {
8782 task. cancel ( )
83+ // Resume continuation with cancellation error if it exists
84+ if let continuation = self . uploadContinuations [ jobId] {
85+ continuation. resume ( throwing: NitroFSError . networkError ( message: " Upload cancelled " ) )
86+ self . uploadContinuations. removeValue ( forKey: jobId)
87+ }
8888 self . taskToJobId. removeValue ( forKey: task. taskIdentifier)
8989 self . uploadTasks. removeValue ( forKey: jobId)
9090 self . progressCallbacks. removeValue ( forKey: jobId)
@@ -177,6 +177,60 @@ extension NitroFSFileUploader: URLSessionTaskDelegate {
177177 }
178178 }
179179 }
180+
181+ func urlSession(
182+ _ session: URLSession ,
183+ task: URLSessionTask ,
184+ didCompleteWithError error: Error ?
185+ ) {
186+ var jobId : String ?
187+ var continuation : CheckedContinuation < Void , Error > ?
188+
189+ taskQueue. sync {
190+ jobId = self . taskToJobId [ task. taskIdentifier]
191+ if let jobId = jobId {
192+ continuation = self . uploadContinuations [ jobId]
193+ }
194+ }
195+
196+ guard let jobId = jobId else { return }
197+
198+ // Remove continuation immediately to prevent double-resume
199+ taskQueue. sync {
200+ self . uploadContinuations. removeValue ( forKey: jobId)
201+ }
202+
203+ if let error = error {
204+ // Handle cancellation separately
205+ if ( error as NSError ) . code == NSURLErrorCancelled {
206+ continuation? . resume ( throwing: NitroFSError . networkError ( message: " Upload cancelled " ) )
207+ } else {
208+ continuation? . resume ( throwing: error)
209+ }
210+ } else {
211+ // Check HTTP response status
212+ if let httpResponse = task. response as? HTTPURLResponse {
213+ guard ( 200 ... 299 ) . contains ( httpResponse. statusCode) else {
214+ continuation? . resume ( throwing: NitroFSError . networkError (
215+ message: " Upload failed with status code: \( httpResponse. statusCode) "
216+ ) )
217+ return
218+ }
219+ }
220+ // Success - resume continuation
221+ continuation? . resume ( )
222+ }
223+
224+ // Cleanup
225+ taskQueue. async {
226+ self . uploadTasks. removeValue ( forKey: jobId)
227+ self . uploadSessions. removeValue ( forKey: jobId)
228+ self . progressCallbacks. removeValue ( forKey: jobId)
229+ self . taskToJobId. removeValue ( forKey: task. taskIdentifier)
230+ }
231+
232+ session. finishTasksAndInvalidate ( )
233+ }
180234}
181235
182236// MARK: - OutputStream Extension
0 commit comments