Skip to content
Merged
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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,33 +39,33 @@ You can conform to the `TUSClientDelegate` to receive updates from the `TUSClien

```swift
extension MyClass: TUSClientDelegate {
func didStartUpload(id: UUID, client: TUSClient) {
func didStartUpload(id: UUID, context: [String: String]?, client: TUSClient) {
print("TUSClient started upload, id is \(id)")
print("TUSClient remaining is \(client.remainingUploads)")
}

func didFinishUpload(id: UUID, url: URL, client: TUSClient) {
func didFinishUpload(id: UUID, url: URL, context: [String: String]?, client: TUSClient) {
print("TUSClient finished upload, id is \(id) url is \(url)")
print("TUSClient remaining is \(client.remainingUploads)")
if client.remainingUploads == 0 {
print("Finished uploading")
}
}

func uploadFailed(id: UUID, error: Error, client: TUSClient) {
func uploadFailed(id: UUID, error: Error, context: [String: String]?, client: TUSClient) {
print("TUSClient upload failed for \(id) error \(error)")
}

func fileError(error: TUSClientError, client: TUSClient) {
print("TUSClient File error \(error)")
func fileError(id: UUID?, error: TUSClientError, client: TUSClient) {
print("TUSClient File error \(error) for id \(id?.uuidString ?? "unknown")")
}


func totalProgress(bytesUploaded: Int, totalBytes: Int, client: TUSClient) {
}


func progressFor(id: UUID, bytesUploaded: Int, totalBytes: Int, client: TUSClient) {
func progressFor(id: UUID, context: [String: String]?, bytesUploaded: Int, totalBytes: Int, client: TUSClient) {

}

Expand Down
49 changes: 45 additions & 4 deletions Sources/TUSKit/TUSClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public protocol TUSClientDelegate: AnyObject {

/// Receive an error related to files. E.g. The `TUSClient` couldn't store a file or remove a file.
func fileError(error: TUSClientError, client: TUSClient)
/// Receive an error related to files. E.g. The `TUSClient` couldn't store a file or remove a file.
/// - Parameters:
/// - id: The upload identifier related to the error when available, otherwise nil.
func fileError(id: UUID?, error: TUSClientError, client: TUSClient)

/// Get the progress of all ongoing uploads combined
///
Expand All @@ -40,6 +44,10 @@ public extension TUSClientDelegate {
func progressFor(id: UUID, context: [String: String]?, progress: Float, client: TUSClient) {
// Optional
}

func fileError(id: UUID?, error: TUSClientError, client: TUSClient) {
fileError(error: error, client: client)
}
}

protocol ProgressDelegate: AnyObject {
Expand Down Expand Up @@ -231,6 +239,18 @@ public final class TUSClient {
scheduler.cancelTask(by: id)
}

/// Cancel an upload and remove its cached data so it will not be retried on the next `start()`.
/// - Parameter id: The id of the upload to cancel and delete.
/// - Returns: `true` if a cached upload was found and deleted, `false` otherwise.
/// - Throws: File-related errors if the cache exists but cannot be removed.
@discardableResult
public func cancelAndDelete(id: UUID) throws -> Bool {
scheduler.cancelTask(by: id)
// Wait for the cancel request to be processed before deleting cached data.
scheduler.queue.sync { }
return try removeCacheFor(id: id)
}

/// This will cancel all running uploads and clear the local cache.
/// Expect errors passed to the delegate for canceled tasks.
/// - Warning: This method is destructive and will remove any stored cache.
Expand Down Expand Up @@ -510,7 +530,7 @@ public final class TUSClient {
} catch let error {
let tusError = TUSClientError.couldnotRemoveFinishedUploads(underlyingError: error)
reportingQueue.async {
self.delegate?.fileError(error: tusError , client: self)
self.delegate?.fileError(id: nil, error: tusError , client: self)
}
}
}
Expand Down Expand Up @@ -629,7 +649,7 @@ public final class TUSClient {
} catch (let error) {
let tusError = TUSClientError.couldNotLoadData(underlyingError: error)
reportingQueue.async {
self.delegate?.fileError(error: tusError, client: self)
self.delegate?.fileError(id: nil, error: tusError, client: self)
}
return []
}
Expand Down Expand Up @@ -683,7 +703,7 @@ extension TUSClient: SchedulerDelegate {
} catch let error {
let tusError = TUSClientError.couldNotDeleteFile(underlyingError: error)
reportingQueue.async {
self.delegate?.fileError(error: tusError, client: self)
self.delegate?.fileError(id: uploadTask.metaData.id, error: tusError, client: self)
}
}

Expand Down Expand Up @@ -725,6 +745,23 @@ extension TUSClient: SchedulerDelegate {
return nil
}
}

func isCancellation(_ error: Error) -> Bool {
if let tusError = error as? TUSClientError, case .taskCancelled = tusError {
return true
}

if let apiError = error as? TUSAPIError, case let .underlyingError(underlying) = apiError {
return isCancellation(underlying)
}

if let urlError = error as? URLError, urlError.code == .cancelled {
return true
}

let nsError = error as NSError
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled
}

var shouldReturnEarly = false
queue.sync {
Expand All @@ -739,13 +776,17 @@ extension TUSClient: SchedulerDelegate {
return
}

if isCancellation(error) {
return
}

metaData.errorCount += 1
do {
try files.encodeAndStore(metaData: metaData)
} catch let error {
let tusError = TUSClientError.couldNotStoreFileMetadata(underlyingError: error)
reportingQueue.async {
self.delegate?.fileError(error: tusError, client: self)
self.delegate?.fileError(id: metaData.id, error: tusError, client: self)
}
}

Expand Down
1 change: 0 additions & 1 deletion Sources/TUSKit/Tasks/UploadDataTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ final class UploadDataTask: NSObject, IdentifiableTask {
guard offset < range.endIndex else { return nil }

let data = fileHandle.readData(ofLength: chunkSize)
print("read data of size \(data.count) at offset \(offset)")
offset += chunkSize
return data
} catch {
Expand Down
2 changes: 1 addition & 1 deletion TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ 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))%")
}
Expand Down
7 changes: 6 additions & 1 deletion Tests/TUSKitTests/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class TUSMockDelegate: TUSClientDelegate {
var startedUploads = [UUID]()
var finishedUploads = [(UUID, URL)]()
var failedUploads = [(UUID, Error)]()
var fileErrorsWithIds = [(UUID?, TUSClientError)]()
var fileErrors = [TUSClientError]()
var progressPerId = [UUID: Int]()
var totalProgressReceived = [Int]()
Expand Down Expand Up @@ -49,6 +50,11 @@ final class TUSMockDelegate: TUSClientDelegate {
fileErrorExpectation?.fulfill()
}

func fileError(id: UUID?, error: TUSClientError, client: TUSClient) {
fileErrorsWithIds.append((id, error))
fileError(error: error, client: client)
}

func uploadFailed(id: UUID, error: Error, context: [String : String]?, client: TUSClient) {
failedUploads.append((id, error))
uploadFailedExpectation?.fulfill()
Expand Down Expand Up @@ -142,4 +148,3 @@ final class MockURLProtocol: URLProtocol {
// This is called if the request gets canceled or completed.
}
}

76 changes: 71 additions & 5 deletions Tests/TUSKitTests/TUSClient/TUSClientInternalTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ final class TUSClientInternalTests: XCTestCase {

do {
relativeStoragePath = URL(string: "TUSTEST")!

let docDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
fullStoragePath = docDir.appendingPathComponent(relativeStoragePath.absoluteString)
files = try Files(storageDirectory: fullStoragePath)
clearDirectory(dir: fullStoragePath)
files = try Files(storageDirectory: relativeStoragePath)
fullStoragePath = files.storageDirectory
clearDirectory(dir: files.storageDirectory)

data = Data("abcdef".utf8)

Expand Down Expand Up @@ -89,4 +87,72 @@ final class TUSClientInternalTests: XCTestCase {
contents = try FileManager.default.contentsOfDirectory(at: fullStoragePath, includingPropertiesForKeys: nil)
XCTAssertFalse(contents.isEmpty, "The client is expected to NOT remove finished uploads on startup")
}

func testFileErrorIncludesIdWhenStoringMetadataFails() throws {
let failingId = UUID()
let missingFilePath = fullStoragePath.appendingPathComponent(failingId.uuidString)
try? FileManager.default.removeItem(at: missingFilePath)

let metaData = UploadMetadata(id: failingId,
filePath: missingFilePath,
uploadURL: URL(string: "https://tus.example.com/files")!,
size: 10,
customHeaders: [:],
mimeType: nil)
let creationTask = try CreationTask(metaData: metaData,
api: TUSAPI(session: URLSession(configuration: .ephemeral)),
files: files,
headerGenerator: HeaderGenerator(handler: nil))

let scheduler = Scheduler()
let expectation = expectation(description: "delegate receives file error with id")
tusDelegate.fileErrorExpectation = expectation

client.onError(error: TUSClientError.couldNotStoreFileMetadata(underlyingError: FilesError.relatedFileNotFound),
task: creationTask,
scheduler: scheduler)

waitForExpectations(timeout: 1)

XCTAssertEqual(tusDelegate.fileErrorsWithIds.count, 1)
XCTAssertEqual(tusDelegate.fileErrorsWithIds.first?.0, failingId)
if case .couldNotStoreFileMetadata = tusDelegate.fileErrorsWithIds.first?.1 {
// Expected error
} else {
XCTFail("Expected a couldNotStoreFileMetadata error")
}
}

func testCancelAndDeleteRemovesCacheAndPreventsResume() throws {
let clientFiles = try Files(storageDirectory: relativeStoragePath)
let id = UUID()
let path = try clientFiles.store(data: data, id: id)
let metaData = UploadMetadata(id: id, filePath: path, uploadURL: URL(string: "io.tus")!, size: data.count, customHeaders: [:], mimeType: nil)
try clientFiles.encodeAndStore(metaData: metaData)

let deleted = try client.cancelAndDelete(id: id)
XCTAssertTrue(deleted)
XCTAssertNil(try clientFiles.findMetadata(id: id))

let resumed = client.start()
XCTAssertTrue(resumed.isEmpty)
}

func testCancellationDoesNotIncrementErrorCountOrRetry() throws {
let metaData = try storeFiles()
let creationTask = try CreationTask(metaData: metaData,
api: TUSAPI(session: URLSession(configuration: .ephemeral)),
files: files,
headerGenerator: HeaderGenerator(handler: nil))
let scheduler = Scheduler()

XCTAssertEqual(metaData.errorCount, 0)

client.onError(error: TUSClientError.taskCancelled, task: creationTask, scheduler: scheduler)

scheduler.queue.sync { }

XCTAssertEqual(metaData.errorCount, 0)
XCTAssertTrue(scheduler.allTasks.isEmpty)
}
}