From 9cf0de1ba792e0d663cb1930415db86c42f03563 Mon Sep 17 00:00:00 2001 From: srvarma7 <48859937+srvarma7@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:38:14 +0530 Subject: [PATCH 1/2] Feat: Response header support in didFinishUpload and fix data racing --- .../Extensions/HTTPURLResponse+Headers.swift | 18 ++++++++++++++++ Sources/TUSKit/TUSAPI.swift | 14 +++++++------ Sources/TUSKit/TUSClient.swift | 21 +++++++++++++++---- Sources/TUSKit/Tasks/UploadDataTask.swift | 14 ++++++------- Sources/TUSKit/UploadMetada.swift | 9 ++++++-- .../TUSKitExample/Helpers/TUSWrapper.swift | 8 +++++++ .../Screens/Upload/UploadsListView.swift | 2 +- 7 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 Sources/TUSKit/Extensions/HTTPURLResponse+Headers.swift diff --git a/Sources/TUSKit/Extensions/HTTPURLResponse+Headers.swift b/Sources/TUSKit/Extensions/HTTPURLResponse+Headers.swift new file mode 100644 index 00000000..f751f568 --- /dev/null +++ b/Sources/TUSKit/Extensions/HTTPURLResponse+Headers.swift @@ -0,0 +1,18 @@ +// +// HTTPURLResponse+Headers.swift +// +// +// Created by Sai Raghu Varma Kallepalli on 11/07/23. +// + +import Foundation + +extension HTTPURLResponse { + func extractHeaders() -> [String: String] { + var headers: [String: String] = [:] + allHeaderFields.forEach { key, value in + headers[String(describing: key)] = String(describing: value) + } + return headers + } +} diff --git a/Sources/TUSKit/TUSAPI.swift b/Sources/TUSKit/TUSAPI.swift index e6ea7dee..9f98ce28 100644 --- a/Sources/TUSKit/TUSAPI.swift +++ b/Sources/TUSKit/TUSAPI.swift @@ -48,6 +48,8 @@ struct Status { let offset: Int } +typealias UploadTaskResult = (Result<(Int, responseHeaders: [String : String]?), TUSAPIError>) + /// The Uploader's responsibility is to perform work related to uploading. /// This includes: Making requests, handling requests, handling errors. final class TUSAPI { @@ -262,7 +264,7 @@ final class TUSAPI { /// - location: The location of where to upload to. /// - completion: Completionhandler for when the upload is finished. @discardableResult - func upload(data: Data, range: Range?, location: URL, metaData: UploadMetadata, customHeaders: [String: String], completion: @escaping (Result) -> Void) -> URLSessionUploadTask { + func upload(data: Data, range: Range?, location: URL, metaData: UploadMetadata, customHeaders: [String: String], completion: @escaping (UploadTaskResult) -> Void) -> URLSessionUploadTask { let offset: Int let length: Int if let range = range { @@ -305,7 +307,7 @@ final class TUSAPI { let offset = Int(offsetStr) else { throw TUSAPIError.couldNotRetrieveOffset } - return offset + return (offset, response.extractHeaders()) } } } @@ -315,7 +317,7 @@ final class TUSAPI { return task } - func upload(fromFile file: URL, offset: Int = 0, location: URL, metaData: UploadMetadata, customHeaders: [String: String], completion: @escaping (Result) -> Void) -> URLSessionUploadTask { + func upload(fromFile file: URL, offset: Int = 0, location: URL, metaData: UploadMetadata, customHeaders: [String: String], completion: @escaping (UploadTaskResult) -> Void) -> URLSessionUploadTask { let length: Int if let fileAttributes = try? FileManager.default.attributesOfItem(atPath: file.path) { if let bytes = fileAttributes[.size] as? Int64 { @@ -353,7 +355,7 @@ final class TUSAPI { let offset = Int(offsetStr) else { throw TUSAPIError.couldNotRetrieveOffset } - return offset + return (offset, response.extractHeaders()) } } } @@ -379,7 +381,7 @@ final class TUSAPI { progressDelegate?.progressUpdated(forID: id, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend) } - func registerCallback(_ completion: @escaping (Result) -> Void, forMetadata metadata: UploadMetadata) { + func registerCallback(_ completion: @escaping (UploadTaskResult) -> Void, forMetadata metadata: UploadMetadata) { queue.sync { self.callbacks[metadata.id.uuidString] = { result in processResult(completion: completion) { @@ -388,7 +390,7 @@ final class TUSAPI { let offset = Int(offsetStr) else { throw TUSAPIError.couldNotRetrieveOffset } - return offset + return (offset, response.extractHeaders()) } } } diff --git a/Sources/TUSKit/TUSClient.swift b/Sources/TUSKit/TUSClient.swift index 698e5ade..4a45dcc1 100644 --- a/Sources/TUSKit/TUSClient.swift +++ b/Sources/TUSKit/TUSClient.swift @@ -22,7 +22,10 @@ public protocol TUSClientDelegate: AnyObject { /// TUSClient is starting an upload func didStartUpload(id: UUID, context: [String: String]?, client: TUSClient) /// `TUSClient` just finished an upload, returns the URL of the uploaded file. + @available(*, deprecated, message: "Use the new didFinishUpload(id: UUID, url: URL, context: [String: String]?, client: TUSClient, responseHeaders: [String: String]?) method instead.") func didFinishUpload(id: UUID, url: URL, context: [String: String]?, client: TUSClient) + /// `TUSClient` just finished an upload, returns the URL of the uploaded file along with `responseHeaders` from server. + func didFinishUpload(id: UUID, url: URL, context: [String: String]?, client: TUSClient, responseHeaders: [String: String]?) /// An upload failed. Returns an error. Could either be a TUSClientError or a networking related error. func uploadFailed(id: UUID, error: Error, context: [String: String]?, client: TUSClient) @@ -48,6 +51,12 @@ public extension TUSClientDelegate { func progressFor(id: UUID, context: [String: String]?, progress: Float, client: TUSClient) { // Optional } + + /// `TUSClient` just finished an upload, returns the URL of the uploaded file along with `responseHeaders` from server. + func didFinishUpload(id: UUID, url: URL, context: [String: String]?, client: TUSClient, responseHeaders: [String: String]?) { + // Supports calling the existing implementation without forcing conformance to the new delegate function. + didFinishUpload(id: id, url: url, context: context, client: client) + } func fileError(id: UUID?, error: TUSClientError, client: TUSClient) { fileError(error: error, client: client) @@ -719,13 +728,17 @@ extension TUSClient: SchedulerDelegate { assertionFailure("Somehow uploaded task did not have a url") return } - + + didFinishUpload(id: uploadTask.metaData.id, url: url, context: uploadTask.metaData.context, client: self, responseHeaders: uploadTask.metaData.responseHeaders) + } + + private func didFinishUpload(id: UUID, url: URL, context: [String: String]?, client: TUSClient, responseHeaders: [String: String]?) { queue.sync { - self.uploads[uploadTask.metaData.id] = nil + self.uploads[id] = nil } - headerGenerator.clearHeaders(for: uploadTask.metaData.id) + headerGenerator.clearHeaders(for: id) reportingQueue.async { - self.delegate?.didFinishUpload(id: uploadTask.metaData.id, url: url, context: uploadTask.metaData.context, client: self) + self.delegate?.didFinishUpload(id: id, url: url, context: context, client: self, responseHeaders: responseHeaders) } } diff --git a/Sources/TUSKit/Tasks/UploadDataTask.swift b/Sources/TUSKit/Tasks/UploadDataTask.swift index cef5ddc6..435c5029 100644 --- a/Sources/TUSKit/Tasks/UploadDataTask.swift +++ b/Sources/TUSKit/Tasks/UploadDataTask.swift @@ -114,12 +114,12 @@ final class UploadDataTask: NSObject, IdentifiableTask { } } - func taskCompleted(result: Result, completed: @escaping TaskCompletion) { + func taskCompleted(result: UploadTaskResult, completed: @escaping TaskCompletion) { do { - let receivedOffset = try result.get() + let receivedOffset = try result.get().0 let currentOffset = metaData.uploadedRange?.upperBound ?? 0 metaData.uploadedRange = 0..? if let range = range { let chunkSize = range.count @@ -146,7 +146,7 @@ final class UploadDataTask: NSObject, IdentifiableTask { } else { nextRange = nil } - + let task = try UploadDataTask(api: api, metaData: metaData, files: files, range: nextRange, headerGenerator: headerGenerator) completed(.success([task])) } catch let error as TUSClientError { diff --git a/Sources/TUSKit/UploadMetada.swift b/Sources/TUSKit/UploadMetada.swift index cd493027..d442bc5f 100644 --- a/Sources/TUSKit/UploadMetada.swift +++ b/Sources/TUSKit/UploadMetada.swift @@ -26,8 +26,8 @@ final class UploadMetadata: Codable { case customHeaders case size case errorCount + case responseHeaders case appliedCustomHeaders - } var isFinished: Bool { @@ -50,6 +50,8 @@ final class UploadMetadata: Codable { } } + var responseHeaders: [String: String]? + private var _remoteDestination: URL? var remoteDestination: URL? { get { @@ -112,10 +114,11 @@ final class UploadMetadata: Codable { } } - init(id: UUID, filePath: URL, uploadURL: URL, size: Int, customHeaders: [String: String]? = nil, mimeType: String? = nil, context: [String: String]? = nil) { + init(id: UUID, filePath: URL, uploadURL: URL, responseHeaders: [String: String]? = nil, size: Int, customHeaders: [String: String]? = nil, mimeType: String? = nil, context: [String: String]? = nil) { self.id = id self._filePath = filePath self.uploadURL = uploadURL + self.responseHeaders = responseHeaders self.size = size self._customHeaders = customHeaders self.mimeType = mimeType @@ -138,6 +141,7 @@ final class UploadMetadata: Codable { _customHeaders = try values.decode([String: String]?.self, forKey: .customHeaders) size = try values.decode(Int.self, forKey: .size) _errorCount = try values.decode(Int.self, forKey: .errorCount) + responseHeaders = try values.decode([String: String].self, forKey: .responseHeaders) _appliedCustomHeaders = try values.decode([String: String]?.self, forKey: .appliedCustomHeaders) } @@ -154,6 +158,7 @@ final class UploadMetadata: Codable { try container.encode(_customHeaders, forKey: .customHeaders) try container.encode(size, forKey: .size) try container.encode(_errorCount, forKey: .errorCount) + try container.encode(responseHeaders, forKey: .responseHeaders) try container.encode(_appliedCustomHeaders, forKey: .appliedCustomHeaders) } diff --git a/TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift b/TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift index 10362cb9..49981736 100644 --- a/TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift +++ b/TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift @@ -98,6 +98,12 @@ extension TUSWrapper: TUSClientDelegate { } func didFinishUpload(id: UUID, url: URL, context: [String : String]?, client: TUSClient) { + /// This function is called if the following + /// `didFinishUpload(id:UUID, url:URL, context:[String:String]?, client:TUSClient, responseHeaders:[String:String]?)` + /// delegate method is not implemented. + } + + func didFinishUpload(id: UUID, url: URL, context: [String : String]?, client: TUSClient, responseHeaders: [String : String]?) { Task { @MainActor in withAnimation { uploads[id] = .uploaded(url: url) @@ -123,7 +129,9 @@ extension TUSWrapper: TUSClientDelegate { } } + func fileError(error: TUSClientError, client: TUSClient) {} func fileError(id: UUID?, error: TUSClientError, client: TUSClient) { } + func totalProgress(bytesUploaded: Int, totalBytes: Int, client: TUSClient) { print("total progress: \(bytesUploaded) / \(totalBytes) => \(Int(Double(bytesUploaded) / Double(totalBytes) * 100))%") } diff --git a/TUSKitExample/TUSKitExample/Screens/Upload/UploadsListView.swift b/TUSKitExample/TUSKitExample/Screens/Upload/UploadsListView.swift index 65263a42..e22d7d6b 100644 --- a/TUSKitExample/TUSKitExample/Screens/Upload/UploadsListView.swift +++ b/TUSKitExample/TUSKitExample/Screens/Upload/UploadsListView.swift @@ -105,7 +105,7 @@ extension UploadsListView { Spacer() Text(uploadCategory.noRecoredMessage) if uploadCategory == .all { - (Text("Upload files for ") + (Text("Upload files ") + Text(Image(systemName: Icon.uploadFileFilled.rawValue))).foregroundColor(.blue) + Text(" tab")) + (Text("Upload files from ") + (Text("Upload files ") + Text(Image(systemName: Icon.uploadFileFilled.rawValue))).foregroundColor(.blue) + Text(" tab")) } Spacer() } From fd7aff96c2a992255caecc5831ed1030144470b2 Mon Sep 17 00:00:00 2001 From: Lefteris Haritou Date: Fri, 26 Jun 2026 12:30:48 +0300 Subject: [PATCH 2/2] Store response headers and allow absent headers UploadDataTask now unpacks the upload result as (offset, responseHeaders) and saves responseHeaders into UploadMetadata. UploadMetadata decoding was made tolerant by using decodeIfPresent for responseHeaders so older metadata without that key won't fail decoding. This propagates server response headers into metadata and avoids crashes when the field is missing. --- Sources/TUSKit/Tasks/UploadDataTask.swift | 3 ++- Sources/TUSKit/UploadMetada.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/TUSKit/Tasks/UploadDataTask.swift b/Sources/TUSKit/Tasks/UploadDataTask.swift index 435c5029..1802d441 100644 --- a/Sources/TUSKit/Tasks/UploadDataTask.swift +++ b/Sources/TUSKit/Tasks/UploadDataTask.swift @@ -116,7 +116,8 @@ final class UploadDataTask: NSObject, IdentifiableTask { func taskCompleted(result: UploadTaskResult, completed: @escaping TaskCompletion) { do { - let receivedOffset = try result.get().0 + let (receivedOffset, responseHeaders) = try result.get() + metaData.responseHeaders = responseHeaders let currentOffset = metaData.uploadedRange?.upperBound ?? 0 metaData.uploadedRange = 0..