From 47552efa225004514e02c3ea737fbe9965747075 Mon Sep 17 00:00:00 2001 From: ThibaultBee Date: Tue, 21 Nov 2023 10:19:10 +0000 Subject: [PATCH] feat(swift5): use Basic auth instead of Bearer --- .openapi-generator/FILES | 4 +- ApiVideoClient.podspec | 7 +- Cartfile | 1 - .../Controller/VideosViewController.swift | 85 +++ Example/Example/MainViewController.swift | 2 +- Package.swift | 9 +- Sources/APIs.swift | 44 +- Sources/AlamofireImplementations.swift | 418 ----------- Sources/Auth/ApiVideoAuthenticator.swift | 52 -- Sources/Auth/ApiVideoCredential.swift | 31 - Sources/Extensions.swift | 6 + Sources/Models.swift | 28 +- Sources/URLSessionImplementations.swift | 691 ++++++++++++++++++ Sources/Upload/FileChunkInputStream.swift | 3 - .../ProgressiveUploadSessionProtocol.swift | 3 - Sources/Upload/RequestTaskQueue.swift | 34 +- .../Upload/UploadChunkRequestTaskQueue.swift | 19 +- .../Integration/VideosApiTests.swift | 10 +- project.yml | 1 - 19 files changed, 862 insertions(+), 586 deletions(-) create mode 100644 Example/Example/Controller/VideosViewController.swift delete mode 100644 Sources/AlamofireImplementations.swift delete mode 100644 Sources/Auth/ApiVideoAuthenticator.swift delete mode 100644 Sources/Auth/ApiVideoCredential.swift create mode 100644 Sources/URLSessionImplementations.swift diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index ba85e5b..f880906 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -15,9 +15,6 @@ Sources/APIs/UploadTokensAPI.swift Sources/APIs/VideosAPI.swift Sources/APIs/WatermarksAPI.swift Sources/APIs/WebhooksAPI.swift -Sources/AlamofireImplementations.swift -Sources/Auth/ApiVideoAuthenticator.swift -Sources/Auth/ApiVideoCredential.swift Sources/CodableHelper.swift Sources/Configuration.swift Sources/Extensions.swift @@ -99,6 +96,7 @@ Sources/Models/WebhooksCreationPayload.swift Sources/Models/WebhooksListResponse.swift Sources/OpenISO8601DateFormatter.swift Sources/SynchronizedDictionary.swift +Sources/URLSessionImplementations.swift Sources/Upload/FileChunkInputStream.swift Sources/Upload/ProgressiveUploadSessionProtocol.swift Sources/Upload/RequestTaskQueue.swift diff --git a/ApiVideoClient.podspec b/ApiVideoClient.podspec index 244a0d6..07848e3 100644 --- a/ApiVideoClient.podspec +++ b/ApiVideoClient.podspec @@ -1,8 +1,8 @@ Pod::Spec.new do |s| s.name = 'ApiVideoClient' - s.ios.deployment_target = '10.0' - s.osx.deployment_target = '10.12' - s.tvos.deployment_target = '10.0' + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.11' + s.tvos.deployment_target = '9.0' # Add back when CocoaPods/CocoaPods#11558 is released #s.watchos.deployment_target = '3.0' s.version = '1.2.1' @@ -13,5 +13,4 @@ Pod::Spec.new do |s| s.summary = 'The official Swift api.video client for iOS, macOS and tvOS' s.source_files = 'Sources/**/*.swift' s.dependency 'AnyCodable-FlightSchool', '~> 0.6.1' - s.dependency 'Alamofire', '~> 5.4.3' end diff --git a/Cartfile b/Cartfile index 4fdc3a6..3f7e630 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1 @@ github "Flight-School/AnyCodable" ~> 0.6.1 -github "Alamofire/Alamofire" ~> 5.4.3 diff --git a/Example/Example/Controller/VideosViewController.swift b/Example/Example/Controller/VideosViewController.swift new file mode 100644 index 0000000..256e4ab --- /dev/null +++ b/Example/Example/Controller/VideosViewController.swift @@ -0,0 +1,85 @@ +// +// ViewController.swift +// Example +// + +import UIKit +import ApiVideoClient + +struct VideosOption{ + let title:String + let videoId: String + let thumbnail: String? + let handler: (()->Void) +} + +class VideosViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + var models = [VideosOption]() + + private func configure(){ + + ApiVideoClient.setApiKey(ClientManager.apiKey) + ApiVideoClient.basePath = ClientManager.environment.rawValue + + VideosAPI.list(title: nil, tags: nil, metadata: nil, description: nil, liveStreamId: nil, sortBy: nil, sortOrder: nil, currentPage: nil, pageSize: nil) { (response, error) in + guard error == nil else { + print(error ?? "error") + return + } + + if ((response) != nil) { + for item in response!.data { + self.models.append(VideosOption(title: item.title ?? "error title", videoId: item.videoId, thumbnail: item.assets?.thumbnail){ + + }) + self.tableView.reloadData() + } + } + } + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = UIColor.clear + return headerView + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return models.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = models[indexPath.row] + + + guard let cell = tableView.dequeueReusableCell(withIdentifier: VideoTableViewCell.identifier, for: indexPath) as? VideoTableViewCell else{ + return UITableViewCell() + } + cell.layer.cornerRadius = 8 + cell.configure(with: model) + return cell + } + + + private let tableView: UITableView = { + let table = UITableView(frame: .zero, style: .grouped) + table.register(VideoTableViewCell.self, forCellReuseIdentifier: VideoTableViewCell.identifier) + return table + }() + + override func viewDidLoad() { + super.viewDidLoad() + configure() + title = "Videos" + view.addSubview(tableView) + tableView.delegate = self + tableView.dataSource = self + tableView.frame = view.bounds + tableView.rowHeight = 310.0 + } + + + + +} + diff --git a/Example/Example/MainViewController.swift b/Example/Example/MainViewController.swift index e1d3bd6..1fe2f9b 100644 --- a/Example/Example/MainViewController.swift +++ b/Example/Example/MainViewController.swift @@ -157,7 +157,7 @@ extension MainViewController: UIImagePickerControllerDelegate, UINavigationContr progressView.isHidden = false // Set client configuration - ApiVideoClient.apiKey = SettingsManager.apiKey + ApiVideoClient.setApiKey(SettingsManager.apiKey) ApiVideoClient.basePath = SettingsManager.environment.rawValue // Upload diff --git a/Package.swift b/Package.swift index 267f9ea..b7f7d3f 100644 --- a/Package.swift +++ b/Package.swift @@ -5,9 +5,9 @@ import PackageDescription let package = Package( name: "ApiVideoClient", platforms: [ - .iOS(.v10), - .macOS(.v10_12), - .tvOS(.v10), + .iOS(.v9), + .macOS(.v10_11), + .tvOS(.v9), .watchOS(.v3), ], products: [ @@ -20,14 +20,13 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. .package(url: "https://github.com/Flight-School/AnyCodable", from: "0.6.1"), - .package(url: "https://github.com/Alamofire/Alamofire", from: "5.4.3"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "ApiVideoClient", - dependencies: ["AnyCodable", "Alamofire", ], + dependencies: ["AnyCodable", ], path: "Sources" ), // Targets for tests diff --git a/Sources/APIs.swift b/Sources/APIs.swift index 5f8883e..c2dbff2 100644 --- a/Sources/APIs.swift +++ b/Sources/APIs.swift @@ -6,16 +6,31 @@ import Foundation public class ApiVideoClient { - public static var apiKey: String? = nil + private static var apiKey: String? = nil public static var basePath = "https://ws.api.video" - internal static var customHeaders:[String: String] = ["AV-Origin-Client": "swift:1.2.1"] + internal static var defaultHeaders:[String: String] = ["AV-Origin-Client": "swift:1.2.1"] + internal static var credential: URLCredential? private static var chunkSize: Int = 50 * 1024 * 1024 - internal static var requestBuilderFactory: RequestBuilderFactory = AlamofireRequestBuilderFactory() - internal static var credential = ApiVideoCredential() + internal static var requestBuilderFactory: RequestBuilderFactory = URLSessionRequestBuilderFactory() public static var apiResponseQueue: DispatchQueue = .main public static var timeout: TimeInterval = 60 + internal static var customHeaders:[String: String] { + var headers = defaultHeaders + if let apiKey = apiKey { + headers["Authorization"] = apiKey + } + return headers + } + + public static func setApiKey(_ apiKey: String?) { + if let apiKey = apiKey { + self.apiKey = "Basic " + "\(apiKey):".toBase64() + } else { + self.apiKey = nil + } + } - public static func setChunkSize(chunkSize: Int) throws { + public static func setChunkSize(_ chunkSize: Int) throws { if (chunkSize > 128 * 1024 * 1024) { throw ParameterError.outOfRange } else if (chunkSize < 5 * 1024 * 1024) { @@ -40,25 +55,25 @@ public class ApiVideoClient { } } - static func isValidVersion(version: String) -> Bool { + static func isValidVersion(_ version: String) -> Bool { let pattern = #"^\d{1,3}(\.\d{1,3}(\.\d{1,3})?)?$"# return isValid(pattern: pattern, field: version) } - static func isValidName(name: String) -> Bool { + static func isValidName(_ name: String) -> Bool { let pattern = #"^[\w\-]{1,50}$"# return isValid(pattern: pattern, field: name) } static func setName(key: String, name: String, version: String) throws { - if(!isValidName(name: name)) { + if(!isValidName(name)) { throw ParameterError.invalidName } - if(!isValidVersion(version: version)) { + if(!isValidVersion(version)) { throw ParameterError.invalidVersion } - ApiVideoClient.customHeaders[key] = name + ":" + version + ApiVideoClient.defaultHeaders[key] = name + ":" + version } public static func setSdkName(name: String, version: String) throws { @@ -68,10 +83,10 @@ public class ApiVideoClient { public static func setApplicationName(name: String, version: String) throws { try setName(key: "AV-Origin-App", name: name, version: version) } - } open class RequestBuilder { + var credential: URLCredential? var headers: [String: String] public var parameters: [String: Any]? public let method: String @@ -79,6 +94,8 @@ open class RequestBuilder { public let requestTask: RequestTask = RequestTask() /// Optional block to obtain a reference to the request's progress instance when available. + /// With the URLSession http client the request's progress only works on iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0. + /// If you need to get the request's progress in older OS versions, please use Alamofire http client. public var onProgressReady: ((Progress) -> Void)? required public init(method: String, URLString: String, parameters: [String: Any]?, headers: [String: String] = [:], onProgressReady: ((Progress) -> Void)? = nil) { @@ -108,6 +125,11 @@ open class RequestBuilder { } return self } + + open func addCredential() -> Self { + credential = ApiVideoClient.credential + return self + } } public protocol RequestBuilderFactory { diff --git a/Sources/AlamofireImplementations.swift b/Sources/AlamofireImplementations.swift deleted file mode 100644 index a064c91..0000000 --- a/Sources/AlamofireImplementations.swift +++ /dev/null @@ -1,418 +0,0 @@ -// AlamofireImplementations.swift -// -// Generated by openapi-generator -// https://openapi-generator.tech -// - -import Foundation -import Alamofire - -class AlamofireRequestBuilderFactory: RequestBuilderFactory { - func getNonDecodableBuilder() -> RequestBuilder.Type { - return AlamofireRequestBuilder.self - } - - func getBuilder() -> RequestBuilder.Type { - return AlamofireDecodableRequestBuilder.self - } -} - -// Store manager to retain its reference -private var managerStore = SynchronizedDictionary() - -open class AlamofireRequestBuilder: RequestBuilder { - required public init(method: String, URLString: String, parameters: [String: Any]?, headers: [String: String] = [:], onProgressReady: ((Progress) -> Void)? = nil) { - super.init(method: method, URLString: URLString, parameters: parameters, headers: headers, onProgressReady: onProgressReady) - } - - /** - May be overridden by a subclass if you want to control the session - configuration. - */ - open func createAlamofireSession(interceptor: RequestInterceptor? = nil) -> Alamofire.Session { - let configuration = URLSessionConfiguration.default - configuration.timeoutIntervalForRequest = ApiVideoClient.timeout - configuration.timeoutIntervalForResource = ApiVideoClient.timeout - configuration.httpAdditionalHeaders = buildHeaders() - return Alamofire.Session(configuration: configuration, - interceptor: interceptor) - } - - /** - May be overridden by a subclass if you want to custom request constructor. - */ - open func createURLRequest() -> URLRequest? { - let xMethod = Alamofire.HTTPMethod(rawValue: method) - - let encoding: ParameterEncoding - - switch xMethod { - case .get, .head: - encoding = URLEncoding() - - case .options, .post, .put, .patch, .delete, .trace, .connect: - encoding = JSONDataEncoding() - - default: - fatalError("Unsupported HTTPMethod - \(xMethod.rawValue)") - } - - guard let originalRequest = try? URLRequest(url: URLString, method: xMethod, headers: HTTPHeaders(buildHeaders())) else { return nil } - return try? encoding.encode(originalRequest, with: parameters) - } - - /** - May be overridden by a subclass if you want to control the Content-Type - that is given to an uploaded form part. - - Return nil to use the default behavior (inferring the Content-Type from - the file extension). Return the desired Content-Type otherwise. - */ - open func contentTypeForFormPart(fileURL: URL) -> String? { - return nil - } - - /** - May be overridden by a subclass if you want to control the request - configuration (e.g. to override the cache policy). - */ - open func makeRequest(manager: Session, method: HTTPMethod, encoding: ParameterEncoding, headers: [String: String]) -> DataRequest { - return manager.request(URLString, method: method, parameters: parameters, encoding: encoding, headers: HTTPHeaders(headers)) - } - - private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> HTTPHeaders { - var disposition = "form-data; name=\"\(name)\"" - if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" } - - var headers: HTTPHeaders = [.contentDisposition(disposition)] - if let mimeType = mimeType { headers.add(.contentType(mimeType)) } - - return headers - } - - @discardableResult - override open func execute(_ apiResponseQueue: DispatchQueue = ApiVideoClient.apiResponseQueue, _ completion: @escaping (_ result: Swift.Result, ErrorResponse>) -> Void) -> RequestTask { - let managerId = UUID().uuidString - // Create a new manager for each request to customize its request header - var manager: Alamofire.Session - - if URLString.hasSuffix("/auth/api-key") { - manager = createAlamofireSession() - } else if let apiKey = ApiVideoClient.apiKey { - // Create the interceptor - let authenticator = ApiVideoAuthenticator(apiKey: apiKey) - let interceptor = AuthenticationInterceptor(authenticator: authenticator, credential: ApiVideoClient.credential) - - manager = createAlamofireSession(interceptor: interceptor) - } else { - manager = createAlamofireSession() - } - managerStore[managerId] = manager - - let xMethod = Alamofire.HTTPMethod(rawValue: method) - - let encoding: ParameterEncoding? - - switch xMethod { - case .get, .head: - encoding = URLEncoding() - - case .options, .post, .put, .patch, .delete, .trace, .connect: - let contentType = headers["Content-Type"] ?? "application/json" - - if contentType == "application/json" { - encoding = JSONDataEncoding() - } else if contentType == "multipart/form-data" { - encoding = nil - - let upload = manager.upload(multipartFormData: { mpForm in - for (k, v) in self.parameters! { - switch v { - case let chunkInputStream as FileChunkInputStream: - let fileURL = chunkInputStream.file - let headers = self.contentHeaders(withName: k, fileName: fileURL.lastPathComponent, mimeType: "video/quicktime") - mpForm.append(chunkInputStream, withLength: UInt64(chunkInputStream.capacity), headers: headers) - case let fileURL as URL: - if let mimeType = self.contentTypeForFormPart(fileURL: fileURL) { - mpForm.append(fileURL, withName: k, fileName: fileURL.lastPathComponent, mimeType: mimeType) - } else { - mpForm.append(fileURL, withName: k) - } - case let string as String: - mpForm.append(string.data(using: String.Encoding.utf8)!, withName: k) - case let number as NSNumber: - mpForm.append(number.stringValue.data(using: String.Encoding.utf8)!, withName: k) - default: - fatalError("Unprocessable value \(v) with key \(k)") - } - } - }, to: URLString, method: xMethod, headers: nil) - .uploadProgress { progress in - if let onProgressReady = self.onProgressReady { - onProgressReady(progress) - } - } - - requestTask.set(request: upload) - - self.processRequest(request: upload, managerId, apiResponseQueue, completion) - } else if contentType == "application/x-www-form-urlencoded" { - encoding = URLEncoding(destination: .httpBody) - } else { - fatalError("Unsupported Media Type - \(contentType)") - } - - default: - fatalError("Unsupported HTTPMethod - \(xMethod.rawValue)") - } - - if let encoding = encoding { - let request = makeRequest(manager: manager, method: xMethod, encoding: encoding, headers: headers) - if let onProgressReady = self.onProgressReady { - onProgressReady(request.uploadProgress) - } - processRequest(request: request, managerId, apiResponseQueue, completion) - requestTask.set(request: request) - } - - return requestTask - } - - fileprivate func processRequest(request: DataRequest, _ managerId: String, _ apiResponseQueue: DispatchQueue, _ completion: @escaping (_ result: Swift.Result, ErrorResponse>) -> Void) { - let cleanupRequest = { - managerStore[managerId] = nil - } - - let validatedRequest = request.validate() - - switch T.self { - case is Void.Type: - validatedRequest.responseData(queue: apiResponseQueue, completionHandler: { voidResponse in - cleanupRequest() - - switch voidResponse.result { - case .success: - completion(.success(Response(response: voidResponse.response!, body: () as! T))) - case let .failure(error): - completion(.failure(ErrorResponse.error(voidResponse.response?.statusCode ?? 500, voidResponse.data, voidResponse.response, error))) - } - - }) - default: - fatalError("Unsupported Response Body Type - \(String(describing: T.self))") - } - } - - open func buildHeaders() -> [String: String] { - var httpHeaders = Alamofire.HTTPHeaders.default.dictionary - for (key, value) in headers { - httpHeaders[key] = value - } - return httpHeaders - } - - fileprivate func getFileName(fromContentDisposition contentDisposition: String?) -> String? { - - guard let contentDisposition = contentDisposition else { - return nil - } - - let items = contentDisposition.components(separatedBy: ";") - - var filename: String? - - for contentItem in items { - - let filenameKey = "filename=" - guard let range = contentItem.range(of: filenameKey) else { - continue - } - - filename = contentItem - return filename? - .replacingCharacters(in: range, with: "") - .replacingOccurrences(of: "\"", with: "") - .trimmingCharacters(in: .whitespacesAndNewlines) - } - - return filename - - } - - fileprivate func getPath(from url: URL) throws -> String { - - guard var path = URLComponents(url: url, resolvingAgainstBaseURL: true)?.path else { - throw DownloadException.requestMissingPath - } - - if path.hasPrefix("/") { - path.remove(at: path.startIndex) - } - - return path - - } - - fileprivate func getURL(from urlRequest: URLRequest) throws -> URL { - - guard let url = urlRequest.url else { - throw DownloadException.requestMissingURL - } - - return url - } - -} - -open class AlamofireDecodableRequestBuilder: AlamofireRequestBuilder { - - override fileprivate func processRequest(request: DataRequest, _ managerId: String, _ apiResponseQueue: DispatchQueue, _ completion: @escaping (_ result: Swift.Result, ErrorResponse>) -> Void) { - let cleanupRequest = { - managerStore[managerId] = nil - } - - let validatedRequest = request.validate() - - switch T.self { - case is String.Type: - validatedRequest.responseString(queue: apiResponseQueue, completionHandler: { stringResponse in - cleanupRequest() - - switch stringResponse.result { - case let .success(value): - completion(.success(Response(response: stringResponse.response!, body: value as! T))) - case let .failure(error): - completion(.failure(ErrorResponse.error(stringResponse.response?.statusCode ?? 500, stringResponse.data, stringResponse.response, error))) - } - - }) - case is URL.Type: - validatedRequest.responseData(queue: apiResponseQueue, completionHandler: { dataResponse in - cleanupRequest() - - do { - - guard case .success = dataResponse.result else { - throw DownloadException.responseFailed - } - - guard let data = dataResponse.data else { - throw DownloadException.responseDataMissing - } - - guard let request = request.request else { - throw DownloadException.requestMissing - } - - let fileManager = FileManager.default - let urlRequest = try request.asURLRequest() - let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] - let requestURL = try self.getURL(from: urlRequest) - - var requestPath = try self.getPath(from: requestURL) - - if let headerFileName = self.getFileName(fromContentDisposition: dataResponse.response?.allHeaderFields["Content-Disposition"] as? String) { - requestPath = requestPath.appending("/\(headerFileName)") - } else { - requestPath = requestPath.appending("/tmp.ApiVideoClient.\(UUID().uuidString)") - } - - let filePath = cachesDirectory.appendingPathComponent(requestPath) - let directoryPath = filePath.deletingLastPathComponent().path - - try fileManager.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil) - try data.write(to: filePath, options: .atomic) - - completion(.success(Response(response: dataResponse.response!, body: filePath as! T))) - - } catch let requestParserError as DownloadException { - completion(.failure(ErrorResponse.error(400, dataResponse.data, dataResponse.response, requestParserError))) - } catch { - completion(.failure(ErrorResponse.error(400, dataResponse.data, dataResponse.response, error))) - } - return - }) - case is Void.Type: - validatedRequest.responseData(queue: apiResponseQueue, completionHandler: { voidResponse in - cleanupRequest() - - switch voidResponse.result { - case .success: - completion(.success(Response(response: voidResponse.response!, body: () as! T))) - case let .failure(error): - completion(.failure(ErrorResponse.error(voidResponse.response?.statusCode ?? 500, voidResponse.data, voidResponse.response, error))) - } - - }) - case is Data.Type: - validatedRequest.responseData(queue: apiResponseQueue, completionHandler: { dataResponse in - cleanupRequest() - - switch dataResponse.result { - case .success: - completion(.success(Response(response: dataResponse.response!, body: dataResponse.data as! T))) - case let .failure(error): - completion(.failure(ErrorResponse.error(dataResponse.response?.statusCode ?? 500, dataResponse.data, dataResponse.response, error))) - } - - }) - default: - validatedRequest.responseData(queue: apiResponseQueue, completionHandler: { dataResponse in - cleanupRequest() - - if case let .failure(error) = dataResponse.result { - var statusCode = dataResponse.response?.statusCode - // Return status code of underlyingError for interception error mainly - if let underlyingErrorResponse = error.underlyingError as? ErrorResponse { - if case let ErrorResponse.error(code, _, _, _) = underlyingErrorResponse { - statusCode = code - } - } - completion(.failure(ErrorResponse.error(statusCode ?? 500, dataResponse.data, dataResponse.response, error))) - return - } - - guard let data = dataResponse.data, !data.isEmpty else { - completion(.failure(ErrorResponse.error(-1, nil, dataResponse.response, DecodableRequestBuilderError.emptyDataResponse))) - return - } - - guard let httpResponse = dataResponse.response else { - completion(.failure(ErrorResponse.error(-2, nil, dataResponse.response, DecodableRequestBuilderError.nilHTTPResponse))) - return - } - - let decodeResult = CodableHelper.decode(T.self, from: data) - - switch decodeResult { - case let .success(decodableObj): - completion(.success(Response(response: httpResponse, body: decodableObj))) - case let .failure(error): - completion(.failure(ErrorResponse.error(httpResponse.statusCode, data, httpResponse, error))) - } - - }) - } - } - -} - -extension JSONDataEncoding: ParameterEncoding { - - // MARK: Encoding - - /// Creates a URL request by encoding parameters and applying them onto an existing request. - /// - /// - parameter urlRequest: The request to have parameters applied. - /// - parameter parameters: The parameters to apply. This should have a single key/value - /// pair with "jsonData" as the key and a Data object as the value. - /// - /// - throws: An `Error` if the encoding process encounters an error. - /// - /// - returns: The encoded request. - public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { - let urlRequest = try urlRequest.asURLRequest() - - return encode(urlRequest, with: parameters) - } -} diff --git a/Sources/Auth/ApiVideoAuthenticator.swift b/Sources/Auth/ApiVideoAuthenticator.swift deleted file mode 100644 index eac01eb..0000000 --- a/Sources/Auth/ApiVideoAuthenticator.swift +++ /dev/null @@ -1,52 +0,0 @@ -// ApiVideoAuthenticator.swift -// - -import Foundation -import Alamofire - -class ApiVideoAuthenticator: Authenticator { - private let apiKey: String - - public init(apiKey: String) { - self.apiKey = apiKey - } - - func apply(_ credential: ApiVideoCredential, to urlRequest: inout URLRequest) { - urlRequest.headers.add(.authorization(bearerToken: credential.accessToken)) - } - - func refresh(_ credential: ApiVideoCredential, - for session: Session, - completion: @escaping (Result) -> Void) { - AdvancedAuthenticationAPI.authenticate(authenticatePayload: AuthenticatePayload(apiKey: apiKey)) { accessToken, error in - if let error = error { - completion(.failure(error)) - } - if let accessToken = accessToken { - ApiVideoClient.credential = ApiVideoCredential(accessToken: accessToken) - completion(.success(ApiVideoClient.credential)) - } - } - } - - func didRequest(_ urlRequest: URLRequest, - with response: HTTPURLResponse, - failDueToAuthenticationError error: Error) -> Bool { - // If authentication server CANNOT invalidate credentials, return `false` - if response.statusCode != 401 { - return false - } - - // If authentication server CAN invalidate credentials, then inspect the response matching against what the - // authentication server returns as an authentication failure. - // We request a new access token if we received a 401 anyway. - return true - } - - func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: ApiVideoCredential) -> Bool { - // If authentication server CAN invalidate credentials, then compare the "Authorization" header value in the - // `URLRequest` against the Bearer token generated with the access token of the `Credential`. - let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value - return urlRequest.headers["Authorization"] == bearerToken - } -} diff --git a/Sources/Auth/ApiVideoCredential.swift b/Sources/Auth/ApiVideoCredential.swift deleted file mode 100644 index 04add43..0000000 --- a/Sources/Auth/ApiVideoCredential.swift +++ /dev/null @@ -1,31 +0,0 @@ -// ApiVideoCredential.swift -// - -import Foundation -import Alamofire - -struct ApiVideoCredential: AuthenticationCredential { - let accessToken: String - let refreshToken: String - let tokenType: String - - let expiration: Date - - // Require refresh if within 5 minutes of expiration - var requiresRefresh: Bool { Date(timeIntervalSinceNow: 60 * 5) > expiration } - - - public init(accessToken: AccessToken) { - self.accessToken = accessToken.accessToken! - self.refreshToken = accessToken.refreshToken! - self.tokenType = accessToken.tokenType! - self.expiration = Date(timeIntervalSinceNow: Double(accessToken.expiresIn!)) - } - - public init() { - self.accessToken = "" - self.refreshToken = "" - self.tokenType = "" - self.expiration = Date() - } -} diff --git a/Sources/Extensions.swift b/Sources/Extensions.swift index 6e91c87..3b8e413 100644 --- a/Sources/Extensions.swift +++ b/Sources/Extensions.swift @@ -115,6 +115,12 @@ extension String: CodingKey { } +extension String { + func toBase64() -> String { + return Data(self.utf8).base64EncodedString() + } +} + extension KeyedEncodingContainerProtocol { public mutating func encodeArray(_ values: [T], forKey key: Self.Key) throws where T: Encodable { diff --git a/Sources/Models.swift b/Sources/Models.swift index d336adb..8436e60 100644 --- a/Sources/Models.swift +++ b/Sources/Models.swift @@ -5,7 +5,6 @@ // import Foundation -import Alamofire protocol JSONEncodable { func encodeToJSON() -> Any @@ -65,30 +64,31 @@ open class Response { public class RequestTask { private var lock = NSRecursiveLock() - private var request: Request? + private var task: URLSessionTask? - internal func set(request: Request) { + internal func set(task: URLSessionTask) { lock.lock() defer { lock.unlock() } - self.request = request + self.task = task } public func cancel() { lock.lock() defer { lock.unlock() } - request?.cancel() - request = nil + task?.cancel() + task = nil } - public var state: Request.State { - request?.state ?? .initialized - } - - public var uploadProgress: Progress? { - request?.uploadProgress + public var isFinished: Bool { + guard let state = task?.state else { + return false + } + + return state == URLSessionTask.State.completed } - public var downloadProgress: Progress? { - request?.downloadProgress + @available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *) + public var progress: Progress? { + return task?.progress } } \ No newline at end of file diff --git a/Sources/URLSessionImplementations.swift b/Sources/URLSessionImplementations.swift new file mode 100644 index 0000000..aaf42d3 --- /dev/null +++ b/Sources/URLSessionImplementations.swift @@ -0,0 +1,691 @@ +// URLSessionImplementations.swift +// +// Generated by openapi-generator +// https://openapi-generator.tech +// + +import Foundation +#if !os(macOS) +import MobileCoreServices +#endif + +class URLSessionRequestBuilderFactory: RequestBuilderFactory { + func getNonDecodableBuilder() -> RequestBuilder.Type { + return URLSessionRequestBuilder.self + } + + func getBuilder() -> RequestBuilder.Type { + return URLSessionDecodableRequestBuilder.self + } +} + +public typealias ApiVideoClientChallengeHandler = ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?)) + +// Store the URLSession's delegate to retain its reference +private let sessionDelegate = SessionDelegate() + +// Store the URLSession to retain its reference +private let defaultURLSession = URLSession(configuration: .default, delegate: sessionDelegate, delegateQueue: nil) + +// Store current taskDidReceiveChallenge for every URLSessionTask +private var challengeHandlerStore = SynchronizedDictionary() + +// Store current URLCredential for every URLSessionTask +private var credentialStore = SynchronizedDictionary() + +open class URLSessionRequestBuilder: RequestBuilder { + + /** + May be assigned if you want to control the authentication challenges. + */ + public var taskDidReceiveChallenge: ApiVideoClientChallengeHandler? + + /** + To observe upload or download progress + */ + public var progressObservation: NSKeyValueObservation? + + /** + May be assigned if you want to do any of those things: + - control the task completion + - intercept and handle errors like authorization + - retry the request. + */ + @available(*, deprecated, message: "Please override execute() method to intercept and handle errors like authorization or retry the request. Check the Wiki for more info. https://github.com/OpenAPITools/openapi-generator/wiki/FAQ#how-do-i-implement-bearer-token-authentication-with-urlsession-on-the-swift-api-client") + public var taskCompletionShouldRetry: ((Data?, URLResponse?, Error?, @escaping (Bool) -> Void) -> Void)? + + required public init(method: String, URLString: String, parameters: [String: Any]?, headers: [String: String] = [:], onProgressReady: ((Progress) -> Void)? = nil) { + super.init(method: method, URLString: URLString, parameters: parameters, headers: headers, onProgressReady: onProgressReady) + } + + /** + May be overridden by a subclass if you want to control the URLSession + configuration. + */ + open func createURLSession() -> URLSession { + return defaultURLSession + } + + /** + May be overridden by a subclass if you want to control the Content-Type + that is given to an uploaded form part. + + Return nil to use the default behavior (inferring the Content-Type from + the file extension). Return the desired Content-Type otherwise. + */ + open func contentTypeForFormPart(fileURL: URL) -> String? { + return nil + } + + /** + May be overridden by a subclass if you want to control the URLRequest + configuration (e.g. to override the cache policy). + */ + open func createURLRequest(urlSession: URLSession, method: HTTPMethod, encoding: ParameterEncoding, headers: [String: String]) throws -> URLRequest { + + guard let url = URL(string: URLString) else { + throw DownloadException.requestMissingURL + } + + var originalRequest = URLRequest(url: url) + originalRequest.timeoutInterval = ApiVideoClient.timeout + originalRequest.httpMethod = method.rawValue + + headers.forEach { key, value in + originalRequest.setValue(value, forHTTPHeaderField: key) + } + + buildHeaders().forEach { key, value in + originalRequest.setValue(value, forHTTPHeaderField: key) + } + + let modifiedRequest = try encoding.encode(originalRequest, with: parameters) + + return modifiedRequest + } + + @discardableResult + override open func execute(_ apiResponseQueue: DispatchQueue = ApiVideoClient.apiResponseQueue, _ completion: @escaping (_ result: Swift.Result, ErrorResponse>) -> Void) -> RequestTask { + let urlSession = createURLSession() + + guard let xMethod = HTTPMethod(rawValue: method) else { + fatalError("Unsupported Http method - \(method)") + } + + let encoding: ParameterEncoding + + switch xMethod { + case .get, .head: + encoding = URLEncoding() + + case .options, .post, .put, .patch, .delete, .trace, .connect: + let contentType = headers["Content-Type"] ?? "application/json" + + if contentType == "application/json" { + encoding = JSONDataEncoding() + } else if contentType == "multipart/form-data" { + encoding = FormDataEncoding(contentTypeForFormPart: contentTypeForFormPart(fileURL:)) + } else if contentType == "application/x-www-form-urlencoded" { + encoding = FormURLEncoding() + } else { + fatalError("Unsupported Media Type - \(contentType)") + } + } + + do { + let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers) + + var taskIdentifier: Int? + let cleanupRequest = { + if let taskIdentifier = taskIdentifier { + challengeHandlerStore[taskIdentifier] = nil + credentialStore[taskIdentifier] = nil + } + } + + let dataTask = urlSession.dataTask(with: request) { data, response, error in + + if let taskCompletionShouldRetry = self.taskCompletionShouldRetry { + + taskCompletionShouldRetry(data, response, error) { shouldRetry in + + if shouldRetry { + cleanupRequest() + self.execute(apiResponseQueue, completion) + } else { + apiResponseQueue.async { + self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion) + cleanupRequest() + } + } + } + } else { + apiResponseQueue.async { + self.processRequestResponse(urlRequest: request, data: data, response: response, error: error, completion: completion) + cleanupRequest() + } + } + } + + if #available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *) { + if let onProgressReady = onProgressReady { + progressObservation = dataTask.progress.observe(\.fractionCompleted) { progress, _ in + onProgressReady(progress) + } + } + } + + taskIdentifier = dataTask.taskIdentifier + challengeHandlerStore[dataTask.taskIdentifier] = taskDidReceiveChallenge + credentialStore[dataTask.taskIdentifier] = credential + + dataTask.resume() + + requestTask.set(task: dataTask) + } catch { + apiResponseQueue.async { + completion(.failure(ErrorResponse.error(415, nil, nil, error))) + } + } + + return requestTask + } + + fileprivate func processRequestResponse(urlRequest: URLRequest, data: Data?, response: URLResponse?, error: Error?, completion: @escaping (_ result: Swift.Result, ErrorResponse>) -> Void) { + + if let error = error { + completion(.failure(ErrorResponse.error(-1, data, response, error))) + return + } + + guard let httpResponse = response as? HTTPURLResponse else { + completion(.failure(ErrorResponse.error(-2, data, response, DecodableRequestBuilderError.nilHTTPResponse))) + return + } + + guard httpResponse.isStatusCodeSuccessful else { + completion(.failure(ErrorResponse.error(httpResponse.statusCode, data, response, DecodableRequestBuilderError.unsuccessfulHTTPStatusCode))) + return + } + + switch T.self { + case is Void.Type: + + completion(.success(Response(response: httpResponse, body: () as! T))) + + default: + fatalError("Unsupported Response Body Type - \(String(describing: T.self))") + } + + } + + open func buildHeaders() -> [String: String] { + var httpHeaders: [String: String] = [:] + for (key, value) in headers { + httpHeaders[key] = value + } + for (key, value) in ApiVideoClient.customHeaders { + httpHeaders[key] = value + } + return httpHeaders + } + + fileprivate func getFileName(fromContentDisposition contentDisposition: String?) -> String? { + + guard let contentDisposition = contentDisposition else { + return nil + } + + let items = contentDisposition.components(separatedBy: ";") + + var filename: String? + + for contentItem in items { + + let filenameKey = "filename=" + guard let range = contentItem.range(of: filenameKey) else { + continue + } + + filename = contentItem + return filename? + .replacingCharacters(in: range, with: "") + .replacingOccurrences(of: "\"", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + return filename + + } + + fileprivate func getPath(from url: URL) throws -> String { + + guard var path = URLComponents(url: url, resolvingAgainstBaseURL: true)?.path else { + throw DownloadException.requestMissingPath + } + + if path.hasPrefix("/") { + path.remove(at: path.startIndex) + } + + return path + + } + + fileprivate func getURL(from urlRequest: URLRequest) throws -> URL { + + guard let url = urlRequest.url else { + throw DownloadException.requestMissingURL + } + + return url + } + +} + +open class URLSessionDecodableRequestBuilder: URLSessionRequestBuilder { + override fileprivate func processRequestResponse(urlRequest: URLRequest, data: Data?, response: URLResponse?, error: Error?, completion: @escaping (_ result: Swift.Result, ErrorResponse>) -> Void) { + + if let error = error { + completion(.failure(ErrorResponse.error(-1, data, response, error))) + return + } + + guard let httpResponse = response as? HTTPURLResponse else { + completion(.failure(ErrorResponse.error(-2, data, response, DecodableRequestBuilderError.nilHTTPResponse))) + return + } + + guard httpResponse.isStatusCodeSuccessful else { + completion(.failure(ErrorResponse.error(httpResponse.statusCode, data, response, DecodableRequestBuilderError.unsuccessfulHTTPStatusCode))) + return + } + + switch T.self { + case is String.Type: + + let body = data.flatMap { String(data: $0, encoding: .utf8) } ?? "" + + completion(.success(Response(response: httpResponse, body: body as! T))) + + case is URL.Type: + do { + + guard error == nil else { + throw DownloadException.responseFailed + } + + guard let data = data else { + throw DownloadException.responseDataMissing + } + + let fileManager = FileManager.default + let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let requestURL = try getURL(from: urlRequest) + + var requestPath = try getPath(from: requestURL) + + if let headerFileName = getFileName(fromContentDisposition: httpResponse.allHeaderFields["Content-Disposition"] as? String) { + requestPath = requestPath.appending("/\(headerFileName)") + } else { + requestPath = requestPath.appending("/tmp.ApiVideoClient.\(UUID().uuidString)") + } + + let filePath = cachesDirectory.appendingPathComponent(requestPath) + let directoryPath = filePath.deletingLastPathComponent().path + + try fileManager.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil) + try data.write(to: filePath, options: .atomic) + + completion(.success(Response(response: httpResponse, body: filePath as! T))) + + } catch let requestParserError as DownloadException { + completion(.failure(ErrorResponse.error(400, data, response, requestParserError))) + } catch { + completion(.failure(ErrorResponse.error(400, data, response, error))) + } + + case is Void.Type: + + completion(.success(Response(response: httpResponse, body: () as! T))) + + case is Data.Type: + + completion(.success(Response(response: httpResponse, body: data as! T))) + + default: + + guard let data = data, !data.isEmpty else { + completion(.failure(ErrorResponse.error(httpResponse.statusCode, nil, response, DecodableRequestBuilderError.emptyDataResponse))) + return + } + + let decodeResult = CodableHelper.decode(T.self, from: data) + + switch decodeResult { + case let .success(decodableObj): + completion(.success(Response(response: httpResponse, body: decodableObj))) + case let .failure(error): + completion(.failure(ErrorResponse.error(httpResponse.statusCode, data, response, error))) + } + } + } +} + +private class SessionDelegate: NSObject, URLSessionTaskDelegate { + func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + + var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling + + var credential: URLCredential? + + if let taskDidReceiveChallenge = challengeHandlerStore[task.taskIdentifier] { + (disposition, credential) = taskDidReceiveChallenge(session, task, challenge) + } else { + if challenge.previousFailureCount > 0 { + disposition = .rejectProtectionSpace + } else { + credential = credentialStore[task.taskIdentifier] ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace) + + if credential != nil { + disposition = .useCredential + } + } + } + + completionHandler(disposition, credential) + } +} + +public enum HTTPMethod: String { + case options = "OPTIONS" + case get = "GET" + case head = "HEAD" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" + case trace = "TRACE" + case connect = "CONNECT" +} + +public protocol ParameterEncoding { + func encode(_ urlRequest: URLRequest, with parameters: [String: Any]?) throws -> URLRequest +} + +private class URLEncoding: ParameterEncoding { + func encode(_ urlRequest: URLRequest, with parameters: [String: Any]?) throws -> URLRequest { + + var urlRequest = urlRequest + + guard let parameters = parameters else { return urlRequest } + + guard let url = urlRequest.url else { + throw DownloadException.requestMissingURL + } + + if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { + urlComponents.queryItems = APIHelper.mapValuesToQueryItems(parameters) + urlRequest.url = urlComponents.url + } + + return urlRequest + } +} + +private class FormDataEncoding: ParameterEncoding { + + let contentTypeForFormPart: (_ fileURL: URL) -> String? + + init(contentTypeForFormPart: @escaping (_ fileURL: URL) -> String?) { + self.contentTypeForFormPart = contentTypeForFormPart + } + + func encode(_ urlRequest: URLRequest, with parameters: [String: Any]?) throws -> URLRequest { + + var urlRequest = urlRequest + + guard let parameters = parameters, !parameters.isEmpty else { + return urlRequest + } + + let boundary = "Boundary-\(UUID().uuidString)" + + urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + for (key, value) in parameters { + switch value { + case let fileChunkInputStream as FileChunkInputStream: + + urlRequest = configureInputStreamRequest( + urlRequest: urlRequest, + boundary: boundary, + name: key, + fileName: fileChunkInputStream.file.lastPathComponent, + inputStream: fileChunkInputStream, + length: UInt64(fileChunkInputStream.capacity) + ) + + case let fileURL as URL: + + urlRequest = try configureFileUploadRequest( + urlRequest: urlRequest, + boundary: boundary, + name: key, + fileURL: fileURL + ) + + case let string as String: + + if let data = string.data(using: .utf8) { + urlRequest = configureDataUploadRequest( + urlRequest: urlRequest, + boundary: boundary, + name: key, + data: data + ) + } + + case let number as NSNumber: + + if let data = number.stringValue.data(using: .utf8) { + urlRequest = configureDataUploadRequest( + urlRequest: urlRequest, + boundary: boundary, + name: key, + data: data + ) + } + + default: + fatalError("Unprocessable value \(value) with key \(key)") + } + } + + var body = urlRequest.httpBody.orEmpty + + body.append("\r\n--\(boundary)--\r\n") + + urlRequest.httpBody = body + + return urlRequest + } + + private func configureInputStreamRequest(urlRequest: URLRequest, boundary: String, name: String, fileName: String, inputStream: InputStream, length: UInt64) -> URLRequest { + + var urlRequest = urlRequest + + var body = urlRequest.httpBody.orEmpty + + let data = encodeInputStream(for: inputStream, withLength: length) + + let mimetype = "video/quicktime" + + // If we already added something then we need an additional newline. + if body.count > 0 { + body.append("\r\n") + } + + // Value boundary. + body.append("--\(boundary)\r\n") + + // Value headers. + body.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n") + body.append("Content-Type: \(mimetype)\r\n") + + // Separate headers and body. + body.append("\r\n") + + // The value data. + body.append(data) + + urlRequest.httpBody = body + + return urlRequest + } + + private func configureFileUploadRequest(urlRequest: URLRequest, boundary: String, name: String, fileURL: URL) throws -> URLRequest { + + var urlRequest = urlRequest + + var body = urlRequest.httpBody.orEmpty + + let fileData = try Data(contentsOf: fileURL) + + let mimetype = contentTypeForFormPart(fileURL) ?? mimeType(for: fileURL) + + let fileName = fileURL.lastPathComponent + + // If we already added something then we need an additional newline. + if body.count > 0 { + body.append("\r\n") + } + + // Value boundary. + body.append("--\(boundary)\r\n") + + // Value headers. + body.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(fileName)\"\r\n") + body.append("Content-Type: \(mimetype)\r\n") + + // Separate headers and body. + body.append("\r\n") + + // The value data. + body.append(fileData) + + urlRequest.httpBody = body + + return urlRequest + } + + private func configureDataUploadRequest(urlRequest: URLRequest, boundary: String, name: String, data: Data) -> URLRequest { + + var urlRequest = urlRequest + + var body = urlRequest.httpBody.orEmpty + + // If we already added something then we need an additional newline. + if body.count > 0 { + body.append("\r\n") + } + + // Value boundary. + body.append("--\(boundary)\r\n") + + // Value headers. + body.append("Content-Disposition: form-data; name=\"\(name)\"\r\n") + + // Separate headers and body. + body.append("\r\n") + + // The value data. + body.append(data) + + urlRequest.httpBody = body + + return urlRequest + + } + + func mimeType(for url: URL) -> String { + let pathExtension = url.pathExtension + + if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() { + if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { + return mimetype as String + } + } + return "application/octet-stream" + } + + private func encodeInputStream(for inputStream: InputStream, withLength length: UInt64) -> Data { + inputStream.open() + defer { inputStream.close() } + + let streamBufferSize = 1024 + var encoded = Data() + + while inputStream.hasBytesAvailable { + var buffer = [UInt8](repeating: 0, count: streamBufferSize) + let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) + + if let error = inputStream.streamError { + fatalError("Reading input stream failed with error \(error)") + } + + if bytesRead > 0 { + encoded.append(buffer, count: bytesRead) + } else { + break + } + } + + guard UInt64(encoded.count) == length else { + fatalError("Unexpected input stream length. Expected: \(length) Got: \(UInt64(encoded.count))") + } + + return encoded + } + +} + +private class FormURLEncoding: ParameterEncoding { + func encode(_ urlRequest: URLRequest, with parameters: [String: Any]?) throws -> URLRequest { + + var urlRequest = urlRequest + + var requestBodyComponents = URLComponents() + requestBodyComponents.queryItems = APIHelper.mapValuesToQueryItems(parameters ?? [:]) + + if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { + urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + } + + urlRequest.httpBody = requestBodyComponents.query?.data(using: .utf8) + + return urlRequest + } +} + +private extension Data { + /// Append string to Data + /// + /// Rather than littering my code with calls to `dataUsingEncoding` to convert strings to Data, and then add that data to the Data, this wraps it in a nice convenient little extension to Data. This converts using UTF-8. + /// + /// - parameter string: The string to be added to the `Data`. + + mutating func append(_ string: String) { + if let data = string.data(using: .utf8) { + append(data) + } + } +} + +private extension Optional where Wrapped == Data { + var orEmpty: Data { + self ?? Data() + } +} + +extension JSONDataEncoding: ParameterEncoding {} diff --git a/Sources/Upload/FileChunkInputStream.swift b/Sources/Upload/FileChunkInputStream.swift index f2a1cde..d728ed6 100644 --- a/Sources/Upload/FileChunkInputStream.swift +++ b/Sources/Upload/FileChunkInputStream.swift @@ -1,6 +1,3 @@ -// FileChunkInputStream.swift -// - import Foundation public class FileChunkInputStream: InputStream { diff --git a/Sources/Upload/ProgressiveUploadSessionProtocol.swift b/Sources/Upload/ProgressiveUploadSessionProtocol.swift index c60261c..88a093a 100644 --- a/Sources/Upload/ProgressiveUploadSessionProtocol.swift +++ b/Sources/Upload/ProgressiveUploadSessionProtocol.swift @@ -1,6 +1,3 @@ -// ProgressiveUploadSessionProtocol.swift -// - import Foundation public protocol ProgressiveUploadSessionProtocol { diff --git a/Sources/Upload/RequestTaskQueue.swift b/Sources/Upload/RequestTaskQueue.swift index 16a020e..c5bea8e 100644 --- a/Sources/Upload/RequestTaskQueue.swift +++ b/Sources/Upload/RequestTaskQueue.swift @@ -1,12 +1,10 @@ import Foundation -import Alamofire public class RequestTaskQueue: RequestTask { private let operationQueue: OperationQueue private var requestBuilders: [RequestBuilder] = [] - private let _downloadProgress = Progress(totalUnitCount: 0) - private let _uploadProgress = Progress(totalUnitCount: 0) + private let _progress = Progress(totalUnitCount: 0) internal init(queueLabel: String) { operationQueue = OperationQueue() @@ -15,34 +13,20 @@ public class RequestTaskQueue: RequestTask { super.init() } - override public var uploadProgress: Progress { + @available(iOS 11.0, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *) + override public var progress: Progress { var completedUnitCount: Int64 = 0 var totalUnitCount: Int64 = 0 requestBuilders.forEach { - if let progress = $0.requestTask.uploadProgress { + if let progress = $0.requestTask.progress { completedUnitCount += progress.completedUnitCount totalUnitCount += progress.totalUnitCount } } - _uploadProgress.totalUnitCount = totalUnitCount - _uploadProgress.completedUnitCount = completedUnitCount - return _uploadProgress - } - - override public var downloadProgress: Progress { - var completedUnitCount: Int64 = 0 - var totalUnitCount: Int64 = 0 - requestBuilders.forEach { - if let progress = $0.requestTask.downloadProgress { - completedUnitCount += progress.completedUnitCount - totalUnitCount += progress.totalUnitCount - } - } - - _downloadProgress.totalUnitCount = totalUnitCount - _downloadProgress.completedUnitCount = completedUnitCount - return _downloadProgress + _progress.totalUnitCount = totalUnitCount + _progress.completedUnitCount = completedUnitCount + return _progress } internal func willExecuteRequestBuilder(requestBuilder: RequestBuilder) -> Void { @@ -62,6 +46,10 @@ public class RequestTaskQueue: RequestTask { } operationQueue.cancelAllOperations() } + + override public var isFinished: Bool { + requestBuilders.allSatisfy { $0.requestTask.isFinished } + } } final class RequestOperation: Operation { diff --git a/Sources/Upload/UploadChunkRequestTaskQueue.swift b/Sources/Upload/UploadChunkRequestTaskQueue.swift index c038d2e..8e76e8f 100644 --- a/Sources/Upload/UploadChunkRequestTaskQueue.swift +++ b/Sources/Upload/UploadChunkRequestTaskQueue.swift @@ -1,6 +1,4 @@ import Foundation -import Alamofire - public class UploadChunkRequestTaskQueue: RequestTaskQueue