Music_Player3/Pods/Tiercel/Sources/General/DownloadTask.swift
2024-06-03 09:48:39 +08:00

607 lines
21 KiB
Swift

//
// DownloadTask.swift
// Tiercel
//
// Created by Daniels on 2018/3/16.
// Copyright © 2018 Daniels. All rights reserved.
//
// 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 UIKit
public class DownloadTask: Task<DownloadTask> {
private enum CodingKeys: CodingKey {
case resumeData
case response
}
private var acceptableStatusCodes: Range<Int> { return 200..<300 }
private var _sessionTask: URLSessionDownloadTask? {
willSet {
_sessionTask?.removeObserver(self, forKeyPath: "currentRequest")
}
didSet {
_sessionTask?.addObserver(self, forKeyPath: "currentRequest", options: [.new], context: nil)
}
}
internal var sessionTask: URLSessionDownloadTask? {
get { protectedDownloadState.read { _ in _sessionTask }}
set { protectedDownloadState.write { _ in _sessionTask = newValue }}
}
public private(set) var response: HTTPURLResponse? {
get { protectedDownloadState.wrappedValue.response }
set { protectedDownloadState.write { $0.response = newValue } }
}
public var filePath: String {
return cache.filePath(fileName: fileName)!
}
public var pathExtension: String? {
let pathExtension = (filePath as NSString).pathExtension
return pathExtension.isEmpty ? nil : pathExtension
}
private struct DownloadState {
var resumeData: Data? {
didSet {
guard let resumeData = resumeData else { return }
tmpFileName = ResumeDataHelper.getTmpFileName(resumeData)
}
}
var response: HTTPURLResponse?
var tmpFileName: String?
var shouldValidateFile: Bool = false
}
private let protectedDownloadState: Protected<DownloadState> = Protected(DownloadState())
private var resumeData: Data? {
get { protectedDownloadState.wrappedValue.resumeData }
set { protectedDownloadState.write { $0.resumeData = newValue } }
}
internal var tmpFileName: String? {
protectedDownloadState.wrappedValue.tmpFileName
}
private var shouldValidateFile: Bool {
get { protectedDownloadState.wrappedValue.shouldValidateFile }
set { protectedDownloadState.write { $0.shouldValidateFile = newValue } }
}
internal init(_ url: URL,
headers: [String: String]? = nil,
fileName: String? = nil,
cache: Cache,
operationQueue: DispatchQueue) {
super.init(url,
headers: headers,
cache: cache,
operationQueue: operationQueue)
if let fileName = fileName, !fileName.isEmpty {
self.fileName = fileName
}
NotificationCenter.default.addObserver(self,
selector: #selector(fixDelegateMethodError),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}
public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let superEncoder = container.superEncoder()
try super.encode(to: superEncoder)
try container.encodeIfPresent(resumeData, forKey: .resumeData)
if let response = response {
let responseData: Data
if #available(iOS 11.0, *) {
responseData = try NSKeyedArchiver.archivedData(withRootObject: (response as HTTPURLResponse), requiringSecureCoding: true)
} else {
responseData = NSKeyedArchiver.archivedData(withRootObject: (response as HTTPURLResponse))
}
try container.encode(responseData, forKey: .response)
}
}
internal required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let superDecoder = try container.superDecoder()
try super.init(from: superDecoder)
resumeData = try container.decodeIfPresent(Data.self, forKey: .resumeData)
if let responseData = try container.decodeIfPresent(Data.self, forKey: .response) {
if #available(iOS 11.0, *) {
response = try? NSKeyedUnarchiver.unarchivedObject(ofClass: HTTPURLResponse.self, from: responseData)
} else {
response = NSKeyedUnarchiver.unarchiveObject(with: responseData) as? HTTPURLResponse
}
}
}
deinit {
sessionTask?.removeObserver(self, forKeyPath: "currentRequest")
NotificationCenter.default.removeObserver(self)
}
@objc private func fixDelegateMethodError() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.sessionTask?.suspend()
self.sessionTask?.resume()
}
}
internal override func execute(_ executer: Executer<DownloadTask>?) {
executer?.execute(self)
}
}
// MARK: - control
extension DownloadTask {
internal func download() {
cache.createDirectory()
guard let manager = manager else { return }
switch status {
case .waiting, .suspended, .failed:
if cache.fileExists(fileName: fileName) {
prepareForDownload(fileExists: true)
} else {
if manager.shouldRun {
prepareForDownload(fileExists: false)
} else {
status = .waiting
progressExecuter?.execute(self)
executeControl()
}
}
case .succeeded:
executeControl()
succeeded(fromRunning: false, immediately: false)
case .running:
status = .running
executeControl()
default: break
}
}
private func prepareForDownload(fileExists: Bool) {
status = .running
protectedState.write {
$0.speed = 0
if $0.startDate == 0 {
$0.startDate = Date().timeIntervalSince1970
}
}
error = nil
response = nil
start(fileExists: fileExists)
}
private func start(fileExists: Bool) {
if fileExists {
manager?.log(.downloadTask("file already exists", task: self))
if let fileInfo = try? FileManager.default.attributesOfItem(atPath: cache.filePath(fileName: fileName)!),
let length = fileInfo[.size] as? Int64 {
progress.totalUnitCount = length
}
executeControl()
operationQueue.async {
self.didComplete(.local)
}
} else {
if let resumeData = resumeData,
cache.retrieveTmpFile(tmpFileName) {
if #available(iOS 10.2, *) {
sessionTask = session?.downloadTask(withResumeData: resumeData)
} else if #available(iOS 10.0, *) {
sessionTask = session?.correctedDownloadTask(withResumeData: resumeData)
} else {
sessionTask = session?.downloadTask(withResumeData: resumeData)
}
} else {
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 0)
if let headers = headers {
request.allHTTPHeaderFields = headers
}
sessionTask = session?.downloadTask(with: request)
progress.completedUnitCount = 0
progress.totalUnitCount = 0
}
progress.setUserInfoObject(progress.completedUnitCount, forKey: .fileCompletedCountKey)
sessionTask?.resume()
manager?.maintainTasks(with: .appendRunningTasks(self))
manager?.storeTasks()
executeControl()
}
}
internal func suspend(onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
guard status == .running || status == .waiting else { return }
controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
if status == .running {
status = .willSuspend
sessionTask?.cancel(byProducingResumeData: { _ in })
} else {
status = .willSuspend
operationQueue.async {
self.didComplete(.local)
}
}
}
internal func cancel(onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
guard status != .succeeded else { return }
controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
if status == .running {
status = .willCancel
sessionTask?.cancel()
} else {
status = .willCancel
operationQueue.async {
self.didComplete(.local)
}
}
}
internal func remove(completely: Bool = false, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
isRemoveCompletely = completely
controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
if status == .running {
status = .willRemove
sessionTask?.cancel()
} else {
status = .willRemove
operationQueue.async {
self.didComplete(.local)
}
}
}
internal func update(_ newHeaders: [String: String]? = nil, newFileName: String? = nil) {
headers = newHeaders
if let newFileName = newFileName, !newFileName.isEmpty {
cache.updateFileName(filePath, newFileName)
fileName = newFileName
}
}
private func validateFile() {
guard let validateHandler = self.validateExecuter else { return }
if !shouldValidateFile {
validateHandler.execute(self)
return
}
guard let verificationCode = verificationCode else { return }
FileChecksumHelper.validateFile(filePath, code: verificationCode, type: verificationType) { [weak self] (result) in
guard let self = self else { return }
self.shouldValidateFile = false
if case let .failure(error) = result {
self.validation = .incorrect
self.manager?.log(.error("file validation failed, url: \(self.url)", error: error))
} else {
self.validation = .correct
self.manager?.log(.downloadTask("file validation successful", task: self))
}
self.manager?.storeTasks()
validateHandler.execute(self)
}
}
}
// MARK: - status handle
extension DownloadTask {
private func didCancelOrRemove() {
//
if status == .willCancel {
status = .canceled
}
if status == .willRemove {
status = .removed
}
cache.remove(self, completely: isRemoveCompletely)
manager?.didCancelOrRemove(self)
}
internal func succeeded(fromRunning: Bool, immediately: Bool) {
if endDate == 0 {
protectedState.write {
$0.endDate = Date().timeIntervalSince1970
$0.timeRemaining = 0
}
}
status = .succeeded
progress.completedUnitCount = progress.totalUnitCount
progressExecuter?.execute(self)
if immediately {
executeCompletion(true)
}
validateFile()
manager?.maintainTasks(with: .succeeded(self))
manager?.determineStatus(fromRunningTask: fromRunning)
}
private func determineStatus(with interruptType: InterruptType) {
var fromRunning = true
switch interruptType {
case let .error(error):
self.error = error
var tempStatus = status
if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
self.resumeData = ResumeDataHelper.handleResumeData(resumeData)
cache.storeTmpFile(tmpFileName)
}
if let _ = (error as NSError).userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? Int {
tempStatus = .suspended
}
if let urlError = error as? URLError, urlError.code != URLError.cancelled {
tempStatus = .failed
}
status = tempStatus
case let .statusCode(statusCode):
self.error = TiercelError.unacceptableStatusCode(code: statusCode)
status = .failed
case let .manual(fromRunningTask):
fromRunning = fromRunningTask
}
switch status {
case .willSuspend:
status = .suspended
progressExecuter?.execute(self)
executeControl()
executeCompletion(false)
case .willCancel, .willRemove:
didCancelOrRemove()
executeControl()
executeCompletion(false)
case .suspended, .failed:
progressExecuter?.execute(self)
executeCompletion(false)
default:
status = .failed
progressExecuter?.execute(self)
executeCompletion(false)
}
manager?.determineStatus(fromRunningTask: fromRunning)
}
}
// MARK: - closure
extension DownloadTask {
@discardableResult
public func validateFile(code: String,
type: FileChecksumHelper.VerificationType,
onMainQueue: Bool = true,
handler: @escaping Handler<DownloadTask>) -> Self {
operationQueue.async {
let (verificationCode, verificationType) = self.protectedState.read {
($0.verificationCode, $0.verificationType)
}
if verificationCode == code &&
verificationType == type &&
self.validation != .unkown {
self.shouldValidateFile = false
} else {
self.shouldValidateFile = true
self.protectedState.write {
$0.verificationCode = code
$0.verificationType = type
}
self.manager?.storeTasks()
}
self.validateExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
if self.status == .succeeded {
self.validateFile()
}
}
return self
}
private func executeCompletion(_ isSucceeded: Bool) {
if let completionExecuter = completionExecuter {
completionExecuter.execute(self)
} else if isSucceeded {
successExecuter?.execute(self)
} else {
failureExecuter?.execute(self)
}
NotificationCenter.default.postNotification(name: DownloadTask.didCompleteNotification, downloadTask: self)
}
private func executeControl() {
controlExecuter?.execute(self)
controlExecuter = nil
}
}
// MARK: - KVO
extension DownloadTask {
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let change = change, let newRequest = change[NSKeyValueChangeKey.newKey] as? URLRequest, let url = newRequest.url {
currentURL = url
manager?.updateUrlMapper(with: self)
}
}
}
// MARK: - info
extension DownloadTask {
internal func updateSpeedAndTimeRemaining() {
let dataCount = progress.completedUnitCount
let lastData: Int64 = progress.userInfo[.fileCompletedCountKey] as? Int64 ?? 0
if dataCount > lastData {
let speed = dataCount - lastData
updateTimeRemaining(speed)
}
progress.setUserInfoObject(dataCount, forKey: .fileCompletedCountKey)
}
private func updateTimeRemaining(_ speed: Int64) {
var timeRemaining: Double
if speed != 0 {
timeRemaining = (Double(progress.totalUnitCount) - Double(progress.completedUnitCount)) / Double(speed)
if timeRemaining >= 0.8 && timeRemaining < 1 {
timeRemaining += 1
}
} else {
timeRemaining = 0
}
protectedState.write {
$0.speed = speed
$0.timeRemaining = Int64(timeRemaining)
}
}
}
// MARK: - callback
extension DownloadTask {
internal func didWriteData(downloadTask: URLSessionDownloadTask, bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
progress.completedUnitCount = totalBytesWritten
progress.totalUnitCount = totalBytesExpectedToWrite
response = downloadTask.response as? HTTPURLResponse
progressExecuter?.execute(self)
manager?.updateProgress()
NotificationCenter.default.postNotification(name: DownloadTask.runningNotification, downloadTask: self)
}
internal func didFinishDownloading(task: URLSessionDownloadTask, to location: URL) {
guard let statusCode = (task.response as? HTTPURLResponse)?.statusCode,
acceptableStatusCodes.contains(statusCode)
else { return }
cache.storeFile(at: location, to: URL(fileURLWithPath: filePath))
cache.removeTmpFile(tmpFileName)
}
internal func didComplete(_ type: CompletionType) {
switch type {
case .local:
switch status {
case .willSuspend,.willCancel, .willRemove:
determineStatus(with: .manual(false))
case .running:
succeeded(fromRunning: false, immediately: true)
default:
return
}
case let .network(task, error):
manager?.maintainTasks(with: .removeRunningTasks(self))
sessionTask = nil
switch status {
case .willCancel, .willRemove:
determineStatus(with: .manual(true))
return
case .willSuspend, .running:
progress.totalUnitCount = task.countOfBytesExpectedToReceive
progress.completedUnitCount = task.countOfBytesReceived
progress.setUserInfoObject(task.countOfBytesReceived, forKey: .fileCompletedCountKey)
let statusCode = (task.response as? HTTPURLResponse)?.statusCode ?? -1
let isAcceptable = acceptableStatusCodes.contains(statusCode)
if error != nil {
response = task.response as? HTTPURLResponse
determineStatus(with: .error(error!))
} else if !isAcceptable {
response = task.response as? HTTPURLResponse
determineStatus(with: .statusCode(statusCode))
} else {
resumeData = nil
succeeded(fromRunning: true, immediately: true)
}
default:
return
}
}
}
}
extension Array where Element == DownloadTask {
@discardableResult
public func progress(onMainQueue: Bool = true, handler: @escaping Handler<DownloadTask>) -> [Element] {
self.forEach { $0.progress(onMainQueue: onMainQueue, handler: handler) }
return self
}
@discardableResult
public func success(onMainQueue: Bool = true, handler: @escaping Handler<DownloadTask>) -> [Element] {
self.forEach { $0.success(onMainQueue: onMainQueue, handler: handler) }
return self
}
@discardableResult
public func failure(onMainQueue: Bool = true, handler: @escaping Handler<DownloadTask>) -> [Element] {
self.forEach { $0.failure(onMainQueue: onMainQueue, handler: handler) }
return self
}
public func validateFile(codes: [String],
type: FileChecksumHelper.VerificationType,
onMainQueue: Bool = true,
handler: @escaping Handler<DownloadTask>) -> [Element] {
for (index, task) in self.enumerated() {
guard let code = codes.safeObject(at: index) else { continue }
task.validateFile(code: code, type: type, onMainQueue: onMainQueue, handler: handler)
}
return self
}
}