| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 | ////  ImageDownloader.swift//  Kingfisher////  Created by Wei Wang on 15/4/6.////  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>////  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#elseimport 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<String>?        /// 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<ImageLoadingResult, KingfisherError>) -> 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<Result<ImageLoadingResult, KingfisherError>, Void> in            let delegate =  Delegate<Result<ImageLoadingResult, KingfisherError>, 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<Image>, 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<ImageLoadingResult, KingfisherError>) -> 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 Taskextension 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 {}
 |