// // ImageDownloader.swift // Kingfisher // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // 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. #if os(macOS) import AppKit #else import UIKit #endif /// Represents a success result of an image downloading progress. public struct ImageLoadingResult { /// The downloaded image. public let image: KFCrossPlatformImage /// Original URL of the image request. public let url: URL? /// The raw data received from downloader. public let originalData: Data } /// Represents a task of an image downloading process. public struct DownloadTask { /// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer /// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task /// for the same URL resource at the same time. /// /// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through. /// You can use them to identify the cancelled task. public let sessionTask: SessionDataTask /// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled. /// To cancel a `DownloadTask`, use `cancel` instead. public let cancelToken: SessionDataTask.CancelToken /// Cancel this task if it is running. It will do nothing if this task is not running. /// /// - Note: /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created /// and returned when you call related methods, but it will share the session downloading task with a previous task. /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask` /// does not affect other `DownloadTask`s. /// /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`. public func cancel() { sessionTask.cancel(token: cancelToken) } } extension DownloadTask { enum WrappedTask { case download(DownloadTask) case dataProviding func cancel() { switch self { case .download(let task): task.cancel() case .dataProviding: break } } var value: DownloadTask? { switch self { case .download(let task): return task case .dataProviding: return nil } } } } /// Represents a downloading manager for requesting the image with a URL from server. open class ImageDownloader { // MARK: Singleton /// The default downloader. public static let `default` = ImageDownloader(name: "default") // MARK: Public Properties /// The duration before the downloading is timeout. Default is 15 seconds. open var downloadTimeout: TimeInterval = 15.0 /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't /// specify the `authenticationChallengeResponder`. /// /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of /// `authenticationChallengeResponder` will be used instead. open var trustedHosts: Set? /// Use this to set supply a configuration for the downloader. By default, /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. /// /// You could change the configuration before a downloading task starts. /// A configuration without persistent storage for caches is requested for downloader working correctly. open var sessionConfiguration = URLSessionConfiguration.ephemeral { didSet { session.invalidateAndCancel() session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) } } /// Whether the download requests should use pipeline or not. Default is false. open var requestsUsePipelining = false /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more. open weak var delegate: ImageDownloaderDelegate? /// A responder for authentication challenge. /// Downloader will forward the received authentication challenge for the downloading session to this responder. open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable? private let name: String private let sessionDelegate: SessionDelegate private var session: URLSession // MARK: Initializers /// Creates a downloader with name. /// /// - Parameter name: The name for the downloader. It should not be empty. public init(name: String) { if name.isEmpty { fatalError("[Kingfisher] You should specify a name for the downloader. " + "A downloader with empty name is not permitted.") } self.name = name sessionDelegate = SessionDelegate() session = URLSession( configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) authenticationChallengeResponder = self setupSessionHandler() } deinit { session.invalidateAndCancel() } private func setupSessionHandler() { sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2) } sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in self.authenticationChallengeResponder?.downloader( self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3) } sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in return (self.delegate ?? self).isValidStatusCode(code, for: self) } sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in let (url, result) = value do { let value = try result.get() self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil) } catch { self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error) } } sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in guard let url = task.task.originalRequest?.url else { return task.mutableData } return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, for: url) } } // MARK: Dowloading Task /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super. /// /// - Parameters: /// - url: Target URL. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue /// defined in `.callbackQueue` in `options` parameter. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. @discardableResult open func downloadImage( with url: URL, options: KingfisherParsedOptionsInfo, completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? { // Creates default request. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout) request.httpShouldUsePipelining = requestsUsePipelining if let requestModifier = options.requestModifier { // Modifies request before sending. guard let r = requestModifier.modified(for: request) else { options.callbackQueue.execute { completionHandler?(.failure(KingfisherError.requestError(reason: .emptyRequest))) } return nil } request = r } // There is a possibility that request modifier changed the url to `nil` or empty. // In this case, throw an error. guard let url = request.url, !url.absoluteString.isEmpty else { options.callbackQueue.execute { completionHandler?(.failure(KingfisherError.requestError(reason: .invalidURL(request: request)))) } return nil } // Wraps `completionHandler` to `onCompleted` respectively. let onCompleted = completionHandler.map { block -> Delegate, Void> in let delegate = Delegate, Void>() delegate.delegate(on: self) { (_, callback) in block(callback) } return delegate } // SessionDataTask.TaskCallback is a wrapper for `onCompleted` and `options` (for processor info) let callback = SessionDataTask.TaskCallback( onCompleted: onCompleted, options: options ) // Ready to start download. Add it to session task manager (`sessionHandler`) let downloadTask: DownloadTask if let existingTask = sessionDelegate.task(for: url) { downloadTask = sessionDelegate.append(existingTask, url: url, callback: callback) } else { let sessionDataTask = session.dataTask(with: request) sessionDataTask.priority = options.downloadPriority downloadTask = sessionDelegate.add(sessionDataTask, url: url, callback: callback) } let sessionTask = downloadTask.sessionTask // Start the session task if not started yet. if !sessionTask.started { sessionTask.onTaskDone.delegate(on: self) { (self, done) in // Underlying downloading finishes. // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback] let (result, callbacks) = done // Before processing the downloaded data. do { let value = try result.get() self.delegate?.imageDownloader( self, didFinishDownloadingImageForURL: url, with: value.1, error: nil ) } catch { self.delegate?.imageDownloader( self, didFinishDownloadingImageForURL: url, with: nil, error: error ) } switch result { // Download finished. Now process the data to an image. case .success(let (data, response)): let processor = ImageDataProcessor( data: data, callbacks: callbacks, processingQueue: options.processingQueue) processor.onImageProcessed.delegate(on: self) { (self, result) in // `onImageProcessed` will be called for `callbacks.count` times, with each // `SessionDataTask.TaskCallback` as the input parameter. // result: Result, callback: SessionDataTask.TaskCallback let (result, callback) = result if let image = try? result.get() { self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response) } let imageResult = result.map { ImageLoadingResult(image: $0, url: url, originalData: data) } let queue = callback.options.callbackQueue queue.execute { callback.onCompleted?.call(imageResult) } } processor.process() case .failure(let error): callbacks.forEach { callback in let queue = callback.options.callbackQueue queue.execute { callback.onCompleted?.call(.failure(error)) } } } } delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) sessionTask.resume() } return downloadTask } /// Downloads an image with a URL and option. /// /// - Parameters: /// - url: Target URL. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue /// defined in `.callbackQueue` in `options` parameter. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. @discardableResult open func downloadImage( with url: URL, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? { var info = KingfisherParsedOptionsInfo(options) if let block = progressBlock { info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } return downloadImage( with: url, options: info, completionHandler: completionHandler) } } // MARK: Cancelling Task extension ImageDownloader { /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers /// for all not-yet-finished downloading tasks. /// /// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask` /// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url, /// use `ImageDownloader.cancel(url:)`. public func cancelAll() { sessionDelegate.cancelAll() } /// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for /// all not-yet-finished downloading tasks for the URL. /// /// - Parameter url: The URL which you want to cancel downloading. public func cancel(url: URL) { sessionDelegate.cancel(url: url) } } // Use the default implementation from extension of `AuthenticationChallengeResponsable`. extension ImageDownloader: AuthenticationChallengeResponsable {} // Use the default implementation from extension of `ImageDownloaderDelegate`. extension ImageDownloader: ImageDownloaderDelegate {}