// // ImageDownloader.swift // Kingfisher // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2018 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 /// Progress update block of downloader. public typealias ImageDownloaderProgressBlock = DownloadProgressBlock /// Completion block of downloader. public typealias ImageDownloaderCompletionHandler = ((_ image: Image?, _ error: NSError?, _ url: URL?, _ originalData: Data?) -> Void) /// Download task. public struct RetrieveImageDownloadTask { let internalTask: URLSessionDataTask /// Downloader by which this task is intialized. public private(set) weak var ownerDownloader: ImageDownloader? /// Cancel this download task. It will trigger the completion handler with an NSURLErrorCancelled error. /// If you want to cancel all downloading tasks, call `cancelAll()` of `ImageDownloader` instance. public func cancel() { ownerDownloader?.cancel(self) } /// The original request URL of this download task. public var url: URL? { return internalTask.originalRequest?.url } /// The relative priority of this download task. /// It represents the `priority` property of the internal `NSURLSessionTask` of this download task. /// The value for it is between 0.0~1.0. Default priority is value of 0.5. /// See documentation on `priority` of `NSURLSessionTask` for more about it. public var priority: Float { get { return internalTask.priority } set { internalTask.priority = newValue } } } ///The code of errors which `ImageDownloader` might encountered. public enum KingfisherError: Int { /// badData: The downloaded data is not an image or the data is corrupted. case badData = 10000 /// notModified: The remote server responsed a 304 code. No image data downloaded. case notModified = 10001 /// The HTTP status code in response is not valid. If an invalid /// code error received, you could check the value under `KingfisherErrorStatusCodeKey` /// in `userInfo` to see the code. case invalidStatusCode = 10002 /// notCached: The image rquested is not in cache but .onlyFromCache is activated. case notCached = 10003 /// The URL is invalid. case invalidURL = 20000 /// The downloading task is cancelled before started. case downloadCancelledBeforeStarting = 30000 } /// Key will be used in the `userInfo` of `.invalidStatusCode` public let KingfisherErrorStatusCodeKey = "statusCode" /// Protocol of `ImageDownloader`. public protocol ImageDownloaderDelegate: class { /** Called when the `ImageDownloader` object successfully downloaded an image from specified URL. - parameter downloader: The `ImageDownloader` object finishes the downloading. - parameter image: Downloaded image. - parameter url: URL of the original request URL. - parameter response: The response object of the downloading process. */ func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?) /** Called when the `ImageDownloader` object starts to download an image from specified URL. - parameter downloader: The `ImageDownloader` object starts the downloading. - parameter url: URL of the original request. - parameter response: The request object of the downloading process. */ func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) /** Check if a received HTTP status code is valid or not. By default, a status code between 200 to 400 (excluded) is considered as valid. If an invalid code is received, the downloader will raise an .invalidStatusCode error. It has a `userInfo` which includes this statusCode and localizedString error message. - parameter code: The received HTTP status code. - parameter downloader: The `ImageDownloader` object asking for validate status code. - returns: Whether this HTTP status code is valid or not. - Note: If the default 200 to 400 valid code does not suit your need, you can implement this method to change that behavior. */ func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool /** Called when the `ImageDownloader` object successfully downloaded image data from specified URL. - parameter downloader: The `ImageDownloader` object finishes data downloading. - parameter data: Downloaded data. - parameter url: URL of the original request URL. - returns: The data from which Kingfisher should use to create an image. - Note: This callback can be used to preprocess raw image data before creation of UIImage instance (i.e. decrypting or verification). */ func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? } extension ImageDownloaderDelegate { public func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?) {} public func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) {} public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool { return (200..<400).contains(code) } public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? { return data } } /// Protocol indicates that an authentication challenge could be handled. public protocol AuthenticationChallengeResponsable: class { /** Called when an session level authentication challenge is received. This method provide a chance to handle and response to the authentication challenge before downloading could start. - parameter downloader: The downloader which receives this challenge. - parameter challenge: An object that contains the request for authentication. - parameter completionHandler: A handler that your delegate method must call. - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionDelegate`. */ func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) /** Called when an session level authentication challenge is received. This method provide a chance to handle and response to the authentication challenge before downloading could start. - parameter downloader: The downloader which receives this challenge. - parameter task: The task whose request requires authentication. - parameter challenge: An object that contains the request for authentication. - parameter completionHandler: A handler that your delegate method must call. - Note: This method is a forward from `URLSessionTaskDelegate.urlSession(:task:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionTaskDelegate`. */ func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) } extension AuthenticationChallengeResponsable { func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) { let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!) completionHandler(.useCredential, credential) return } } completionHandler(.performDefaultHandling, nil) } func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { completionHandler(.performDefaultHandling, nil) } } /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server. open class ImageDownloader { class ImageFetchLoad { var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]() var responseData = NSMutableData() var downloadTaskCount = 0 var downloadTask: RetrieveImageDownloadTask? var cancelSemaphore: DispatchSemaphore? } // MARK: - Public property /// The duration before the download 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 implemention 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 downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly. open var sessionConfiguration = URLSessionConfiguration.ephemeral { didSet { session?.invalidateAndCancel() session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: OperationQueue.main) } } /// Whether the download requests should use pipeling or not. Default is false. open var requestsUsePipelining = false fileprivate let sessionHandler: ImageDownloaderSessionHandler fileprivate var session: URLSession? /// 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? // MARK: - Internal property let barrierQueue: DispatchQueue let processQueue: DispatchQueue let cancelQueue: DispatchQueue typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?) var fetchLoads = [URL: ImageFetchLoad]() // MARK: - Public method /// The default downloader. public static let `default` = ImageDownloader(name: "default") /** Init a downloader with name. - parameter name: The name for the downloader. It should not be empty. - returns: The downloader object. */ 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.") } barrierQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Barrier.\(name)", attributes: .concurrent) processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)", attributes: .concurrent) cancelQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Cancel.\(name)") sessionHandler = ImageDownloaderSessionHandler() // Provide a default implement for challenge responder. authenticationChallengeResponder = sessionHandler session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: .main) } deinit { session?.invalidateAndCancel() } func fetchLoad(for url: URL) -> ImageFetchLoad? { var fetchLoad: ImageFetchLoad? barrierQueue.sync(flags: .barrier) { fetchLoad = fetchLoads[url] } return fetchLoad } /** Download an image with a URL and option. - parameter url: Target URL. - parameter retrieveImageTask: The task to cooporate with cache. Pass `nil` if you are not trying to use downloader and cache. - parameter options: The options could control download behavior. See `KingfisherOptionsInfo`. - parameter progressBlock: Called when the download progress updated. - parameter completionHandler: Called when the download progress finishes. - returns: A downloading task. You could call `cancel` on it to stop the downloading process. */ @discardableResult open func downloadImage(with url: URL, retrieveImageTask: RetrieveImageTask? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: ImageDownloaderProgressBlock? = nil, completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask? { if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting { completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil) return nil } let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout) request.httpShouldUsePipelining = requestsUsePipelining if let modifier = options?.modifier { guard let r = modifier.modified(for: request) else { completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil) return nil } request = r } // There is a possiblility that request modifier changed the url to `nil` or empty. guard let url = request.url, !url.absoluteString.isEmpty else { completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil) return nil } var downloadTask: RetrieveImageDownloadTask? setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in if fetchLoad.downloadTask == nil { let dataTask = session.dataTask(with: request) fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self) dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority dataTask.resume() self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) // Hold self while the task is executing. self.sessionHandler.downloadHolder = self } fetchLoad.downloadTaskCount += 1 downloadTask = fetchLoad.downloadTask retrieveImageTask?.downloadTask = downloadTask } return downloadTask } } // MARK: - Download method extension ImageDownloader { // A single key may have multiple callbacks. Only download once. func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) { func prepareFetchLoad() { barrierQueue.sync(flags: .barrier) { let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad() let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler) loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo)) fetchLoads[url] = loadObjectForURL if let session = session { started(session, loadObjectForURL) } } } if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 { if fetchLoad.cancelSemaphore == nil { fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0) } cancelQueue.async { _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture) fetchLoad.cancelSemaphore = nil prepareFetchLoad() } } else { prepareFetchLoad() } } private func cancelTaskImpl(_ task: RetrieveImageDownloadTask, fetchLoad: ImageFetchLoad? = nil, ignoreTaskCount: Bool = false) { func getFetchLoad(from task: RetrieveImageDownloadTask) -> ImageFetchLoad? { guard let URL = task.internalTask.originalRequest?.url, let imageFetchLoad = self.fetchLoads[URL] else { return nil } return imageFetchLoad } guard let imageFetchLoad = fetchLoad ?? getFetchLoad(from: task) else { return } imageFetchLoad.downloadTaskCount -= 1 if ignoreTaskCount || imageFetchLoad.downloadTaskCount == 0 { task.internalTask.cancel() } } func cancel(_ task: RetrieveImageDownloadTask) { barrierQueue.sync(flags: .barrier) { cancelTaskImpl(task) } } /// Cancel all downloading tasks. It will trigger the completion handlers for all not-yet-finished /// downloading tasks with an NSURLErrorCancelled error. /// /// If you need to only cancel a certain task, call `cancel()` on the `RetrieveImageDownloadTask` /// returned by the downloading methods. public func cancelAll() { barrierQueue.sync(flags: .barrier) { fetchLoads.forEach { v in let fetchLoad = v.value guard let task = fetchLoad.downloadTask else { return } cancelTaskImpl(task, fetchLoad: fetchLoad, ignoreTaskCount: true) } } } } // MARK: - NSURLSessionDataDelegate /// Delegate class for `NSURLSessionTaskDelegate`. /// The session object will hold its delegate until it gets invalidated. /// If we use `ImageDownloader` as the session delegate, it will not be released. /// So we need an additional handler to break the retain cycle. // See https://github.com/onevcat/Kingfisher/issues/235 final class ImageDownloaderSessionHandler: NSObject, URLSessionDataDelegate, AuthenticationChallengeResponsable { // The holder will keep downloader not released while a data task is being executed. // It will be set when the task started, and reset when the task finished. var downloadHolder: ImageDownloader? func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { guard let downloader = downloadHolder else { completionHandler(.cancel) return } if let statusCode = (response as? HTTPURLResponse)?.statusCode, let url = dataTask.originalRequest?.url, !(downloader.delegate ?? downloader).isValidStatusCode(statusCode, for: downloader) { let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidStatusCode.rawValue, userInfo: [KingfisherErrorStatusCodeKey: statusCode, NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)]) callCompletionHandlerFailure(error: error, url: url) } completionHandler(.allow) } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { guard let downloader = downloadHolder else { return } if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) { fetchLoad.responseData.append(data) if let expectedLength = dataTask.response?.expectedContentLength { for content in fetchLoad.contents { DispatchQueue.main.async { content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength) } } } } } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { guard let url = task.originalRequest?.url else { return } guard error == nil else { callCompletionHandlerFailure(error: error!, url: url) return } processImage(for: task, url: url) } /** This method is exposed since the compiler requests. Do not call it. */ func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard let downloader = downloadHolder else { return } downloader.authenticationChallengeResponder?.downloader(downloader, didReceive: challenge, completionHandler: completionHandler) } func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard let downloader = downloadHolder else { return } downloader.authenticationChallengeResponder?.downloader(downloader, task: task, didReceive: challenge, completionHandler: completionHandler) } private func cleanFetchLoad(for url: URL) { guard let downloader = downloadHolder else { return } downloader.barrierQueue.sync(flags: .barrier) { downloader.fetchLoads.removeValue(forKey: url) if downloader.fetchLoads.isEmpty { downloadHolder = nil } } } private func callCompletionHandlerFailure(error: Error, url: URL) { guard let downloader = downloadHolder, let fetchLoad = downloader.fetchLoad(for: url) else { return } // We need to clean the fetch load first, before actually calling completion handler. cleanFetchLoad(for: url) var leftSignal: Int repeat { leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0 } while leftSignal != 0 for content in fetchLoad.contents { content.options.callbackDispatchQueue.safeAsync { content.callback.completionHandler?(nil, error as NSError, url, nil) } } } private func processImage(for task: URLSessionTask, url: URL) { guard let downloader = downloadHolder else { return } // We are on main queue when receiving this. downloader.processQueue.async { guard let fetchLoad = downloader.fetchLoad(for: url) else { return } self.cleanFetchLoad(for: url) let data: Data? let fetchedData = fetchLoad.responseData as Data if let delegate = downloader.delegate { data = delegate.imageDownloader(downloader, didDownload: fetchedData, for: url) } else { data = fetchedData } // Cache the processed images. So we do not need to re-process the image if using the same processor. // Key is the identifier of processor. var imageCache: [String: Image] = [:] for content in fetchLoad.contents { let options = content.options let completionHandler = content.callback.completionHandler let callbackQueue = options.callbackDispatchQueue let processor = options.processor var image = imageCache[processor.identifier] if let data = data, image == nil { image = processor.process(item: .data(data), options: options) // Add the processed image to cache. // If `image` is nil, nothing will happen (since the key is not existing before). imageCache[processor.identifier] = image } if let image = image { downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response) let imageModifier = options.imageModifier let finalImage = imageModifier.modify(image) if options.backgroundDecode { let decodedImage = finalImage.kf.decoded callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) } } else { callbackQueue.safeAsync { completionHandler?(finalImage, nil, url, data) } } } else { if let res = task.response as? HTTPURLResponse , res.statusCode == 304 { let notModified = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil) completionHandler?(nil, notModified, url, nil) continue } let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil) callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) } } } } } } // Placeholder. For retrieving extension methods of ImageDownloaderDelegate extension ImageDownloader: ImageDownloaderDelegate {}