| 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
 
- #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<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 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 {}
 
 
  |