diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b6b9414e..f5cb3e30 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,7 +3,7 @@ However to keep things clean and neat we ask you follow these small guidelines to help ensure everything runs smoothly. ## Pull Requests - Please direct all Pull Requests to the `swift-development` branch of the repo, to ensure that master will always match with the CocoaPod. + Please direct all Pull Requests to the `main` branch of the repo. Also please diff --git a/.github/workflows/tuskit-ci.yml b/.github/workflows/tuskit-ci.yml index ce5e2d11..a5a4ba54 100644 --- a/.github/workflows/tuskit-ci.yml +++ b/.github/workflows/tuskit-ci.yml @@ -6,7 +6,7 @@ jobs: strategy: matrix: os: ["macos-latest"] - swift: ["5.5"] + swift: ["5"] runs-on: ${{ matrix.os }} steps: - name: Extract Branch Name @@ -19,9 +19,3 @@ jobs: run: swift build -Xswiftc --disable-experimental-concurrency - name: Run tests run: swift test -Xswiftc --disable-experimental-concurrency - - uses: 8398a7/action-slack@v3 - if: failure() && env.BRANCH == 'master' - with: - status: failure - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/TUSKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/TUSKit.xcscheme new file mode 100644 index 00000000..070cd6f2 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/TUSKit.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 34d9b48a..60d4a28c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +# 3.1.7 + +# Enhancements +- It's now possible to inspect the status code for failed uploads that did not have a 200 OK HTTP status code. See the following example from the sample app: + +```swift +func uploadFailed(id: UUID, error: Error, context: [String : String]?, client: TUSClient) { + Task { @MainActor in + uploads[id] = .failed(error: error) + + if case TUSClientError.couldNotUploadFile(underlyingError: let underlyingError) = error, + case TUSAPIError.failedRequest(let response) = underlyingError { + print("upload failed with response \(response)") + } + } +} +``` + +# 3.1.6 + +# Enhancements +- Added ability to fetch in progress / current uploads using `getStoredUploads()` on a `TUSClient` instance. + +# 3.1.5 +## Fixed +- Fixed issue with missing custom headers. + +# 3.1.4 +## Fixed +- Fix compile error Xcode 14 + # 3.1.3 ## Fixed - Added `supportedExtensions` to client diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..db3301aa --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 tus - Resumable File Uploads + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift index f6590027..87fa8e45 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "TUSKit", - platforms: [.iOS(.v10), .macOS(.v10_10)], + platforms: [.iOS(.v13), .macOS(.v11)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( diff --git a/ROADMAP.md b/ROADMAP.md index 8b137891..67460e30 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1 +1,14 @@ +# Roadmap +- [x] Release version with latest changes (changes: https://github.com/tus/TUSKit/compare/3.1.4...main, relevant issues: https://github.com/tus/TUSKit/issues/138, https://github.com/tus/TUSKit/issues/141 and https://community.transloadit.com/t/ios-tus-client-version-update/16395/3) +- [ ] Create a documentation describing the release process (pods, swift package manager?) +- [ ] Update metadata (author, language, repo) in CocoaPods: https://cocoapods.org/pods/TUSKit +- [ ] Create an example app where TUSKit is used in (would show you how to use the client from a customer’s point of view. Should help evolve the API) + - [x] Add pause / resume functionality + - [x] Add progress indicator + - [x] Add ability to resume uploads from previous app session + - [ ] Add ability to upload files in background +- [ ] Review and address issues & PRs in GitHub until there are zero +- [ ] ~~Create a release blog post on tus.io with the changes from 2.x to 3.x and some usage examples~~ +- [ ] Fix CI tests +- [ ] Think about automating releasing using CI diff --git a/Sources/TUSKit/Files.swift b/Sources/TUSKit/Files.swift index ca09e80e..adcc8b13 100644 --- a/Sources/TUSKit/Files.swift +++ b/Sources/TUSKit/Files.swift @@ -67,7 +67,16 @@ final class Files { } static private var documentsDirectory: URL { +#if os(macOS) + var directory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + if let bundleId = Bundle.main.bundleIdentifier { + directory = directory.appendingPathComponent(bundleId) + } + return directory +#else return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] +#endif + } /// Loads all metadata (decoded plist files) from the target directory. @@ -80,7 +89,6 @@ final class Files { /// - Returns: An array of UploadMetadata types func loadAllMetadata() throws -> [UploadMetadata] { try queue.sync { - let directoryContents = try FileManager.default.contentsOfDirectory(at: storageDirectory, includingPropertiesForKeys: nil) // if you want to filter the directory contents you can do like this: @@ -153,11 +161,14 @@ final class Files { /// - Throws: Any error from FileManager when removing a file. func removeFileAndMetadata(_ metaData: UploadMetadata) throws { let filePath = metaData.filePath - let metaDataPath = metaData.filePath.appendingPathExtension("plist") + let fileName = filePath.lastPathComponent + let metaDataPath = storageDirectory.appendingPathComponent(fileName).appendingPathExtension("plist") try queue.sync { - try FileManager.default.removeItem(at: filePath) try FileManager.default.removeItem(at: metaDataPath) +#if os(iOS) + try FileManager.default.removeItem(at: filePath) +#endif } } @@ -174,8 +185,8 @@ final class Files { // Could not find the file that's related to this metadata. throw FilesError.relatedFileNotFound } - - let targetLocation = metaData.filePath.appendingPathExtension("plist") + let fileName = metaData.filePath.lastPathComponent + let targetLocation = storageDirectory.appendingPathComponent(fileName).appendingPathExtension("plist") try self.makeDirectoryIfNeeded() let encoder = PropertyListEncoder() diff --git a/Sources/TUSKit/TUSAPI.swift b/Sources/TUSKit/TUSAPI.swift index acb8fc69..dd351efd 100644 --- a/Sources/TUSKit/TUSAPI.swift +++ b/Sources/TUSKit/TUSAPI.swift @@ -8,11 +8,12 @@ import Foundation /// The errors a TUSAPI can return -enum TUSAPIError: Error { +public enum TUSAPIError: Error { case underlyingError(Error) case couldNotFetchStatus case couldNotRetrieveOffset case couldNotRetrieveLocation + case failedRequest(HTTPURLResponse) } /// The status of an upload. @@ -51,6 +52,10 @@ final class TUSAPI { processResult(completion: completion) { let (_, response) = try result.get() + guard (200...299).contains(response.statusCode) else { + throw TUSAPIError.failedRequest(response) + } + guard let lengthStr = response.allHeaderFields[caseInsensitive: "upload-Length"] as? String, let length = Int(lengthStr), let offsetStr = response.allHeaderFields[caseInsensitive: "upload-Offset"] as? String, @@ -78,6 +83,10 @@ final class TUSAPI { let task = session.dataTask(request: request) { (result: Result<(Data?, HTTPURLResponse), Error>) in processResult(completion: completion) { let (_, response) = try result.get() + + guard (200...299).contains(response.statusCode) else { + throw TUSAPIError.failedRequest(response) + } guard let location = response.allHeaderFields[caseInsensitive: "location"] as? String, let locationURL = URL(string: location, relativeTo: metaData.uploadURL) else { @@ -104,6 +113,11 @@ final class TUSAPI { if let mimeType = metaData.mimeType, !mimeType.isEmpty { metaDataDict["filetype"] = mimeType } + + if let context = metaData.context { + metaDataDict = metaDataDict.merging(context) { _, new in new } + } + return metaDataDict } @@ -114,7 +128,7 @@ final class TUSAPI { for (key, value) in dict { let appendingStr: String if !str.isEmpty { - str += ", " + str += "," } appendingStr = "\(key) \(value.toBase64())" str = str + appendingStr @@ -169,6 +183,10 @@ final class TUSAPI { processResult(completion: completion) { let (_, response) = try result.get() + guard (200...299).contains(response.statusCode) else { + throw TUSAPIError.failedRequest(response) + } + guard let offsetStr = response.allHeaderFields[caseInsensitive: "upload-offset"] as? String, let offset = Int(offsetStr) else { throw TUSAPIError.couldNotRetrieveOffset diff --git a/Sources/TUSKit/TUSBackground.swift b/Sources/TUSKit/TUSBackground.swift index 1d11c199..1167df69 100644 --- a/Sources/TUSKit/TUSBackground.swift +++ b/Sources/TUSKit/TUSBackground.swift @@ -9,7 +9,6 @@ import Foundation import BackgroundTasks #if os(iOS) -@available(iOS 13.0, *) /// Perform background uploading final class TUSBackground { diff --git a/Sources/TUSKit/TUSClient.swift b/Sources/TUSKit/TUSClient.swift index 53e4659d..17df5b5e 100644 --- a/Sources/TUSKit/TUSClient.swift +++ b/Sources/TUSKit/TUSClient.swift @@ -28,22 +28,25 @@ 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, *) func totalProgress(bytesUploaded: Int, totalBytes: Int, client: TUSClient) - @available(iOS 11.0, macOS 10.13, *) /// 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) + + func didRetryUpload(id: UUID, error: Error, attempt: Int, context: [String: String]?, client: TUSClient) } public extension TUSClientDelegate { func progressFor(id: UUID, context: [String: String]?, progress: Float, client: TUSClient) { // Optional } + + func didRetryUpload(id: UUID, error: Error, attempt: Int, context: [String: String]?, client: TUSClient) { + // Optional + } } protocol ProgressDelegate: AnyObject { - @available(iOS 11.0, macOS 10.13, *) func progressUpdatedFor(metaData: UploadMetadata, totalUploadedBytes: Int) } @@ -63,8 +66,12 @@ public final class TUSClient { // MARK: - Private Properties + public var retryDelays: [TimeInterval] = [0, 0] + /// How often to try an upload if it fails. A retryCount of 2 means 3 total uploads max. (1 initial upload, and on repeated failure, 2 more retries.) - private let retryCount = 2 + private var retryCount: Int { + retryDelays.count + } private let files: Files private var didStopAndCancel = false @@ -76,10 +83,18 @@ public final class TUSClient { private var uploads = [UUID: UploadMetadata]() #if os(iOS) + private var _backgroundClient: Any? + @available(iOS 13.0, *) - private lazy var backgroundClient: TUSBackground = { - return TUSBackground(api: api, files: files, chunkSize: chunkSize) - }() + /// Lazy properties are considered as stored properties in Swift 5.7, so they can no longer be marked as unavailable. Hence + /// the computed property backed by storage var. + private var backgroundClient: TUSBackground? { + if _backgroundClient == nil { + _backgroundClient = TUSBackground(api: api, files: files, chunkSize: chunkSize) + } + + return _backgroundClient as? TUSBackground + } #endif /// Initialize a TUSClient @@ -147,17 +162,23 @@ public final class TUSClient { /// If data can not be found at a location, it will attempt to locate the data by prefixing the path with file:// /// - Parameters: /// - filePath: The path to a file on a local filesystem - /// - uploadURL: A custom URL to upload to. For if you don't want to use the default server url from the config. Will call the `create` on this custom url to get the definitive upload url. + /// - uploadURL: A custom URL to upload to. For if you don't want to use the default server url from the config. Will + /// call the `create` on this custom url to get the definitive upload url. /// - customHeaders: Any headers you want to add to an upload - /// - context: Add a custom context when uploading files that you will receive back in a later stage. Useful for custom metadata you want to associate with the upload. Don't put sensitive information in here! Since a context will be stored to the disk. - /// - Returns: ANn id + /// - context: Custom metadata you want to associate with the upload. The data will be stored to the disk and is included as key-value pair + /// in the `Upload-Metadata` HTTP header for in the creation request. The keys must not be empty and must not include spaces or commas. + /// - Returns: An identifier. /// - Throws: TUSClientError @discardableResult public func uploadFileAt(filePath: URL, uploadURL: URL? = nil, customHeaders: [String: String] = [:], context: [String: String]? = nil) throws -> UUID { didStopAndCancel = false do { let id = UUID() + #if os(macOS) + let destinationFilePath = filePath + #elseif os(iOS) let destinationFilePath = try files.copy(from: filePath, id: id) + #endif try scheduleTask(for: destinationFilePath, id: id, uploadURL: uploadURL, customHeaders: customHeaders, context: context) return id } catch let error as TUSClientError { @@ -173,8 +194,9 @@ public final class TUSClient { /// - preferredFileExtension: A file extension to add when saving the file. E.g. You can add ".JPG" to raw data that's being saved. This will help the uploader's metadata. /// - uploadURL: A custom URL to upload to. For if you don't want to use the default server url. Will call the `create` on this custom url to get the definitive upload url. /// - customHeaders: The headers to upload. - /// - context: Add a custom context when uploading files that you will receive back in a later stage. Useful for custom metadata you want to associate with the upload. Don't put sensitive information in here! Since a context will be stored to the disk. - /// - Returns: An id + /// - context: Custom metadata you want to associate with the upload. The data will be stored to the disk and is included as key-value pair + /// in the `Upload-Metadata` HTTP header for in the creation request. The keys must not be empty and must not include spaces or commas. + /// - Returns: An identifier. /// - Throws: TUSClientError @discardableResult public func upload(data: Data, preferredFileExtension: String? = nil, uploadURL: URL? = nil, customHeaders: [String: String] = [:], context: [String: String]? = nil) throws -> UUID { @@ -267,7 +289,7 @@ public final class TUSClient { @discardableResult public func retry(id: UUID) throws -> Bool { do { - guard uploads[id] == nil else { return false } + guard uploads[id] != nil else { return false } guard let metaData = try files.findMetadata(id: id) else { return false } @@ -287,9 +309,8 @@ public final class TUSClient { /// This will signal the OS to upload files when appropriate (e.g. when a phone is on a charger and on Wifi). /// Note that the OS decides when uploading begins. #if os(iOS) - @available(iOS 13.0, *) public func scheduleBackgroundTasks() { - backgroundClient.scheduleBackgroundTasks() + //backgroundClient?.scheduleBackgroundTasks() } #endif @@ -305,6 +326,15 @@ public final class TUSClient { } } + /// Return the all the stored uploads. Good to check after launch or after background processing for example, to handle them at a later stage. + /// - Returns: An UploadInfo array of all the stored uploads. + public func getStoredUploads() throws -> [UploadInfo] { + try files.loadAllMetadata().compactMap { metaData in + return UploadInfo(id: metaData.id, uploadURL: metaData.uploadURL, filePath: metaData.filePath, remoteDestination: metaData.remoteDestination, context: metaData.context, uploadedRange: metaData.uploadedRange, mimeType: metaData.mimeType, customHeaders: metaData.customHeaders, size: metaData.size) + } + } + + // MARK: - Private /// Check for any uploads that are finished and remove them from the cache. @@ -500,6 +530,7 @@ extension TUSClient: SchedulerDelegate { let canRetry = metaData.errorCount <= retryCount if canRetry { + delegate?.didRetryUpload(id: metaData.id, error: error, attempt: metaData.errorCount, context: metaData.context, client: self) scheduler.addTask(task: task) } else { // Exhausted all retries, reporting back as failure. uploads[metaData.id] = nil @@ -541,7 +572,6 @@ func taskFor(metaData: UploadMetadata, api: TUSAPI, files: Files, chunkSize: Int extension TUSClient: ProgressDelegate { - @available(iOS 11.0, macOS 10.13, *) func progressUpdatedFor(metaData: UploadMetadata, totalUploadedBytes: Int) { delegate?.progressFor(id: metaData.id, context: metaData.context, bytesUploaded: totalUploadedBytes, totalBytes: metaData.size, client: self) diff --git a/Sources/TUSKit/TUSClientError.swift b/Sources/TUSKit/TUSClientError.swift index 2745dffb..df662106 100644 --- a/Sources/TUSKit/TUSClientError.swift +++ b/Sources/TUSKit/TUSClientError.swift @@ -9,7 +9,7 @@ public enum TUSClientError: Error { case couldNotLoadData(underlyingError: Error) case couldNotStoreFileMetadata(underlyingError: Error) case couldNotCreateFileOnServer - case couldNotUploadFile + case couldNotUploadFile(underlyingError: Error) case couldNotGetFileStatus case fileSizeMismatchWithServer case couldNotDeleteFile(underlyingError: Error) @@ -18,4 +18,6 @@ public enum TUSClientError: Error { case couldnotRemoveFinishedUploads(underlyingError: Error) case receivedUnexpectedOffset case missingRemoteDestination + case emptyUploadRange + case rangeLargerThanFile } diff --git a/Sources/TUSKit/Tasks/UploadDataTask.swift b/Sources/TUSKit/Tasks/UploadDataTask.swift index d5097a46..2389229f 100644 --- a/Sources/TUSKit/Tasks/UploadDataTask.swift +++ b/Sources/TUSKit/Tasks/UploadDataTask.swift @@ -44,19 +44,19 @@ final class UploadDataTask: NSObject, IdentifiableTask { if let range = range, range.count == 0 { // Improve: Enrich error assertionFailure("Ended up with an empty range to upload.") - throw TUSClientError.couldNotUploadFile + throw TUSClientError.couldNotUploadFile(underlyingError: TUSClientError.emptyUploadRange) } if (range?.count ?? 0) > metaData.size { assertionFailure("The range \(String(describing: range?.count)) to upload is larger than the size \(metaData.size)") - throw TUSClientError.couldNotUploadFile + throw TUSClientError.couldNotUploadFile(underlyingError: TUSClientError.rangeLargerThanFile) } if let destination = metaData.remoteDestination { self.metaData.remoteDestination = destination } else { assertionFailure("No remote destination for upload task") - throw TUSClientError.couldNotUploadFile + throw TUSClientError.couldNotUploadFile(underlyingError: TUSClientError.missingRemoteDestination) } self.range = range } @@ -91,6 +91,8 @@ final class UploadDataTask: NSObject, IdentifiableTask { } let task = api.upload(data: dataToUpload, range: range, location: remoteDestination, metaData: self.metaData) { [weak self] result in + self?.observation?.invalidate() + self?.queue.async { guard let self = self else { return } // Getting rid of needing .self inside this closure @@ -138,7 +140,7 @@ final class UploadDataTask: NSObject, IdentifiableTask { } catch let error as TUSClientError { completed(.failure(error)) } catch { - completed(.failure(TUSClientError.couldNotUploadFile)) + completed(.failure(TUSClientError.couldNotUploadFile(underlyingError: error))) } } @@ -146,12 +148,9 @@ final class UploadDataTask: NSObject, IdentifiableTask { sessionTask = task - if #available(iOS 11.0, macOS 10.13, *) { - observeTask(task: task, size: dataToUpload.count) - } + observeTask(task: task, size: dataToUpload.count) } - @available(iOS 11.0, macOS 10.13, *) func observeTask(task: URLSessionUploadTask, size: Int) { let targetRange = self.range ?? 0..? + public var mimeType: String? + public var customHeaders: [String: String]? + public let size: Int + + init(id: UUID, uploadURL: URL, filePath: URL, remoteDestination: URL? = nil, context: [String : String]? = nil, uploadedRange: Range? = nil, mimeType: String? = nil, customHeaders: [String : String]? = nil, size: Int) { + self.id = id + self.uploadURL = uploadURL + self.filePath = filePath + self.remoteDestination = remoteDestination + self.context = context + self.uploadedRange = uploadedRange + self.mimeType = mimeType + self.customHeaders = customHeaders + self.size = size + } + +} diff --git a/TUSKit.podspec b/TUSKit.podspec index 84e05dab..ba80acba 100644 --- a/TUSKit.podspec +++ b/TUSKit.podspec @@ -7,7 +7,7 @@ Pod::Spec.new do |s| s.name = 'TUSKit' - s.version = '3.1.3' + s.version = '3.1.7' s.summary = 'TUSKit client in Swift' s.swift_version = '5.0' @@ -24,7 +24,7 @@ Swift client for https://tus.io called TUSKit. Mac and iOS compatible. s.homepage = 'https://github.com/tus/tus-ios-client' s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'Tjeerd in t Veen' => 'tjeerd@twinapps.co' } + s.author = 'Transloadit' s.source = { :git => 'https://github.com/tus/tus-ios-client.git', :tag => s.version.to_s } s.platform = :ios diff --git a/TUSKitExample/TUSKitExample.xcodeproj/project.pbxproj b/TUSKitExample/TUSKitExample.xcodeproj/project.pbxproj index c1c79e97..6fcfaa59 100644 --- a/TUSKitExample/TUSKitExample.xcodeproj/project.pbxproj +++ b/TUSKitExample/TUSKitExample.xcodeproj/project.pbxproj @@ -17,6 +17,10 @@ 2EB9149F26F09D7800548562 /* TUSKitExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB9149E26F09D7800548562 /* TUSKitExampleUITests.swift */; }; 2EB914B826F0A2CE00548562 /* TUSKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2EB914B726F0A2CE00548562 /* TUSKit */; }; 2EB914BA26F0B11C00548562 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB914B926F0B11C00548562 /* PhotoPicker.swift */; }; + 978A729D29ACE487002A9440 /* UploadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978A729C29ACE487002A9440 /* UploadsView.swift */; }; + 978A729F29ACE4D2002A9440 /* FilePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978A729E29ACE4D2002A9440 /* FilePickerView.swift */; }; + 978A72A229ACE5F7002A9440 /* TUSWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978A72A129ACE5F7002A9440 /* TUSWrapper.swift */; }; + 97B438502987C770001C802F /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B4384F2987C770001C802F /* DocumentPicker.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -53,6 +57,10 @@ 2EB914A026F09D7800548562 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2EB914B626F0A29E00548562 /* TUSKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = TUSKit; path = ..; sourceTree = ""; }; 2EB914B926F0B11C00548562 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; + 978A729C29ACE487002A9440 /* UploadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadsView.swift; sourceTree = ""; }; + 978A729E29ACE4D2002A9440 /* FilePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePickerView.swift; sourceTree = ""; }; + 978A72A129ACE5F7002A9440 /* TUSWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUSWrapper.swift; sourceTree = ""; }; + 97B4384F2987C770001C802F /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -106,10 +114,10 @@ 2EB9147B26F09D7600548562 /* TUSKitExample */ = { isa = PBXGroup; children = ( + 978A72A029ACE5DB002A9440 /* Helpers */, + 978A729B29ACE471002A9440 /* Screens */, 2EB9147C26F09D7600548562 /* AppDelegate.swift */, - 2EB914B926F0B11C00548562 /* PhotoPicker.swift */, 2EB9147E26F09D7600548562 /* SceneDelegate.swift */, - 2EB9148026F09D7600548562 /* ContentView.swift */, 2EB9148226F09D7800548562 /* Assets.xcassets */, 2EB9148726F09D7800548562 /* LaunchScreen.storyboard */, 2EB9148A26F09D7800548562 /* Info.plist */, @@ -151,6 +159,26 @@ name = Frameworks; sourceTree = ""; }; + 978A729B29ACE471002A9440 /* Screens */ = { + isa = PBXGroup; + children = ( + 2EB9148026F09D7600548562 /* ContentView.swift */, + 978A729C29ACE487002A9440 /* UploadsView.swift */, + 978A729E29ACE4D2002A9440 /* FilePickerView.swift */, + ); + path = Screens; + sourceTree = ""; + }; + 978A72A029ACE5DB002A9440 /* Helpers */ = { + isa = PBXGroup; + children = ( + 2EB914B926F0B11C00548562 /* PhotoPicker.swift */, + 97B4384F2987C770001C802F /* DocumentPicker.swift */, + 978A72A129ACE5F7002A9440 /* TUSWrapper.swift */, + ); + path = Helpers; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -286,9 +314,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 978A72A229ACE5F7002A9440 /* TUSWrapper.swift in Sources */, 2EB914BA26F0B11C00548562 /* PhotoPicker.swift in Sources */, 2EB9147D26F09D7600548562 /* AppDelegate.swift in Sources */, 2EB9147F26F09D7600548562 /* SceneDelegate.swift in Sources */, + 978A729D29ACE487002A9440 /* UploadsView.swift in Sources */, + 978A729F29ACE4D2002A9440 /* FilePickerView.swift in Sources */, + 97B438502987C770001C802F /* DocumentPicker.swift in Sources */, 2EB9148126F09D7600548562 /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -459,6 +491,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"TUSKitExample/Preview Content\""; + DEVELOPMENT_TEAM = 4JMM8JMG3H; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = TUSKitExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; @@ -480,6 +513,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"TUSKitExample/Preview Content\""; + DEVELOPMENT_TEAM = 4JMM8JMG3H; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = TUSKitExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; diff --git a/TUSKitExample/TUSKitExample/ContentView.swift b/TUSKitExample/TUSKitExample/ContentView.swift deleted file mode 100644 index deb9d437..00000000 --- a/TUSKitExample/TUSKitExample/ContentView.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ContentView.swift -// TUSKitExample -// -// Created by Tjeerd in ‘t Veen on 14/09/2021. -// - -import SwiftUI -import TUSKit -import PhotosUI - -struct ContentView: View { - - let photoPicker: PhotoPicker - - @State private var showingImagePicker = false - - init(photoPicker: PhotoPicker) { - self.photoPicker = photoPicker - } - - var body: some View { - VStack { - Text("TUSKit Demo") - .font(.title) - .padding() - - Button("Select image") { - showingImagePicker.toggle() - }.sheet(isPresented:$showingImagePicker, content: { - self.photoPicker - }) - } - } -} - -struct ContentView_Previews: PreviewProvider { - @State static var isPresented = false - static let tusClient = try! TUSClient(server: URL(string: "https://tusd.tusdemo.net/files")!, sessionIdentifier: "TUSClient", storageDirectory: URL(string: "TUS")!) - static var previews: some View { - let photoPicker = PhotoPicker(tusClient: tusClient) - ContentView(photoPicker: photoPicker) - } -} diff --git a/TUSKitExample/TUSKitExample/Helpers/DocumentPicker.swift b/TUSKitExample/TUSKitExample/Helpers/DocumentPicker.swift new file mode 100644 index 00000000..98770074 --- /dev/null +++ b/TUSKitExample/TUSKitExample/Helpers/DocumentPicker.swift @@ -0,0 +1,81 @@ +// +// DocumentPicker.swift +// TUSKitExample +// +// Created by Donny Wals on 30/01/2023. +// + +import Foundation +import TUSKit +import UIKit +import SwiftUI + +struct DocumentPicker: UIViewControllerRepresentable { + + @Environment(\.presentationMode) var presentationMode + + let tusClient: TUSClient + + init(tusClient: TUSClient) { + self.tusClient = tusClient + } + + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { + let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.data, .image, .pdf]) + picker.allowsMultipleSelection = true + picker.shouldShowFileExtensions = true + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { } + + func makeCoordinator() -> Coordinator { + Coordinator(self, tusClient: tusClient) + } + + // Use a Coordinator to act as your PHPickerViewControllerDelegate + class Coordinator: NSObject, UIDocumentPickerDelegate { + + private let parent: DocumentPicker + private let tusClient: TUSClient + + init(_ parent: DocumentPicker, tusClient: TUSClient) { + self.parent = parent + self.tusClient = tusClient + + super.init() + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + var files = [Data]() + for url in urls { + guard url.startAccessingSecurityScopedResource() else { + continue + } + + defer { + url.stopAccessingSecurityScopedResource() + } + + do { + let data = try Data(contentsOf: url) + files.append(data) + } catch { + print(error) + } + } + + do { + try self.tusClient.uploadMultiple(dataFiles: files) + tusClient.scheduleBackgroundTasks() + } catch { + print(error) + } + + parent.presentationMode.wrappedValue.dismiss() + } + + } +} + diff --git a/TUSKitExample/TUSKitExample/PhotoPicker.swift b/TUSKitExample/TUSKitExample/Helpers/PhotoPicker.swift similarity index 86% rename from TUSKitExample/TUSKitExample/PhotoPicker.swift rename to TUSKitExample/TUSKitExample/Helpers/PhotoPicker.swift index 9d9d7e37..256231a6 100644 --- a/TUSKitExample/TUSKitExample/PhotoPicker.swift +++ b/TUSKitExample/TUSKitExample/Helpers/PhotoPicker.swift @@ -50,8 +50,12 @@ struct PhotoPicker: UIViewControllerRepresentable { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { var images = [Data]() - for result in results { + results.forEach { result in + let semaphore = DispatchSemaphore(value: 0) result.itemProvider.loadObject(ofClass: UIImage.self, completionHandler: { [weak self] (object, error) in + defer { + semaphore.signal() + } guard let self = self else { return } if let image = object as? UIImage { @@ -71,10 +75,15 @@ struct PhotoPicker: UIViewControllerRepresentable { } } else { - print(object) - print(error) + if let object { + print(object) + } + if let error { + print(error) + } } }) + semaphore.wait() } parent.presentationMode.wrappedValue.dismiss() } diff --git a/TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift b/TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift new file mode 100644 index 00000000..e46841cb --- /dev/null +++ b/TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift @@ -0,0 +1,85 @@ +// +// TUSWrapper.swift +// TUSKitExample +// +// Created by Donny Wals on 27/02/2023. +// + +import Foundation +import TUSKit + +enum UploadStatus { + case paused(bytesUploaded: Int, totalBytes: Int) + case uploading(bytesUploaded: Int, totalBytes: Int) + case failed(error: Error) + case uploaded(url: URL) +} + +class TUSWrapper: TUSClientDelegate, ObservableObject { + let client: TUSClient + + @MainActor + @Published private(set) var uploads: [UUID: UploadStatus] = [:] + + init(client: TUSClient) { + self.client = client + client.delegate = self + } + + @MainActor + func pauseUpload(id: UUID) { + try? client.cancel(id: id) + + if case let .uploading(bytesUploaded, totalBytes) = uploads[id] { + uploads[id] = .paused(bytesUploaded: bytesUploaded, totalBytes: totalBytes) + } + } + + @MainActor + func resumeUpload(id: UUID) { + _ = try? client.retry(id: id) + + if case let .paused(bytesUploaded, totalBytes) = uploads[id] { + uploads[id] = .uploading(bytesUploaded: bytesUploaded, totalBytes: totalBytes) + } + } + + @MainActor + func clearUpload(id: UUID) { + _ = try? client.cancel(id: id) + _ = try? client.removeCacheFor(id: id) + uploads[id] = nil + } + + func progressFor(id: UUID, context: [String: String]?, bytesUploaded: Int, totalBytes: Int, client: TUSClient) { + Task { @MainActor in + uploads[id] = .uploading(bytesUploaded: bytesUploaded, totalBytes: totalBytes) + } + } + + func didStartUpload(id: UUID, context: [String : String]?, client: TUSClient) { + Task { @MainActor in + uploads[id] = .uploading(bytesUploaded: 0, totalBytes: Int.max) + } + } + + func didFinishUpload(id: UUID, url: URL, context: [String : String]?, client: TUSClient) { + Task { @MainActor in + uploads[id] = .uploaded(url: url) + } + } + + func uploadFailed(id: UUID, error: Error, context: [String : String]?, client: TUSClient) { + Task { @MainActor in + uploads[id] = .failed(error: error) + + if case TUSClientError.couldNotUploadFile(underlyingError: let underlyingError) = error, + case TUSAPIError.failedRequest(let response) = underlyingError { + print("upload failed with response \(response)") + } + } + } + + func fileError(error: TUSClientError, client: TUSClient) { } + func totalProgress(bytesUploaded: Int, totalBytes: Int, client: TUSClient) { } +} diff --git a/TUSKitExample/TUSKitExample/SceneDelegate.swift b/TUSKitExample/TUSKitExample/SceneDelegate.swift index b4f6873d..e6a5d9e7 100644 --- a/TUSKitExample/TUSKitExample/SceneDelegate.swift +++ b/TUSKitExample/TUSKitExample/SceneDelegate.swift @@ -14,14 +14,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var tusClient: TUSClient! + var wrapper: TUSWrapper! @State var isPresented = false func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { do { - tusClient = try TUSClient(server: URL(string: "https://tusd.tusdemo.net/files")!, sessionIdentifier: "TUS DEMO", storageDirectory: URL(string: "/TUS")!) - tusClient.delegate = self + tusClient = try TUSClient(server: URL(string: "https://tusd.tusdemo.net/files")!, sessionIdentifier: "TUS DEMO", storageDirectory: URL(string: "/TUS")!, chunkSize: 100 * 1024 * 1024) + wrapper = TUSWrapper(client: tusClient) let remainingUploads = tusClient.start() switch remainingUploads.count { case 0: @@ -41,13 +42,21 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // ...alternatively, you can delete them too // tusClient.removeCacheFor(id: id) } + + + /* + + // You can get stored uploads with tusClient.getStoredUploads() + let storedUploads = try tusClient.getStoredUploads() + for storedUpload in storedUploads { + print("\(storedUpload) Stored upload") + } + */ } catch { assertionFailure("Could not fetch failed id's from disk, or could not instantiate TUSClient \(error)") } - let photoPicker = PhotoPicker(tusClient: tusClient) - - let contentView = ContentView(photoPicker: photoPicker) + let contentView = ContentView(tusWrapper: wrapper) // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { @@ -60,66 +69,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // We can already trigger background tasks. Once the background-scheduler runs, the tasks will upload. tusClient.scheduleBackgroundTasks() } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - -} - -extension SceneDelegate: TUSClientDelegate { - - func totalProgress(bytesUploaded: Int, totalBytes: Int, client: TUSClient) { - print("TUSClient total upload progress: \(bytesUploaded) of \(totalBytes) bytes.") - } - - func progressFor(id: UUID, context: [String: String]?, bytesUploaded: Int, totalBytes: Int, client: TUSClient) { - print("TUSClient single upload progress: \(bytesUploaded) / \(totalBytes)") - } - - 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, 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, context: [String : String]?, client: TUSClient) { - print("TUSClient upload failed for \(id) error \(error)") - } - - func fileError(error: TUSClientError, client: TUSClient) { - print("TUSClient File error \(error)") - } - } diff --git a/TUSKitExample/TUSKitExample/Screens/ContentView.swift b/TUSKitExample/TUSKitExample/Screens/ContentView.swift new file mode 100644 index 00000000..c8444291 --- /dev/null +++ b/TUSKitExample/TUSKitExample/Screens/ContentView.swift @@ -0,0 +1,47 @@ +// +// ContentView.swift +// TUSKitExample +// +// Created by Tjeerd in ‘t Veen on 14/09/2021. +// + +import SwiftUI +import TUSKit +import PhotosUI + +struct ContentView: View { + let tusWrapper: TUSWrapper + + var body: some View { + TabView { + FilePickerView( + photoPicker: PhotoPicker(tusClient: tusWrapper.client), + filePicker: DocumentPicker(tusClient: tusWrapper.client) + ) + .tabItem { + VStack { + Image(systemName: "square.and.arrow.up") + Text("Upload files") + } + } + + UploadsView( + tusWrapper: tusWrapper + ) + .tabItem { + VStack { + Image(systemName: "list.bullet") + Text("Uploads") + } + } + } + } +} + +struct ContentView_Previews: PreviewProvider { + @State static var isPresented = false + static let tusClient = try! TUSClient(server: URL(string: "https://tusd.tusdemo.net/files")!, sessionIdentifier: "TUSClient", storageDirectory: URL(string: "TUS")!) + static var previews: some View { + ContentView(tusWrapper: TUSWrapper(client: tusClient)) + } +} diff --git a/TUSKitExample/TUSKitExample/Screens/FilePickerView.swift b/TUSKitExample/TUSKitExample/Screens/FilePickerView.swift new file mode 100644 index 00000000..271fa364 --- /dev/null +++ b/TUSKitExample/TUSKitExample/Screens/FilePickerView.swift @@ -0,0 +1,43 @@ +// +// FilePickerView.swift +// TUSKitExample +// +// Created by Donny Wals on 27/02/2023. +// + +import Foundation +import SwiftUI +import TUSKit + +struct FilePickerView: View { + let photoPicker: PhotoPicker + let filePicker: DocumentPicker + + @State private var showingImagePicker = false + @State private var showingFilePicker = false + + init(photoPicker: PhotoPicker, filePicker: DocumentPicker) { + self.photoPicker = photoPicker + self.filePicker = filePicker + } + + var body: some View { + VStack(spacing: 8) { + Text("TUSKit Demo") + .font(.title) + .padding() + + Button("Select image") { + showingImagePicker.toggle() + }.sheet(isPresented: $showingImagePicker, content: { + self.photoPicker + }) + + Button("Select file") { + showingFilePicker.toggle() + }.sheet(isPresented: $showingFilePicker, content: { + self.filePicker + }) + } + } +} diff --git a/TUSKitExample/TUSKitExample/Screens/UploadsView.swift b/TUSKitExample/TUSKitExample/Screens/UploadsView.swift new file mode 100644 index 00000000..345bc326 --- /dev/null +++ b/TUSKitExample/TUSKitExample/Screens/UploadsView.swift @@ -0,0 +1,79 @@ +// +// UploadsView.swift +// TUSKitExample +// +// Created by Donny Wals on 27/02/2023. +// + +import Foundation +import SwiftUI +import TUSKit + +struct UploadsView: View { + @ObservedObject var tusWrapper: TUSWrapper + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + ForEach(Array(tusWrapper.uploads), id: \.key) { idx in + switch idx.value { + case .uploading(let bytesUploaded, let totalBytes): + HStack(spacing: 8) { + Button(action: { + tusWrapper.pauseUpload(id: idx.key) + }, label: { + Image(systemName: "playpause.fill") + }) + + Button(action: { + tusWrapper.clearUpload(id: idx.key) + }, label: { + Image(systemName: "trash.fill") + }) + + Text("Item \(idx.key) uploading - \(bytesUploaded) / \(totalBytes)") + + Spacer() + } + case .paused(let bytesUploaded, let totalBytes): + HStack(spacing: 8) { + Button(action: { + tusWrapper.resumeUpload(id: idx.key) + }, label: { + Image(systemName: "playpause.fill") + }) + + Button(action: { + tusWrapper.clearUpload(id: idx.key) + }, label: { + Image(systemName: "trash.fill") + }) + + Text("Item \(idx.key) paused - \(bytesUploaded) / \(totalBytes)") + + Spacer() + } + case .uploaded(let url): + HStack { + Text("Item \(idx.key) - Has been uploaded") + + Spacer() + } + case .failed(let error): + HStack(spacing: 8) { + Button(action: { + tusWrapper.clearUpload(id: idx.key) + }, label: { + Image(systemName: "trash.fill") + }) + + Text("Item \(idx.key) failed") + + Spacer() + } + } + } + }.padding([.leading, .trailing]) + } + } +} diff --git a/Tests/TUSKitTests/TUSClient/TUSClientTests.swift b/Tests/TUSKitTests/TUSClient/TUSClientTests.swift index 21c9e093..ad646962 100644 --- a/Tests/TUSKitTests/TUSClient/TUSClientTests.swift +++ b/Tests/TUSKitTests/TUSClient/TUSClientTests.swift @@ -90,6 +90,14 @@ final class TUSClientTests: XCTestCase { XCTAssertEqual(0, client.remainingUploads) } + func testgetStoredUploads() throws { + let taskIDtoCancel = try client.upload(data: data) + try client.cancel(id: taskIDtoCancel) + let storedUploads = try client.getStoredUploads() + + XCTAssert(storedUploads.contains(where: { $0.id == taskIDtoCancel })) + } + // MARK: - Supported Extensions func testClientExcludesCreationStep() throws { diff --git a/Tests/TUSKitTests/TUSClient/TUSClient_ContextTests.swift b/Tests/TUSKitTests/TUSClient/TUSClient_ContextTests.swift index 31301839..a03b3940 100644 --- a/Tests/TUSKitTests/TUSClient/TUSClient_ContextTests.swift +++ b/Tests/TUSKitTests/TUSClient/TUSClient_ContextTests.swift @@ -103,6 +103,41 @@ final class TUSClient_ContextTests: XCTestCase { XCTAssert(tusDelegate.receivedContexts.contains(expectedContext)) } + func testContextIsIncludedInUploadMetadata() throws { + let key = "SomeKey" + let value = "SomeValue" + let context = [key: value] + + // Store file + let documentDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + func storeFileInDocumentsDir() throws -> URL { + let targetLocation = documentDir.appendingPathComponent("myfile.txt") + try data.write(to: targetLocation) + return targetLocation + } + + let location = try storeFileInDocumentsDir() + + let startedExpectation = expectation(description: "Waiting for uploads to start") + tusDelegate.startUploadExpectation = startedExpectation + + try client.uploadFileAt(filePath: location, context: context) + wait(for: [startedExpectation], timeout: 5) + + // Validate + let createRequests = MockURLProtocol.receivedRequests.filter { $0.httpMethod == "POST" } + + for request in createRequests { + let headers = try XCTUnwrap(request.allHTTPHeaderFields) + let metadata = try XCTUnwrap(headers["Upload-Metadata"]) + .components(separatedBy: CharacterSet([" ", ","])) + .filter { !$0.isEmpty } + + XCTAssert(metadata.contains(key)) + XCTAssert(metadata.contains(value.toBase64())) + } + } + // MARK: - Private helper methods for uploading private func waitForUploadsToFinish(_ amount: Int = 1) { diff --git a/Tests/TUSKitTests/TUSClient/TUSClient_CustomHeadersTests.swift b/Tests/TUSKitTests/TUSClient/TUSClient_CustomHeadersTests.swift index ed47ffa6..3de81c13 100644 --- a/Tests/TUSKitTests/TUSClient/TUSClient_CustomHeadersTests.swift +++ b/Tests/TUSKitTests/TUSClient/TUSClient_CustomHeadersTests.swift @@ -73,39 +73,6 @@ final class TUSClient_CustomHeadersTests: XCTestCase { XCTAssert(headers[key] == value, "Expected custom header '\(key)' to exist on headers with value: '\(value)'") } } - - // TODO: Decide if we want to keep this. -// func testUploadingWithCustomHeadersForData() throws { -// // Make sure client adds custom headers -// -// let createRequestsFirst = MockURLProtocol.receivedRequests.filter { $0.httpMethod == "POST" } -// XCTAssert(createRequestsFirst.isEmpty) -// -// // Expected values -// let key = "TUSKit" -// let value = "TransloaditKit" -// let customHeaders = [key: value] -// -// let finishedExpectation = expectation(description: "Waiting for uploads to start") -// finishedExpectation.expectedFulfillmentCount = 2 -// tusDelegate.finishUploadExpectation = finishedExpectation -// -// try client.uploadMultiple(dataFiles: [data, data], customHeaders: customHeaders) -// wait(for: [finishedExpectation], timeout: 5) -// -// // Validate -// let createRequests = MockURLProtocol.receivedRequests.filter { $0.httpMethod == "POST" } -// XCTAssertFalse(createRequests.isEmpty) -// -// for request in createRequests { -// let headers = try XCTUnwrap(request.allHTTPHeaderFields) -// let metaDataString = try XCTUnwrap(headers["Upload-Metadata"]) -// for (key, value) in customHeaders { -// XCTAssert(metaDataString.contains(key), "Expected \(metaDataString) to contain \(key)") -// XCTAssert(metaDataString.contains(value.toBase64()), "Expected \(metaDataString) to contain base 64 value for \(value)") -// } -// } -// } } diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 00000000..ab8fc9fe --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,10 @@ +# TUSKit Release checklist + +* Update [CHANGELOG.md](http://CHANGELOG.md) +* Update TUSKit.podspec with new version nr. +* Make a commit +* Tag update commit +* Make sure to push commits _and_ tag +* Publish updated podspec `pod trunk push TUSKit.podspec` + * If you're doing this for the first time, register with `pod trunk register ‘’ ‘’` + * If you don't have access but are supposed to have this access, reach out to @kvz \ No newline at end of file