apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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