Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions Sources/TUSKit/TUSAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ final class TUSAPI {
private var backgroundHandler: (() -> Void)? = nil
private var callbacks: [String: (Result<(Data?, HTTPURLResponse), Error>) -> Void] = [:]
private var taskData: [String: Data] = [:]
private var progressObservations: [String: NSKeyValueObservation] = [:]
weak var progressDelegate: ProgressDelegate?

deinit {
progressObservations.values.forEach { $0.invalidate() }
if session.delegate is SessionDataDelegate {
session.finishTasksAndInvalidate()
}
Expand Down Expand Up @@ -285,6 +288,8 @@ final class TUSAPI {
task.taskDescription = metaData.id.uuidString
if #available(iOS 15.0, macOS 12, *), !session.configuration.sessionSendsLaunchEvents {
task.delegate = sessionDelegate
} else if !(session.delegate is SessionDataDelegate) {
observeProgressForCompatibility(task: task, totalBytesExpectedToSend: Int64(data.count))
}

queue.sync {
Expand Down Expand Up @@ -336,12 +341,14 @@ final class TUSAPI {
task.taskDescription = metaData.id.uuidString
if #available(iOS 15.0, macOS 12, *), !session.configuration.sessionSendsLaunchEvents {
task.delegate = sessionDelegate
} else if !(session.delegate is SessionDataDelegate) {
observeProgressForCompatibility(task: task, totalBytesExpectedToSend: Int64(length))
}

queue.sync {
self.callbacks[metaData.id.uuidString] = { result in
processResult(completion: completion) {
let (data, response) = try result.get()
let (_, response) = try result.get()
guard let offsetStr = response.allHeaderFields[caseInsensitive: "upload-offset"] as? String,
let offset = Int(offsetStr) else {
throw TUSAPIError.couldNotRetrieveOffset
Expand All @@ -353,12 +360,30 @@ final class TUSAPI {
task.resume()
return task
}


func observeProgressForCompatibility(task: URLSessionTask, totalBytesExpectedToSend: Int64) {
guard let identifier = task.taskDescription else { return }

let observation = task.progress.observe(\.completedUnitCount) { [weak self, weak task] progress, _ in
guard let self, let task else { return }
self.handleProgressForTask(task, totalBytesSent: progress.completedUnitCount, totalBytesExpectedToSend: totalBytesExpectedToSend)
}

queue.sync {
progressObservations[identifier] = observation
}
}

func handleProgressForTask(_ task: URLSessionTask, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
guard let identifier = task.taskDescription, let id = UUID(uuidString: identifier) else { return }
progressDelegate?.progressUpdated(forID: id, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend)
}

func registerCallback(_ completion: @escaping (Result<Int, TUSAPIError>) -> Void, forMetadata metadata: UploadMetadata) {
queue.sync {
self.callbacks[metadata.id.uuidString] = { result in
processResult(completion: completion) {
let (data, response) = try result.get()
let (_, response) = try result.get()
guard let offsetStr = response.allHeaderFields[caseInsensitive: "upload-offset"] as? String,
let offset = Int(offsetStr) else {
throw TUSAPIError.couldNotRetrieveOffset
Expand Down Expand Up @@ -442,7 +467,7 @@ extension Dictionary {
}
}

private extension TUSAPI {
extension TUSAPI {
final class SessionDataDelegate: NSObject, URLSessionDataDelegate {
weak var api: TUSAPI?

Expand All @@ -454,35 +479,40 @@ private extension TUSAPI {
api.taskData[identifier, default: Data()].append(data)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
api?.handleProgressForTask(task, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
api?.handleCompletionOfTask(task, withError: error)
}

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
api?.handleFinishOfBackgroundURLSessionEvents()
}
}

func handleCompletionOfTask(_ task: URLSessionTask, withError error: Error?) {
queue.sync {
guard let identifier = task.taskDescription else {
return
}

defer {
callbacks.removeValue(forKey: identifier)
taskData.removeValue(forKey: identifier)
progressObservations.removeValue(forKey: identifier)
}

guard let completion = callbacks[identifier] else {
return
}

if let error = error {
completion(.failure(TUSAPIError.underlyingError(error)))
return
}

guard let response = task.response as? HTTPURLResponse else {
completion(.failure(TUSAPIError.underlyingError(NetworkError.noHTTPURLResponse)))
return
Expand All @@ -493,7 +523,7 @@ private extension TUSAPI {
completion(.success(success))
}
}

func handleFinishOfBackgroundURLSessionEvents() {
if let backgroundHandler {
DispatchQueue.main.async {
Expand Down
61 changes: 32 additions & 29 deletions Sources/TUSKit/TUSClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,8 @@ public protocol TUSClientDelegate: AnyObject {
/// - Important: The total is based on active uploads, so it will lower once files are uploaded. This is because it's ambiguous what the total is. E.g. You can be uploading 100 bytes, after 50 bytes are uploaded, let's say you add 150 more bytes, is the total then 250 or 200? And what if the upload is done, and you add 50 more. Is the total 50 or 300? or 250?
///
/// As a rule of thumb: The total will be highest on the start, a good starting point is to compare the progress against that number.
@available(iOS 11.0, macOS 10.13, watchOS 6.0, *)
func totalProgress(bytesUploaded: Int, totalBytes: Int, client: TUSClient)

@available(iOS 11.0, macOS 10.13, watchOS 6.0, *)

/// Get the progress of a specific upload by id. The id is given when adding an upload and methods of this delegate.
func progressFor(id: UUID, context: [String: String]?, bytesUploaded: Int, totalBytes: Int, client: TUSClient)
}
Expand All @@ -57,8 +55,7 @@ public extension TUSClientDelegate {
}

protocol ProgressDelegate: AnyObject {
@available(iOS 11.0, macOS 10.13, watchOS 6.0, *)
func progressUpdatedFor(metaData: UploadMetadata, totalUploadedBytes: Int)
func progressUpdated(forID id: UUID, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
}

/// The TUSKit client.
Expand Down Expand Up @@ -89,9 +86,9 @@ public final class TUSClient {
private let api: TUSAPI
private let chunkSize: Int?
/// Keep track of uploads and their id's
private var uploads = [UUID: UploadMetadata]()
var uploads = [UUID: UploadMetadata]()
private let queue = DispatchQueue(label: "com.TUSKit.TUSClient")
private let reportingQueue: DispatchQueue
let reportingQueue: DispatchQueue
private let headerGenerator: HeaderGenerator

/// Initialize a TUSClient with support for background URLSessions and uploads
Expand Down Expand Up @@ -136,9 +133,10 @@ public final class TUSClient {
self.reportingQueue = reportingQueue
self.headerGenerator = HeaderGenerator(handler: generateHeaders)
scheduler.delegate = self
self.api.progressDelegate = self
reregisterCallbacks()
}

/// Initialize a TUSClient
/// - Parameters:
/// - server: The URL of the server where you want to upload to.
Expand Down Expand Up @@ -171,6 +169,7 @@ public final class TUSClient {
self.reportingQueue = reportingQueue
self.headerGenerator = HeaderGenerator(handler: generateHeaders)
scheduler.delegate = self
self.api.progressDelegate = self
removeFinishedUploads()
reregisterCallbacks()
}
Expand Down Expand Up @@ -209,10 +208,11 @@ public final class TUSClient {
self.reportingQueue = reportingQueue
self.headerGenerator = HeaderGenerator(handler: generateHeaders)
scheduler.delegate = self
self.api.progressDelegate = self
removeFinishedUploads()
reregisterCallbacks()
}

// MARK: - Starting and stopping

/// Kick off the client to start uploading any locally stored files.
Expand Down Expand Up @@ -546,7 +546,7 @@ public final class TUSClient {
guard let allMetadata = try? files.loadAllMetadata() else {
return
}

for metadata in allMetadata {
api.checkTaskExists(for: metadata) { [weak self] taskExists in
guard let self else {
Expand All @@ -556,11 +556,13 @@ public final class TUSClient {
let task = try? UploadDataTask(api: self.api, metaData: metadata, files: self.files, headerGenerator: self.headerGenerator) else {
return
}


self.queue.sync { self.uploads[metadata.id] = metadata }

self.api.registerCallback({ result in
task.taskCompleted(result: result, completed: { [weak self] result in
if case .failure = result {
try? self?.retry(id: metadata.id)
_ = try? self?.retry(id: metadata.id)
}
})
}, forMetadata: metadata)
Expand Down Expand Up @@ -605,7 +607,7 @@ public final class TUSClient {
}
}

guard let task = try taskFor(metaData: metaData, api: api, files: files, chunkSize: chunkSize, progressDelegate: self, headerGenerator: headerGenerator) else {
guard let task = try taskFor(metaData: metaData, api: api, files: files, chunkSize: chunkSize, headerGenerator: headerGenerator) else {
assertionFailure("Could not find a task for metaData \(metaData)")
return
}
Expand Down Expand Up @@ -664,7 +666,7 @@ public final class TUSClient {
/// Schedule a single task if needed. Will decide what task to schedule for the metaData.
/// - Parameter metaData:The metaData the schedule.
private func scheduleTask(for metaData: UploadMetadata) throws {
guard let task = try taskFor(metaData: metaData, api: api, files: files, chunkSize: chunkSize, progressDelegate: self, headerGenerator: headerGenerator) else {
guard let task = try taskFor(metaData: metaData, api: api, files: files, chunkSize: chunkSize, headerGenerator: headerGenerator) else {
throw TUSClientError.uploadIsAlreadyFinished
}
queue.sync {
Expand Down Expand Up @@ -826,26 +828,30 @@ private extension String {
/// Decide which task to create based on metaData.
/// - Parameter metaData: The `UploadMetadata` for which to create a `Task`.
/// - Returns: The task that has to be performed for the relevant metaData. Will return nil if metaData's file is already uploaded / finished. (no task needed).
func taskFor(metaData: UploadMetadata, api: TUSAPI, files: Files, chunkSize: Int?, progressDelegate: ProgressDelegate? = nil, headerGenerator: HeaderGenerator) throws -> ScheduledTask? {
func taskFor(metaData: UploadMetadata, api: TUSAPI, files: Files, chunkSize: Int?, headerGenerator: HeaderGenerator) throws -> ScheduledTask? {
guard !metaData.isFinished else {
return nil
}

if let remoteDestination = metaData.remoteDestination {
let statusTask = StatusTask(api: api, remoteDestination: remoteDestination, metaData: metaData, files: files, chunkSize: chunkSize, headerGenerator: headerGenerator)
statusTask.progressDelegate = progressDelegate
return statusTask
return StatusTask(api: api, remoteDestination: remoteDestination, metaData: metaData, files: files, chunkSize: chunkSize, headerGenerator: headerGenerator)
} else {
let creationTask = try CreationTask(metaData: metaData, api: api, files: files, chunkSize: chunkSize, headerGenerator: headerGenerator)
creationTask.progressDelegate = progressDelegate
return creationTask
return try CreationTask(metaData: metaData, api: api, files: files, chunkSize: chunkSize, headerGenerator: headerGenerator)
}
}

extension TUSClient: ProgressDelegate {

@available(iOS 11.0, macOS 10.13, watchOS 6.0, *)
func progressUpdatedFor(metaData: UploadMetadata, totalUploadedBytes: Int) {

func progressUpdated(forID id: UUID, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
var metaData: UploadMetadata?
queue.sync {
metaData = self.uploads[id]
}
guard let metaData else { return }

let alreadyUploaded = metaData.uploadedRange?.count ?? 0
let totalUploadedBytes = alreadyUploaded + Int(totalBytesSent)

reportingQueue.async {
self.delegate?.progressFor(id: metaData.id, context: metaData.context, bytesUploaded: totalUploadedBytes, totalBytes: metaData.size, client: self)
}
Expand All @@ -858,10 +864,7 @@ extension TUSClient: ProgressDelegate {
uploadsCopy = self.uploads
}
for (_, metaDataForTotal) in uploadsCopy {
guard metaDataForTotal.id != metaData.id else {
continue
}

guard metaDataForTotal.id != id else { continue }
totalBytesUploaded += metaDataForTotal.uploadedRange?.count ?? 0
totalSize += metaDataForTotal.size
}
Expand Down
3 changes: 0 additions & 3 deletions Sources/TUSKit/Tasks/CreationTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ final class CreationTask: IdentifiableTask {
metaData.id
}

weak var progressDelegate: ProgressDelegate?
let metaData: UploadMetadata

private let api: TUSAPI
Expand Down Expand Up @@ -56,7 +55,6 @@ final class CreationTask: IdentifiableTask {
let files = self.files
let chunkSize = self.chunkSize
let api = self.api
let progressDelegate = self.progressDelegate

do {
let remoteDestination = try result.get()
Expand All @@ -69,7 +67,6 @@ final class CreationTask: IdentifiableTask {
} else {
task = try UploadDataTask(api: api, metaData: metaData, files: files, headerGenerator: self.headerGenerator)
}
task.progressDelegate = progressDelegate
if self.didCancel {
completed(.failure(TUSClientError.taskCancelled))
} else {
Expand Down
3 changes: 0 additions & 3 deletions Sources/TUSKit/Tasks/StatusTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ final class StatusTask: IdentifiableTask {
metaData.id
}

weak var progressDelegate: ProgressDelegate?
let api: TUSAPI
let files: Files
let remoteDestination: URL
Expand Down Expand Up @@ -57,7 +56,6 @@ final class StatusTask: IdentifiableTask {
let files = self.files
let chunkSize = self.chunkSize
let api = self.api
let progressDelegate = self.progressDelegate

do {
let status = try result.get()
Expand Down Expand Up @@ -92,7 +90,6 @@ final class StatusTask: IdentifiableTask {
}

let task = try UploadDataTask(api: api, metaData: metaData, files: files, range: nextRange, headerGenerator: self.headerGenerator)
task.progressDelegate = progressDelegate
completed(.success([task]))
}
} catch let error as TUSClientError {
Expand Down
Loading