RadrootsAppleBackgroundURLSessionDelegate.swift (10600B)
1 import Foundation 2 3 #if os(iOS) 4 final class RadrootsTransferSessionDelegate: NSObject, 5 URLSessionDownloadDelegate, URLSessionDataDelegate, 6 URLSessionTaskDelegate, @unchecked Sendable { 7 private let coordinator: RadrootsTransferCoordinator 8 private let downloadStagingRoot: URL 9 private let fileManager: FileManager 10 private let lock = NSLock() 11 private var stagedDownloadResultsByTaskIdentifier: [Int: RadrootsStagedBackgroundDownloadResult] 12 let callbacks = RadrootsTransferCallbackQueue() 13 private let responses = RadrootsTransferResponseCollector() 14 15 init( 16 coordinator: RadrootsTransferCoordinator, downloadStagingRoot: URL, 17 fileManager: FileManager 18 ) { 19 self.coordinator = coordinator 20 self.downloadStagingRoot = downloadStagingRoot 21 self.fileManager = fileManager 22 stagedDownloadResultsByTaskIdentifier = [:] 23 } 24 25 func urlSession( 26 _: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL 27 ) { 28 guard let identifier = transferIdentifier(from: downloadTask) else { return } 29 let result: RadrootsStagedBackgroundDownloadResult 30 do { 31 guard 32 RadrootsNativeDestinationPolicy.responseMatches( 33 downloadTask.response?.url, original: downloadTask.originalRequest, 34 current: downloadTask.currentRequest 35 ), 36 let descriptor = RadrootsBackgroundURLTaskDescriptor( 37 taskDescription: downloadTask.taskDescription 38 ), 39 let fileSize = try location.resourceValues(forKeys: [.fileSizeKey]).fileSize, 40 fileSize >= 0, 41 UInt64(fileSize) <= descriptor.maximumTransferBytes 42 else { 43 throw RadrootsBackgroundTransferError.transferFailure 44 } 45 try fileManager.createDirectory(at: downloadStagingRoot, withIntermediateDirectories: true) 46 let destination = downloadStagingRoot.appendingPathComponent( 47 "\(identifier.rawValue)-\(downloadTask.taskIdentifier).download" 48 ).standardizedFileURL 49 if fileManager.fileExists(atPath: destination.path) { 50 try fileManager.removeItem(at: destination) 51 } 52 try fileManager.moveItem(at: location, to: destination) 53 try fileManager.setAttributes( 54 [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], 55 ofItemAtPath: destination.path 56 ) 57 result = .file(destination) 58 } catch { 59 result = .failure 60 } 61 recordDownloadResult(result, taskIdentifier: downloadTask.taskIdentifier) 62 } 63 64 func urlSession( 65 _: URLSession, downloadTask: URLSessionDownloadTask, didWriteData _: Int64, 66 totalBytesWritten: Int64, 67 totalBytesExpectedToWrite: Int64 68 ) { 69 guard let identifier = transferIdentifier(from: downloadTask) else { return } 70 if exceedsTransferLimit( 71 task: downloadTask, 72 bytesTransferred: totalBytesWritten, 73 totalBytesExpected: totalBytesExpectedToWrite 74 ) { 75 downloadTask.cancel() 76 return 77 } 78 Task { 79 await coordinator.updateProgress( 80 identifier: identifier, bytesTransferred: totalBytesWritten, 81 totalBytesExpected: Self.expectedByteCount(totalBytesExpectedToWrite), 82 executionID: RadrootsBackgroundURLTaskDescriptor(taskDescription: downloadTask.taskDescription)? 83 .executionID 84 ) 85 } 86 } 87 88 func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 89 let shouldCancel = responses.append(data, taskIdentifier: dataTask.taskIdentifier, 90 fallbackLimit: responseLimit(dataTask)) 91 if shouldCancel { 92 dataTask.cancel() 93 } 94 } 95 96 func urlSession( 97 _: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, 98 completionHandler: @escaping (URLSession.ResponseDisposition) -> Void 99 ) { 100 let accepted = RadrootsNativeDestinationPolicy.responseMatches( 101 response.url, original: dataTask.originalRequest, current: dataTask.currentRequest 102 ) && responses.begin( 103 response, 104 taskIdentifier: dataTask.taskIdentifier, 105 fallbackLimit: responseLimit(dataTask) 106 ) 107 completionHandler(accepted ? .allow : .cancel) 108 } 109 110 func urlSession( 111 _: URLSession, task: URLSessionTask, didSendBodyData _: Int64, totalBytesSent: Int64, 112 totalBytesExpectedToSend: Int64 113 ) { 114 guard let identifier = transferIdentifier(from: task) else { return } 115 if exceedsTransferLimit( 116 task: task, bytesTransferred: totalBytesSent, totalBytesExpected: totalBytesExpectedToSend 117 ) { 118 task.cancel() 119 return 120 } 121 Task { 122 await coordinator.updateProgress( 123 identifier: identifier, bytesTransferred: totalBytesSent, 124 totalBytesExpected: Self.expectedByteCount(totalBytesExpectedToSend), 125 executionID: RadrootsBackgroundURLTaskDescriptor(taskDescription: task.taskDescription)?.executionID 126 ) 127 } 128 } 129 130 func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 131 let bytesTransferred = max(max(task.countOfBytesReceived, task.countOfBytesSent), 0) 132 let expected = Self.expectedByteCount( 133 max(task.countOfBytesExpectedToReceive, task.countOfBytesExpectedToSend) 134 ) 135 let stagedDownloadResult = takeDownloadResult(taskIdentifier: task.taskIdentifier) 136 let httpResult = takeHTTPResult(for: task) 137 guard let identifier = transferIdentifier(from: task) else { 138 if case let .file(url) = stagedDownloadResult { 139 try? fileManager.removeItem(at: url) 140 } 141 return 142 } 143 callbacks.enqueue(receipt: identifier) { [coordinator] in 144 await coordinator.complete( 145 identifier: identifier, 146 completion: RadrootsTransferCompletion(platformError: error, 147 stagedDownloadResult: stagedDownloadResult, 148 httpResult: httpResult, 149 bytesTransferred: bytesTransferred, 150 totalBytesExpected: expected), 151 executionID: RadrootsBackgroundURLTaskDescriptor(taskDescription: task.taskDescription)?.executionID 152 ) 153 await coordinator.releaseUploadLease( 154 executionID: RadrootsBackgroundURLTaskDescriptor(taskDescription: task.taskDescription)?.executionID 155 ) 156 } 157 } 158 159 func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { 160 callbacks.enqueue { [coordinator] in 161 await coordinator.finishBackgroundEvents(identifier: session.configuration.identifier) 162 } 163 } 164 165 func urlSession( 166 _: URLSession, 167 task _: URLSessionTask, 168 willPerformHTTPRedirection _: HTTPURLResponse, 169 newRequest _: URLRequest, 170 completionHandler: @escaping (URLRequest?) -> Void 171 ) { 172 completionHandler(nil) 173 } 174 175 private func recordDownloadResult( 176 _ result: RadrootsStagedBackgroundDownloadResult, taskIdentifier: Int 177 ) { 178 lock.lock() 179 defer { lock.unlock() } 180 stagedDownloadResultsByTaskIdentifier[taskIdentifier] = result 181 } 182 183 private func takeDownloadResult(taskIdentifier: Int) -> RadrootsStagedBackgroundDownloadResult? { 184 lock.lock() 185 defer { lock.unlock() } 186 return stagedDownloadResultsByTaskIdentifier.removeValue(forKey: taskIdentifier) 187 } 188 189 func registerResponseBodyLimit(_ limit: Int, taskIdentifier: Int) { 190 responses.register(limit, taskIdentifier: taskIdentifier) 191 } 192 193 private func responseLimit(_ task: URLSessionTask) -> Int { 194 RadrootsBackgroundURLTaskDescriptor(taskDescription: task.taskDescription)? 195 .maximumResponseBodyBytes ?? 65536 196 } 197 198 private func takeHTTPResult(for task: URLSessionTask) -> RadrootsBackgroundHTTPResult { 199 responses.take(taskIdentifier: task.taskIdentifier, response: task.response as? HTTPURLResponse, 200 destinationMismatch: task.response != nil && !RadrootsNativeDestinationPolicy 201 .responseMatches( 202 task.response?.url, 203 original: task.originalRequest, 204 current: task.currentRequest 205 )) 206 } 207 208 private func transferIdentifier(from task: URLSessionTask) 209 -> RadrootsBackgroundTransferIdentifier? { 210 RadrootsBackgroundURLTaskDescriptor(taskDescription: task.taskDescription)?.identifier 211 } 212 213 private func exceedsTransferLimit( 214 task: URLSessionTask, bytesTransferred: Int64, totalBytesExpected: Int64 215 ) -> Bool { 216 guard 217 let descriptor = RadrootsBackgroundURLTaskDescriptor(taskDescription: task.taskDescription) 218 else { return true } 219 return bytesTransferred > 0 && UInt64(bytesTransferred) > descriptor.maximumTransferBytes 220 || totalBytesExpected > 0 && UInt64(totalBytesExpected) > descriptor.maximumTransferBytes 221 } 222 223 private static func expectedByteCount(_ value: Int64) -> Int64? { 224 value >= 0 ? value : nil 225 } 226 } 227 #endif