// // DownloadRequest.swift // // Copyright (c) 2014-2024 Alamofire Software Foundation (http://alamofire.org/) // // 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. // import Foundation /// `Request` subclass which downloads `Data` to a file on disk using `URLSessionDownloadTask`. public final class DownloadRequest: Request, @unchecked Sendable { /// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination /// `URL`. public struct Options: OptionSet, Sendable { /// Specifies that intermediate directories for the destination URL should be created. public static let createIntermediateDirectories = Options(rawValue: 1 << 0) /// Specifies that any previous file at the destination `URL` should be removed. public static let removePreviousFile = Options(rawValue: 1 << 1) public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } } // MARK: Destination /// A closure executed once a `DownloadRequest` has successfully completed in order to determine where to move the /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL /// and the `HTTPURLResponse`, and returns two values: the file URL where the temporary file should be moved and /// the options defining how the file should be moved. /// /// - Note: Downloads from a local `file://` `URL`s do not use the `Destination` closure, as those downloads do not /// return an `HTTPURLResponse`. Instead the file is merely moved within the temporary directory. public typealias Destination = @Sendable (_ temporaryURL: URL, _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) /// Creates a download file destination closure which uses the default file manager to move the temporary file to a /// file URL in the first available directory with the specified search path directory and search path domain mask. /// /// - Parameters: /// - directory: The search path directory. `.documentDirectory` by default. /// - domain: The search path domain mask. `.userDomainMask` by default. /// - options: `DownloadRequest.Options` used when moving the downloaded file to its destination. None by /// default. /// - Returns: The `Destination` closure. public class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory, in domain: FileManager.SearchPathDomainMask = .userDomainMask, options: Options = []) -> Destination { { temporaryURL, response in let directoryURLs = FileManager.default.urls(for: directory, in: domain) let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL return (url, options) } } /// Default `Destination` used by Alamofire to ensure all downloads persist. This `Destination` prepends /// `Alamofire_` to the automatically generated download name and moves it within the temporary directory. Files /// with this destination must be additionally moved if they should survive the system reclamation of temporary /// space. static let defaultDestination: Destination = { url, _ in (defaultDestinationURL(url), []) } /// Default `URL` creation closure. Creates a `URL` in the temporary directory with `Alamofire_` prepended to the /// provided file name. static let defaultDestinationURL: @Sendable (URL) -> URL = { url in let filename = "Alamofire_\(url.lastPathComponent)" let destination = url.deletingLastPathComponent().appendingPathComponent(filename) return destination } // MARK: Downloadable /// Type describing the source used to create the underlying `URLSessionDownloadTask`. public enum Downloadable { /// Download should be started from the `URLRequest` produced by the associated `URLRequestConvertible` value. case request(any URLRequestConvertible) /// Download should be started from the associated resume `Data` value. case resumeData(Data) } // MARK: Mutable State /// Type containing all mutable state for `DownloadRequest` instances. private struct DownloadRequestMutableState { /// Possible resume `Data` produced when cancelling the instance. var resumeData: Data? /// `URL` to which `Data` is being downloaded. var fileURL: URL? } /// Protected mutable state specific to `DownloadRequest`. private let mutableDownloadState = Protected(DownloadRequestMutableState()) /// If the download is resumable and is eventually cancelled or fails, this value may be used to resume the download /// using the `download(resumingWith data:)` API. /// /// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel). public var resumeData: Data? { #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. return mutableDownloadState.resumeData ?? error?.downloadResumeData #else return mutableDownloadState.resumeData #endif } /// If the download is successful, the `URL` where the file was downloaded. public var fileURL: URL? { mutableDownloadState.fileURL } // MARK: Initial State /// `Downloadable` value used for this instance. public let downloadable: Downloadable /// The `Destination` to which the downloaded file is moved. let destination: Destination /// Creates a `DownloadRequest` using the provided parameters. /// /// - Parameters: /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. /// - downloadable: `Downloadable` value used to create `URLSessionDownloadTasks` for the instance. /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets /// `underlyingQueue`, but can be passed another queue from a `Session`. /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request` /// - destination: `Destination` closure used to move the downloaded file to its final location. init(id: UUID = UUID(), downloadable: Downloadable, underlyingQueue: DispatchQueue, serializationQueue: DispatchQueue, eventMonitor: (any EventMonitor)?, interceptor: (any RequestInterceptor)?, delegate: any RequestDelegate, destination: @escaping Destination) { self.downloadable = downloadable self.destination = destination super.init(id: id, underlyingQueue: underlyingQueue, serializationQueue: serializationQueue, eventMonitor: eventMonitor, interceptor: interceptor, delegate: delegate) } override func reset() { super.reset() mutableDownloadState.write { $0.resumeData = nil $0.fileURL = nil } } /// Called when a download has finished. /// /// - Parameters: /// - task: `URLSessionTask` that finished the download. /// - result: `Result` of the automatic move to `destination`. func didFinishDownloading(using task: URLSessionTask, with result: Result) { eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result) switch result { case let .success(url): mutableDownloadState.fileURL = url case let .failure(error): self.error = error } } /// Updates the `downloadProgress` using the provided values. /// /// - Parameters: /// - bytesWritten: Total bytes written so far. /// - totalBytesExpectedToWrite: Total bytes expected to write. func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) { downloadProgress.totalUnitCount = totalBytesExpectedToWrite downloadProgress.completedUnitCount += bytesWritten downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } } override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { session.downloadTask(with: request) } /// Creates a `URLSessionTask` from the provided resume data. /// /// - Parameters: /// - data: `Data` used to resume the download. /// - session: `URLSession` used to create the `URLSessionTask`. /// /// - Returns: The `URLSessionTask` created. public func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask { session.downloadTask(withResumeData: data) } /// Cancels the instance. Once cancelled, a `DownloadRequest` can no longer be resumed or suspended. /// /// - Note: This method will NOT produce resume data. If you wish to cancel and produce resume data, use /// `cancel(producingResumeData:)` or `cancel(byProducingResumeData:)`. /// /// - Returns: The instance. @discardableResult override public func cancel() -> Self { cancel(producingResumeData: false) } /// Cancels the instance, optionally producing resume data. Once cancelled, a `DownloadRequest` can no longer be /// resumed or suspended. /// /// - Note: If `producingResumeData` is `true`, the `resumeData` property will be populated with any resume data, if /// available. /// /// - Returns: The instance. @discardableResult public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self { cancel(optionallyProducingResumeData: shouldProduceResumeData ? { @Sendable _ in } : nil) } /// Cancels the instance while producing resume data. Once cancelled, a `DownloadRequest` can no longer be resumed /// or suspended. /// /// - Note: The resume data passed to the completion handler will also be available on the instance's `resumeData` /// property. /// /// - Parameter completionHandler: The completion handler that is called when the download has been successfully /// cancelled. It is not guaranteed to be called on a particular queue, so you may /// want use an appropriate queue to perform your work. /// /// - Returns: The instance. @preconcurrency @discardableResult public func cancel(byProducingResumeData completionHandler: @Sendable @escaping (_ data: Data?) -> Void) -> Self { cancel(optionallyProducingResumeData: completionHandler) } /// Internal implementation of cancellation that optionally takes a resume data handler. If no handler is passed, /// cancellation is performed without producing resume data. /// /// - Parameter completionHandler: Optional resume data handler. /// /// - Returns: The instance. private func cancel(optionallyProducingResumeData completionHandler: (@Sendable (_ resumeData: Data?) -> Void)?) -> Self { mutableState.write { mutableState in guard mutableState.state.canTransitionTo(.cancelled) else { return } mutableState.state = .cancelled underlyingQueue.async { self.didCancel() } guard let task = mutableState.tasks.last as? URLSessionDownloadTask, task.state != .completed else { underlyingQueue.async { self.finish() } return } if let completionHandler { // Resume to ensure metrics are gathered. task.resume() task.cancel { resumeData in self.mutableDownloadState.resumeData = resumeData self.underlyingQueue.async { self.didCancelTask(task) } completionHandler(resumeData) } } else { // Resume to ensure metrics are gathered. task.resume() task.cancel() self.underlyingQueue.async { self.didCancelTask(task) } } } return self } /// Validates the request, using the specified closure. /// /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. /// /// - Parameter validation: `Validation` closure to validate the response. /// /// - Returns: The instance. @discardableResult public func validate(_ validation: @escaping Validation) -> Self { let validator: () -> Void = { [unowned self] in guard error == nil, let response else { return } let result = validation(request, response, fileURL) if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) } eventMonitor?.request(self, didValidateRequest: request, response: response, fileURL: fileURL, withResult: result) } validators.write { $0.append(validator) } return self } // MARK: - Response Serialization /// Adds a handler to be called once the request has finished. /// /// - Parameters: /// - queue: The queue on which the completion handler is dispatched. `.main` by default. /// - completionHandler: The code to be executed once the request has finished. /// /// - Returns: The request. @preconcurrency @discardableResult public func response(queue: DispatchQueue = .main, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self { appendResponseSerializer { // Start work that should be on the serialization queue. let result = AFResult(value: self.fileURL, error: self.error) // End work that should be on the serialization queue. self.underlyingQueue.async { let response = DownloadResponse(request: self.request, response: self.response, fileURL: self.fileURL, resumeData: self.resumeData, metrics: self.metrics, serializationDuration: 0, result: result) self.eventMonitor?.request(self, didParseResponse: response) self.responseSerializerDidComplete { queue.async { completionHandler(response) } } } } return self } private func _response(queue: DispatchQueue = .main, responseSerializer: Serializer, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self { appendResponseSerializer { // Start work that should be on the serialization queue. let start = ProcessInfo.processInfo.systemUptime let result: AFResult = Result { try responseSerializer.serializeDownload(request: self.request, response: self.response, fileURL: self.fileURL, error: self.error) }.mapError { error in error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) } let end = ProcessInfo.processInfo.systemUptime // End work that should be on the serialization queue. self.underlyingQueue.async { let response = DownloadResponse(request: self.request, response: self.response, fileURL: self.fileURL, resumeData: self.resumeData, metrics: self.metrics, serializationDuration: end - start, result: result) self.eventMonitor?.request(self, didParseResponse: response) guard let serializerError = result.failure, let delegate = self.delegate else { self.responseSerializerDidComplete { queue.async { completionHandler(response) } } return } delegate.retryResult(for: self, dueTo: serializerError) { retryResult in var didComplete: (@Sendable () -> Void)? defer { if let didComplete { self.responseSerializerDidComplete { queue.async { didComplete() } } } } switch retryResult { case .doNotRetry: didComplete = { completionHandler(response) } case let .doNotRetryWithError(retryError): let result: AFResult = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError")) let response = DownloadResponse(request: self.request, response: self.response, fileURL: self.fileURL, resumeData: self.resumeData, metrics: self.metrics, serializationDuration: end - start, result: result) didComplete = { completionHandler(response) } case .retry, .retryWithDelay: delegate.retryRequest(self, withDelay: retryResult.delay) } } } } return self } /// Adds a handler to be called once the request has finished. /// /// - Note: This handler will read the entire downloaded file into memory, use with caution. /// /// - Parameters: /// - queue: The queue on which the completion handler is dispatched. `.main` by default. /// - responseSerializer: The response serializer responsible for serializing the request, response, and data /// contained in the destination `URL`. /// - completionHandler: The code to be executed once the request has finished. /// /// - Returns: The request. @discardableResult public func response(queue: DispatchQueue = .main, responseSerializer: Serializer, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self { _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) } /// Adds a handler to be called once the request has finished. /// /// - Note: This handler will read the entire downloaded file into memory, use with caution. /// /// - Parameters: /// - queue: The queue on which the completion handler is dispatched. `.main` by default. /// - responseSerializer: The response serializer responsible for serializing the request, response, and data /// contained in the destination `URL`. /// - completionHandler: The code to be executed once the request has finished. /// /// - Returns: The request. @discardableResult public func response(queue: DispatchQueue = .main, responseSerializer: Serializer, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self { _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) } /// Adds a handler using a `URLResponseSerializer` to be called once the request is finished. /// /// - Parameters: /// - queue: The queue on which the completion handler is called. `.main` by default. /// - completionHandler: A closure to be executed once the request has finished. /// /// - Returns: The request. @preconcurrency @discardableResult public func responseURL(queue: DispatchQueue = .main, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self { response(queue: queue, responseSerializer: URLResponseSerializer(), completionHandler: completionHandler) } /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. /// /// - Note: This handler will read the entire downloaded file into memory, use with caution. /// /// - Parameters: /// - queue: The queue on which the completion handler is called. `.main` by default. /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the /// `completionHandler`. `PassthroughPreprocessor()` by default. /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. /// - completionHandler: A closure to be executed once the request has finished. /// /// - Returns: The request. @preconcurrency @discardableResult public func responseData(queue: DispatchQueue = .main, dataPreprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self { response(queue: queue, responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, emptyResponseCodes: emptyResponseCodes, emptyRequestMethods: emptyRequestMethods), completionHandler: completionHandler) } /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. /// /// - Note: This handler will read the entire downloaded file into memory, use with caution. /// /// - Parameters: /// - queue: The queue on which the completion handler is dispatched. `.main` by default. /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the /// `completionHandler`. `PassthroughPreprocessor()` by default. /// - encoding: The string encoding. Defaults to `nil`, in which case the encoding will be determined /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. /// - completionHandler: A closure to be executed once the request has finished. /// /// - Returns: The request. @preconcurrency @discardableResult public func responseString(queue: DispatchQueue = .main, dataPreprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, encoding: String.Encoding? = nil, emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self { response(queue: queue, responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, encoding: encoding, emptyResponseCodes: emptyResponseCodes, emptyRequestMethods: emptyRequestMethods), completionHandler: completionHandler) } /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. /// /// - Note: This handler will read the entire downloaded file into memory, use with caution. /// /// - Parameters: /// - queue: The queue on which the completion handler is dispatched. `.main` by default. /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the /// `completionHandler`. `PassthroughPreprocessor()` by default. /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. /// - options: `JSONSerialization.ReadingOptions` used when parsing the response. `.allowFragments` /// by default. /// - completionHandler: A closure to be executed once the request has finished. /// /// - Returns: The request. @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") @preconcurrency @discardableResult public func responseJSON(queue: DispatchQueue = .main, dataPreprocessor: any DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, emptyResponseCodes: Set = JSONResponseSerializer.defaultEmptyResponseCodes, emptyRequestMethods: Set = JSONResponseSerializer.defaultEmptyRequestMethods, options: JSONSerialization.ReadingOptions = .allowFragments, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self { response(queue: queue, responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, emptyResponseCodes: emptyResponseCodes, emptyRequestMethods: emptyRequestMethods, options: options), completionHandler: completionHandler) } /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. /// /// - Note: This handler will read the entire downloaded file into memory, use with caution. /// /// - Parameters: /// - type: `Decodable` type to decode from response data. /// - queue: The queue on which the completion handler is dispatched. `.main` by default. /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the /// `completionHandler`. `PassthroughPreprocessor()` by default. /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. /// - completionHandler: A closure to be executed once the request has finished. /// /// - Returns: The request. @preconcurrency @discardableResult public func responseDecodable(of type: T.Type = T.self, queue: DispatchQueue = .main, dataPreprocessor: any DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, decoder: any DataDecoder = JSONDecoder(), emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods, completionHandler: @Sendable @escaping (AFDownloadResponse) -> Void) -> Self where T: Sendable { response(queue: queue, responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, decoder: decoder, emptyResponseCodes: emptyResponseCodes, emptyRequestMethods: emptyRequestMethods), completionHandler: completionHandler) } }