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..1802d441 100644 --- a/Sources/TUSKit/Tasks/UploadDataTask.swift +++ b/Sources/TUSKit/Tasks/UploadDataTask.swift @@ -114,12 +114,13 @@ 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, responseHeaders) = try result.get() + metaData.responseHeaders = responseHeaders let currentOffset = metaData.uploadedRange?.upperBound ?? 0 metaData.uploadedRange = 0..? if let range = range { let chunkSize = range.count @@ -146,7 +147,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..0673d802 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.decodeIfPresent([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() }