|
| 1 | +// |
| 2 | +// BGUploadService.swift |
| 3 | +// BGUploader |
| 4 | +// |
| 5 | +// Created by Ruben Nine on 20/10/21. |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | +import FilestackSDK |
| 10 | + |
| 11 | +public protocol BGUploadServiceDelegate: AnyObject { |
| 12 | + /// Called after an upload task completes (either successfully or failing.) |
| 13 | + /// |
| 14 | + /// You may query `url` to determine the file `URL` that was uploaded and `status` to determine completion status |
| 15 | + /// on the returned `BackgroundUploadTaskResult` object. |
| 16 | + func uploadService(_ uploadService: BGUploadService, didCompleteWith result: BackgroundUploadTaskResult) |
| 17 | +} |
| 18 | + |
| 19 | +public class BGUploadService: NSObject { |
| 20 | + // MARK: - Public Properties |
| 21 | + |
| 22 | + public let backgroundIdentifer = "com.filestack.BGUploader" |
| 23 | + public weak var delegate: BGUploadServiceDelegate? |
| 24 | + |
| 25 | + // MARK: - Private Properties |
| 26 | + |
| 27 | + private let storeURL = URL(string: "https://www.filestackapi.com/api/store/S3")! |
| 28 | + private var transitorySessionData = [URLSessionTask: Data]() |
| 29 | + private let fsClient: Client |
| 30 | + |
| 31 | + private lazy var session: URLSession = { |
| 32 | + let configuration: URLSessionConfiguration |
| 33 | + |
| 34 | + configuration = .background(withIdentifier: backgroundIdentifer) |
| 35 | + configuration.isDiscretionary = false |
| 36 | + configuration.waitsForConnectivity = true |
| 37 | + |
| 38 | + return URLSession(configuration: configuration, delegate: self, delegateQueue: .main) |
| 39 | + }() |
| 40 | + |
| 41 | + // MARK: - Lifecycle |
| 42 | + |
| 43 | + public init(fsClient: Client) { |
| 44 | + self.fsClient = fsClient |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +// MARK: - URLSessionDataDelegate Protocol Implementation |
| 49 | + |
| 50 | +extension BGUploadService: URLSessionDataDelegate { |
| 51 | + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { |
| 52 | + transitorySessionData[dataTask] = data |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +// MARK: - URLSessionTaskDelegate Protocol Implementation |
| 57 | + |
| 58 | +extension BGUploadService: URLSessionTaskDelegate { |
| 59 | + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Swift.Error?) { |
| 60 | + guard let result = UserDefaults.backgroundUploadProcess.tasks[task.taskIdentifier] else { return } |
| 61 | + |
| 62 | + if task.state == .completed, let responseData = transitorySessionData[task] { |
| 63 | + transitorySessionData.removeValue(forKey: task) |
| 64 | + |
| 65 | + do { |
| 66 | + let storeResponse = try JSONDecoder().decode(StoreResponse.self, from: responseData) |
| 67 | + result.status = .completed(response: storeResponse) |
| 68 | + } catch { |
| 69 | + result.status = .failed(error: .undecodableJSONResponse) |
| 70 | + } |
| 71 | + } else if let error = error { |
| 72 | + result.status = .failed(error: .other(description: error.localizedDescription)) |
| 73 | + } else { |
| 74 | + result.status = .failed(error: .unknown) |
| 75 | + } |
| 76 | + |
| 77 | + delegate?.uploadService(self, didCompleteWith: result) |
| 78 | + removeTaskResult(with: task.taskIdentifier) |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +// MARK: - BGUploadService Error |
| 83 | + |
| 84 | +public extension BGUploadService { |
| 85 | + enum Error: Swift.Error, Equatable, Codable { |
| 86 | + case undecodableJSONResponse |
| 87 | + case other(description: String) |
| 88 | + case unknown |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +// MARK: - Public Functions |
| 93 | + |
| 94 | +public extension BGUploadService { |
| 95 | + /// Uploads an `URL` to Filestack using a background `URLSession`. |
| 96 | + @discardableResult |
| 97 | + func upload(url: URL) -> URLSessionUploadTask? { |
| 98 | + let task = session.uploadTask(with: storeRequest(for: url), fromFile: url) |
| 99 | + |
| 100 | + addTaskResult(with: task.taskIdentifier, for: url) |
| 101 | + |
| 102 | + task.resume() |
| 103 | + |
| 104 | + return task |
| 105 | + } |
| 106 | + |
| 107 | + /// Resumes any pending background uploads. |
| 108 | + /// |
| 109 | + /// Call this function on your `AppDelegate.application(_:,handleEventsForBackgroundURLSession:,completionHandler:)` |
| 110 | + func resumePendingUploads(completionHandler: @escaping () -> Void) { |
| 111 | + session.getAllTasks { tasks in |
| 112 | + for task in tasks { |
| 113 | + task.resume() |
| 114 | + } |
| 115 | + |
| 116 | + completionHandler() |
| 117 | + } |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +// MARK: - Private Functions |
| 122 | + |
| 123 | +private extension BGUploadService { |
| 124 | + /// Returns an `URLRequest` setup for uploading a file using |
| 125 | + /// Filestack's [Basic Uploads](https://www.filestack.com/docs/uploads/uploading/#basic-uploads) API. |
| 126 | + func storeRequest(for url: URL) -> URLRequest { |
| 127 | + var components = URLComponents(url: storeURL, resolvingAgainstBaseURL: false)! |
| 128 | + var queryItems: [URLQueryItem] = [] |
| 129 | + |
| 130 | + queryItems = [ |
| 131 | + URLQueryItem(name: "key", value: fsClient.apiKey), |
| 132 | + URLQueryItem(name: "filename", value: url.filename) |
| 133 | + ] |
| 134 | + |
| 135 | + if let security = fsClient.security { |
| 136 | + queryItems.append(URLQueryItem(name: "policy", value: security.encodedPolicy)) |
| 137 | + queryItems.append(URLQueryItem(name: "signature", value: security.signature)) |
| 138 | + } |
| 139 | + |
| 140 | + components.queryItems = queryItems |
| 141 | + |
| 142 | + var request = URLRequest(url: components.url!) |
| 143 | + |
| 144 | + request.addValue(url.mimeType ?? "text/plain", forHTTPHeaderField: "Content-Type") |
| 145 | + request.httpMethod = "POST" |
| 146 | + |
| 147 | + return request |
| 148 | + } |
| 149 | + |
| 150 | + @discardableResult |
| 151 | + func addTaskResult(with taskIdentifier: Int, for url: URL) -> BackgroundUploadTaskResult { |
| 152 | + let taskResult = BackgroundUploadTaskResult(url: url) |
| 153 | + UserDefaults.backgroundUploadProcess.tasks[taskIdentifier] = taskResult |
| 154 | + return taskResult |
| 155 | + } |
| 156 | + |
| 157 | + @discardableResult |
| 158 | + private func removeTaskResult(with taskIdentifier: Int) -> BackgroundUploadTaskResult? { |
| 159 | + return UserDefaults.backgroundUploadProcess.tasks.removeValue(forKey: taskIdentifier) |
| 160 | + } |
| 161 | +} |
0 commit comments