Files
Adobe-Downloader/Adobe Downloader/Utils/NewDownloadUtils.swift

1594 lines
67 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// NewDownloadUtils.swift
// Adobe Downloader
//
// Created by X1a0He on 2/26/25.
//
import Foundation
actor AsyncFlag {
private var value: Bool = false
func set() {
value = true
}
func isSet() -> Bool {
return value
}
func reset() {
value = false
}
}
actor AsyncSemaphore {
private var permits: Int
private var waiters: [CheckedContinuation<Void, Never>] = []
init(value: Int) {
self.permits = value
}
func wait() async {
if permits > 0 {
permits -= 1
return
}
await withCheckedContinuation { continuation in
waiters.append(continuation)
}
}
func signal() {
if waiters.isEmpty {
permits += 1
} else {
let waiter = waiters.removeFirst()
waiter.resume()
}
}
}
actor ConcurrentDownloadProgressManager {
private var packageProgresses: [String: Double] = [:]
private var packageSizes: [String: Int64] = [:]
private var packageSpeeds: [String: Double] = [:]
private var totalSize: Int64 = 0
private var lastUpdateTime = Date()
func initialize(packages: [(id: String, size: Int64)]) {
totalSize = packages.reduce(0) { $0 + $1.size }
for package in packages {
packageProgresses[package.id] = 0.0
packageSizes[package.id] = package.size
packageSpeeds[package.id] = 0.0
}
}
func updatePackageProgress(packageId: String, progress: Double, speed: Double = 0.0) {
packageProgresses[packageId] = progress
packageSpeeds[packageId] = speed
}
func markPackageCompleted(packageId: String) {
packageProgresses[packageId] = 1.0
packageSpeeds[packageId] = 0.0
}
func getTotalProgress() -> (progress: Double, downloadedSize: Int64, totalSpeed: Double) {
let totalDownloaded = packageProgresses.reduce(Int64(0)) { sum, item in
let size = packageSizes[item.key] ?? 0
return sum + Int64(Double(size) * item.value)
}
let totalProgress = totalSize > 0 ? Double(totalDownloaded) / Double(totalSize) : 0
let totalSpeed = packageSpeeds.values.reduce(0, +)
return (totalProgress, totalDownloaded, totalSpeed)
}
func isAllCompleted() -> Bool {
return packageProgresses.allSatisfy { $0.value >= 1.0 }
}
}
class NewDownloadUtils {
private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
var completionHandler: (URL?, URLResponse?, Error?) -> Void
var progressHandler: ((Int64, Int64, Int64) -> Void)?
var destinationDirectory: URL
var fileName: String
private var hasCompleted = false
private let completionLock = NSLock()
private var lastUpdateTime = Date()
private var lastBytes: Int64 = 0
private var hasReceivedData = false
var onFirstDataReceived: (() -> Void)?
init(destinationDirectory: URL,
fileName: String,
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void,
progressHandler: ((Int64, Int64, Int64) -> Void)? = nil) {
self.destinationDirectory = destinationDirectory
self.fileName = fileName
self.completionHandler = completionHandler
self.progressHandler = progressHandler
super.init()
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
completionLock.lock()
defer { completionLock.unlock() }
guard !hasCompleted else { return }
hasCompleted = true
do {
if !FileManager.default.fileExists(atPath: destinationDirectory.path) {
try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true)
}
let destinationURL = destinationDirectory.appendingPathComponent(fileName)
if FileManager.default.fileExists(atPath: destinationURL.path) {
try FileManager.default.removeItem(at: destinationURL)
}
try FileManager.default.moveItem(at: location, to: destinationURL)
completionHandler(destinationURL, downloadTask.response, nil)
} catch {
print("File operation error in delegate: \(error.localizedDescription)")
completionHandler(nil, downloadTask.response, error)
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
completionLock.lock()
defer { completionLock.unlock() }
guard !hasCompleted else { return }
hasCompleted = true
if let error = error {
switch (error as NSError).code {
case NSURLErrorCancelled:
return
case NSURLErrorTimedOut:
completionHandler(nil, task.response, NetworkError.downloadError("下载超时", error))
case NSURLErrorNotConnectedToInternet:
completionHandler(nil, task.response, NetworkError.noConnection)
default:
completionHandler(nil, task.response, error)
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
guard totalBytesExpectedToWrite > 0 else { return }
guard bytesWritten > 0 else { return }
if !hasReceivedData {
hasReceivedData = true
onFirstDataReceived?()
}
handleProgressUpdate(
bytesWritten: bytesWritten,
totalBytesWritten: totalBytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite
)
}
func cleanup() {
completionHandler = { _, _, _ in }
progressHandler = nil
}
private func handleProgressUpdate(bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let now = Date()
let timeDiff = now.timeIntervalSince(lastUpdateTime)
guard timeDiff >= NetworkConstants.progressUpdateInterval else { return }
Task {
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
}
lastUpdateTime = now
lastBytes = totalBytesWritten
}
}
func handleCustomDownload(task: NewDownloadTask, customDependencies: [DependenciesToDownload]) async throws {
await MainActor.run {
task.setStatus(.preparing(DownloadStatus.PrepareInfo(
message: String(localized: "正在准备自定义下载..."),
timestamp: Date(),
stage: .fetchingInfo
)))
}
for dependencyToDownload in customDependencies {
let productDir = task.directory.appendingPathComponent("\(dependencyToDownload.sapCode)")
if !FileManager.default.fileExists(atPath: productDir.path) {
try FileManager.default.createDirectory(at: productDir, withIntermediateDirectories: true)
}
if let applicationJson = dependencyToDownload.applicationJson {
var processedJsonString = applicationJson
if let jsonData = applicationJson.data(using: .utf8),
var appInfo = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
let selectedPackageNames = Set(dependencyToDownload.packages.filter { $0.isSelected }.map { $0.fullPackageName })
if var packages = appInfo["Packages"] as? [String: Any],
let packageArray = packages["Package"] as? [[String: Any]] {
let filteredPackages = packageArray.filter { package in
if let packageName = package["PackageName"] as? String {
let fullPackageName = packageName.hasSuffix(".zip") ? packageName : "\(packageName).zip"
return selectedPackageNames.contains(fullPackageName)
}
if let fullPackageName = package["fullPackageName"] as? String {
return selectedPackageNames.contains(fullPackageName)
}
return false
}
packages["Package"] = filteredPackages
appInfo["Packages"] = packages
}
if var modules = appInfo["Modules"] as? [String: Any],
let moduleArray = modules["Module"] as? [[String: Any]] {
let selectedPackageNamesWithoutZip = Set(dependencyToDownload.packages.filter { $0.isSelected }.compactMap { package in
let name = package.fullPackageName
return name.hasSuffix(".zip") ? String(name.dropLast(4)) : name
})
let filteredModules = moduleArray.filter { module in
if let referencePackages = module["ReferencePackages"] as? [String: Any],
let referencePackageArray = referencePackages["ReferencePackage"] as? [String] {
return referencePackageArray.contains { packageName in
selectedPackageNamesWithoutZip.contains(packageName)
}
}
return false
}
modules["Module"] = filteredModules
appInfo["Modules"] = modules
}
if let processedData = try? JSONSerialization.data(withJSONObject: appInfo, options: .prettyPrinted),
let processedString = String(data: processedData, encoding: .utf8) {
processedJsonString = processedString
}
}
let jsonURL = productDir.appendingPathComponent("application.json")
try processedJsonString.write(to: jsonURL, atomically: true, encoding: String.Encoding.utf8)
}
}
let filteredDependencies = customDependencies.map { dependency in
let selectedPackages = dependency.packages.filter { $0.isSelected }
let filteredDependency = DependenciesToDownload(
sapCode: dependency.sapCode,
version: dependency.version,
buildGuid: dependency.buildGuid,
applicationJson: dependency.applicationJson ?? ""
)
filteredDependency.packages = selectedPackages
return filteredDependency
}.filter { !$0.packages.isEmpty }
let totalSize = filteredDependencies.reduce(0) { productSum, product in
productSum + product.packages.reduce(0) { packageSum, pkg in
packageSum + (pkg.downloadSize > 0 ? pkg.downloadSize : 0)
}
}
await MainActor.run {
task.dependenciesToDownload = filteredDependencies
task.totalSize = totalSize
}
await startConcurrentDownloadProcess(task: task)
}
private func startConcurrentDownloadProcess(task: NewDownloadTask) async {
let maxConcurrency = StorageData.shared.maxConcurrentDownloads
let progressManager = ConcurrentDownloadProgressManager()
let isCancelled = await globalCancelTracker.isCancelled(task.id)
let isPaused = await globalCancelTracker.isPaused(task.id)
if isCancelled || isPaused {
return
}
var allPackages: [(package: Package, dependency: DependenciesToDownload, originalIndex: Int)] = []
var currentIndex = 0
for dependency in task.dependenciesToDownload {
for package in dependency.packages where !package.downloaded {
if package.status == .paused {
await MainActor.run {
package.status = .waiting
}
}
allPackages.append((package: package, dependency: dependency, originalIndex: currentIndex))
currentIndex += 1
}
}
allPackages.sort { $0.originalIndex < $1.originalIndex }
if allPackages.isEmpty {
await MainActor.run {
task.setStatus(.completed(DownloadStatus.CompletionInfo(
timestamp: Date(),
totalTime: Date().timeIntervalSince(task.createAt),
totalSize: task.totalSize
)))
}
await globalNetworkManager.saveTask(task)
return
}
let allTaskPackages = task.dependenciesToDownload.flatMap { $0.packages }
let allPackageInfo = allTaskPackages.map { (id: $0.fullPackageName, size: $0.downloadSize) }
await progressManager.initialize(packages: allPackageInfo)
for package in allTaskPackages {
if package.downloaded {
await progressManager.updatePackageProgress(
packageId: package.fullPackageName,
progress: 1.0,
speed: 0.0
)
} else {
await progressManager.updatePackageProgress(
packageId: package.fullPackageName,
progress: package.progress,
speed: package.speed
)
}
}
let packagesSnapshot = allPackages
await MainActor.run {
let totalPackages = packagesSnapshot.count
task.currentPackage = packagesSnapshot.first?.package
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
fileName: packagesSnapshot.first?.package.fullPackageName ?? "",
currentPackageIndex: 0,
totalPackages: totalPackages,
startTime: Date(),
estimatedTimeRemaining: nil
)))
task.objectWillChange.send()
}
await updateTaskProgress(task: task, progressManager: progressManager)
await prepareDownloadEnvironment(task: task)
do {
try await withThrowingTaskGroup(of: Void.self) { group in
let semaphore = AsyncSemaphore(value: maxConcurrency)
let totalCount = allPackages.count
let isCancelledFlag = AsyncFlag()
for (index, (package, dependency, _)) in allPackages.enumerated() {
group.addTask { [weak self] in
guard let self = self else {
return
}
await semaphore.wait()
defer {
Task {
await semaphore.signal()
}
}
try Task.checkCancellation()
if await isCancelledFlag.isSet() {
return
}
let isCancelled = await globalCancelTracker.isCancelled(task.id)
let isPaused = await globalCancelTracker.isPaused(task.id)
if isCancelled || isPaused {
await isCancelledFlag.set()
await MainActor.run {
package.status = isPaused ? .paused : .waiting
}
return
}
await MainActor.run {
package.status = .downloading
task.currentPackage = package
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
fileName: package.fullPackageName,
currentPackageIndex: index,
totalPackages: totalCount,
startTime: Date(),
estimatedTimeRemaining: nil
)))
task.objectWillChange.send()
}
do {
try await self.downloadPackageWithProgress(
package: package,
task: task,
product: dependency,
progressManager: progressManager,
cancelFlag: isCancelledFlag
)
await progressManager.markPackageCompleted(packageId: package.fullPackageName)
await MainActor.run {
dependency.completedPackages = dependency.packages.filter { $0.downloaded }.count
dependency.objectWillChange.send()
}
await self.updateTaskProgress(task: task, progressManager: progressManager)
await globalNetworkManager.saveTask(task)
} catch {
if !Task.isCancelled {
let isPauseRelated = {
if case NetworkError.cancelled = error {
return true
}
if let urlError = error as? URLError {
return urlError.code == .cancelled
}
if error is CancellationError {
return true
}
return false
}()
if isPauseRelated {
await MainActor.run {
package.status = .paused
}
} else {
await MainActor.run {
package.status = .failed(error.localizedDescription)
}
throw error
}
}
}
}
}
try await group.waitForAll()
}
if await progressManager.isAllCompleted() {
await MainActor.run {
task.setStatus(.completed(DownloadStatus.CompletionInfo(
timestamp: Date(),
totalTime: Date().timeIntervalSince(task.createAt),
totalSize: task.totalSize
)))
}
await globalNetworkManager.saveTask(task)
}
} catch {
if !Task.isCancelled {
let isPauseRelated = {
if case NetworkError.cancelled = error {
return true
}
if let urlError = error as? URLError {
return urlError.code == .cancelled
}
if error is CancellationError {
return true
}
return false
}()
if !isPauseRelated {
await handleError(task.id, error)
}
}
}
}
private func prepareDownloadEnvironment(task: NewDownloadTask) async {
let driverPath = task.directory.appendingPathComponent("driver.xml")
if !FileManager.default.fileExists(atPath: driverPath.path) {
if let productInfo = globalCcmResult.products.first(where: { $0.id == task.productId && $0.version == task.productVersion }) {
var selectedModules: [[String: Any]] = []
if let mainDependency = task.dependenciesToDownload.first(where: { $0.sapCode == task.productId }) {
let productDir = task.directory.appendingPathComponent(mainDependency.sapCode)
let jsonURL = productDir.appendingPathComponent("application.json")
if let jsonString = try? String(contentsOf: jsonURL, encoding: .utf8),
let jsonData = jsonString.data(using: .utf8),
let appInfo = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let modules = appInfo["Modules"] as? [String: Any],
let moduleArray = modules["Module"] as? [[String: Any]] {
selectedModules = moduleArray
}
}
let driverXml = generateDriverXML(
version: task.productVersion,
language: task.language,
productInfo: productInfo,
displayName: task.displayName,
modules: selectedModules
)
do {
try driverXml.write(to: driverPath, atomically: true, encoding: String.Encoding.utf8)
} catch {
print("Error generating driver.xml:", error.localizedDescription)
await MainActor.run {
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: "生成 driver.xml 失败: \(error.localizedDescription)",
error: error,
timestamp: Date(),
recoverable: false
)))
}
return
}
}
}
for dependencyToDownload in task.dependenciesToDownload {
let productDir = task.directory.appendingPathComponent(dependencyToDownload.sapCode)
if !FileManager.default.fileExists(atPath: productDir.path) {
do {
try FileManager.default.createDirectory(
at: productDir,
withIntermediateDirectories: true,
attributes: nil
)
} catch {
print("Error creating directory for \(dependencyToDownload.sapCode): \(error)")
continue
}
}
}
}
private func generatePackageIdentifier(package: Package, task: NewDownloadTask, dependency: DependenciesToDownload) -> String? {
let stableId = "\(task.productId)_\(task.productVersion)_\(dependency.sapCode)_\(package.fullPackageName)"
let stableHash = abs(stableId.hash)
let identifier = "Adobe_Downloader_\(stableHash)_\(package.fullPackageName)"
return identifier
}
private func downloadPackageWithProgress(
package: Package,
task: NewDownloadTask,
product: DependenciesToDownload,
progressManager: ConcurrentDownloadProgressManager,
cancelFlag: AsyncFlag
) async throws {
guard !package.fullPackageName.isEmpty,
!package.downloadURL.isEmpty,
package.downloadSize > 0 else {
return
}
let cleanCdn = globalCdn.hasSuffix("/") ? String(globalCdn.dropLast()) : globalCdn
let cleanPath = package.downloadURL.hasPrefix("/") ? package.downloadURL : "/\(package.downloadURL)"
let downloadURL = cleanCdn + cleanPath
guard let url = URL(string: downloadURL) else { return }
guard let packageIdentifier = generatePackageIdentifier(package: package, task: task, dependency: product) else { return }
let destinationURL = task.directory.appendingPathComponent(product.sapCode).appendingPathComponent(package.fullPackageName)
try await ChunkedDownloadManager.shared.downloadFileWithChunks(
packageIdentifier: packageIdentifier,
url: url,
destinationURL: destinationURL,
headers: NetworkConstants.downloadHeaders,
validationURL: package.validationURL,
progressHandler: { progress, downloadedSize, totalSize, speed in
Task {
await MainActor.run {
package.downloadedSize = downloadedSize
package.progress = progress
package.speed = speed
package.objectWillChange.send()
}
await progressManager.updatePackageProgress(
packageId: package.fullPackageName,
progress: progress,
speed: speed
)
await self.updateTaskProgress(task: task, progressManager: progressManager)
}
},
cancellationHandler: {
let isCancelled = await globalCancelTracker.isCancelled(task.id)
let isPaused = await globalCancelTracker.isPaused(task.id)
let isFlagCancelled = await cancelFlag.isSet()
return isCancelled || isPaused || isFlagCancelled
}
)
await MainActor.run {
package.downloadedSize = package.downloadSize
package.progress = 1.0
package.status = .completed
package.downloaded = true
}
}
private func updateTaskProgress(task: NewDownloadTask, progressManager: ConcurrentDownloadProgressManager) async {
let progressInfo = await progressManager.getTotalProgress()
await MainActor.run {
task.totalDownloadedSize = progressInfo.downloadedSize
task.totalProgress = progressInfo.progress
task.totalSpeed = progressInfo.totalSpeed
task.objectWillChange.send()
}
await globalNetworkManager.saveTask(task)
}
private func startDownloadProcess(task: NewDownloadTask) async {
actor DownloadProgress {
var currentPackageIndex: Int = 0
func increment() { currentPackageIndex += 1 }
func get() -> Int { return currentPackageIndex }
}
let progress = DownloadProgress()
await MainActor.run {
let totalPackages = task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count }
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
fileName: task.currentPackage?.fullPackageName ?? "",
currentPackageIndex: 0,
totalPackages: totalPackages,
startTime: Date(),
estimatedTimeRemaining: nil
)))
task.objectWillChange.send()
}
let driverPath = task.directory.appendingPathComponent("driver.xml")
if !FileManager.default.fileExists(atPath: driverPath.path) {
if let productInfo = globalCcmResult.products.first(where: { $0.id == task.productId && $0.version == task.productVersion }) {
var selectedModules: [[String: Any]] = []
if let mainDependency = task.dependenciesToDownload.first(where: { $0.sapCode == task.productId }) {
let productDir = task.directory.appendingPathComponent(mainDependency.sapCode)
let jsonURL = productDir.appendingPathComponent("application.json")
if let jsonString = try? String(contentsOf: jsonURL, encoding: .utf8),
let jsonData = jsonString.data(using: .utf8),
let appInfo = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let modules = appInfo["Modules"] as? [String: Any],
let moduleArray = modules["Module"] as? [[String: Any]] {
selectedModules = moduleArray
}
}
let driverXml = generateDriverXML(
version: task.productVersion,
language: task.language,
productInfo: productInfo,
displayName: task.displayName,
modules: selectedModules
)
do {
try driverXml.write(to: driverPath, atomically: true, encoding: String.Encoding.utf8)
} catch {
print("Error generating driver.xml:", error.localizedDescription)
await MainActor.run {
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: "生成 driver.xml 失败: \(error.localizedDescription)",
error: error,
timestamp: Date(),
recoverable: false
)))
}
return
}
}
}
for dependencyToDownload in task.dependenciesToDownload {
let productDir = task.directory.appendingPathComponent(dependencyToDownload.sapCode)
if !FileManager.default.fileExists(atPath: productDir.path) {
do {
try FileManager.default.createDirectory(
at: productDir,
withIntermediateDirectories: true,
attributes: nil
)
} catch {
print("Error creating directory for \(dependencyToDownload.sapCode): \(error)")
continue
}
}
}
for dependencyToDownload in task.dependenciesToDownload {
for package in dependencyToDownload.packages where !package.downloaded {
let currentIndex = await progress.get()
await MainActor.run {
task.currentPackage = package
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
fileName: package.fullPackageName,
currentPackageIndex: currentIndex,
totalPackages: task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count },
startTime: Date(),
estimatedTimeRemaining: nil
)))
}
await globalNetworkManager.saveTask(task)
await progress.increment()
guard !package.fullPackageName.isEmpty,
!package.downloadURL.isEmpty,
package.downloadSize > 0 else {
continue
}
let cleanCdn = globalCdn.hasSuffix("/") ? String(globalCdn.dropLast()) : globalCdn
let cleanPath = package.downloadURL.hasPrefix("/") ? package.downloadURL : "/\(package.downloadURL)"
let downloadURL = cleanCdn + cleanPath
guard let url = URL(string: downloadURL) else { continue }
do {
try await downloadPackage(package: package, task: task, product: dependencyToDownload, url: url)
} catch {
print("Error downloading package \(package.fullPackageName): \(error.localizedDescription)")
await handleError(task.id, error)
return
}
}
}
let allPackagesDownloaded = task.dependenciesToDownload.allSatisfy { product in
product.packages.allSatisfy { $0.downloaded }
}
if allPackagesDownloaded {
await MainActor.run {
task.setStatus(.completed(DownloadStatus.CompletionInfo(
timestamp: Date(),
totalTime: Date().timeIntervalSince(task.createAt),
totalSize: task.totalSize
)))
}
await globalNetworkManager.saveTask(task)
}
}
func handleError(_ taskId: UUID, _ error: Error) async {
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
guard let task = task else { return }
let (errorMessage, isRecoverable) = classifyError(error)
if isRecoverable && task.retryCount < NetworkConstants.maxRetryAttempts {
task.retryCount += 1
let nextRetryDate = Date().addingTimeInterval(TimeInterval(NetworkConstants.retryDelay / 1_000_000_000))
task.setStatus(.retrying(DownloadStatus.RetryInfo(
attempt: task.retryCount,
maxAttempts: NetworkConstants.maxRetryAttempts,
reason: errorMessage,
nextRetryDate: nextRetryDate
)))
Task {
do {
try await Task.sleep(nanoseconds: NetworkConstants.retryDelay)
if await globalCancelTracker.isCancelled(taskId) == false {
await resumeDownloadTask(taskId: taskId)
}
} catch {
print("Retry cancelled for task: \(taskId)")
}
}
} else {
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: errorMessage,
error: error,
timestamp: Date(),
recoverable: isRecoverable
)))
if !isRecoverable, let currentPackage = task.currentPackage {
let destinationDir = task.directory.appendingPathComponent("\(task.productId)")
let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName)
try? FileManager.default.removeItem(at: fileURL)
if let packageIdentifier = generatePackageIdentifier(package: currentPackage, task: task, dependency: task.dependenciesToDownload.first(where: { $0.packages.contains(where: { $0.id == currentPackage.id }) })!) {
ChunkedDownloadManager.shared.clearChunkedDownloadState(packageIdentifier: packageIdentifier)
}
}
await globalNetworkManager.saveTask(task)
await MainActor.run {
globalNetworkManager.updateDockBadge()
globalNetworkManager.objectWillChange.send()
}
}
}
func resumeDownloadTask(taskId: UUID) async {
let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId })
guard let task = task else {
return
}
await globalCancelTracker.resume(taskId)
await MainActor.run {
let totalPackages = task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count }
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
fileName: task.currentPackage?.fullPackageName ?? "",
currentPackageIndex: 0,
totalPackages: totalPackages,
startTime: Date(),
estimatedTimeRemaining: nil
)))
task.objectWillChange.send()
}
await globalNetworkManager.saveTask(task)
await MainActor.run {
globalNetworkManager.objectWillChange.send()
}
if task.productId == "APRO" {
if let currentPackage = task.currentPackage,
let product = task.dependenciesToDownload.first {
try? await downloadPackage(
package: currentPackage,
task: task,
product: product,
url: URL(string: currentPackage.downloadURL)
)
}
} else {
await startConcurrentDownloadProcess(task: task)
}
}
private func classifyError(_ error: Error) -> (message: String, recoverable: Bool) {
switch error {
case let networkError as NetworkError:
switch networkError {
case .noConnection:
return (String(localized: "网络连接已断开"), true)
case .timeout:
return (String(localized: "下载超时"), true)
case .serverUnreachable:
return (String(localized: "服务器无法访问"), true)
case .insufficientStorage:
return (String(localized: "存储空间不足"), false)
case .filePermissionDenied:
return (String(localized: "没有写入权限"), false)
default:
return (networkError.localizedDescription, false)
}
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .dataNotAllowed:
return (String(localized: "网络连接已断开"), true)
case .timedOut:
return (String(localized: "连接超时"), true)
case .cancelled:
return (String(localized: "下载已取消"), false)
case .cannotConnectToHost, .dnsLookupFailed:
return (String(localized: "无法连接到服务器"), true)
default:
return (urlError.localizedDescription, true)
}
default:
return (error.localizedDescription, false)
}
}
private func downloadPackage(package: Package, task: NewDownloadTask, product: DependenciesToDownload, url: URL? = nil) async throws {
var lastUpdateTime = Date()
var lastBytes: Int64 = 0
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) -> Void in
let delegate = DownloadDelegate(
destinationDirectory: task.directory.appendingPathComponent(product.sapCode),
fileName: package.fullPackageName,
completionHandler: { [weak globalNetworkManager] (localURL: URL?, response: URLResponse?, error: Error?) in
if let error = error {
if (error as NSError).code == NSURLErrorCancelled {
continuation.resume()
} else {
continuation.resume(throwing: error)
}
return
}
Task { @MainActor in
package.downloadedSize = package.downloadSize
package.progress = 1.0
package.status = .completed
package.downloaded = true
var totalDownloaded: Int64 = 0
var totalSize: Int64 = 0
for prod in task.dependenciesToDownload {
for pkg in prod.packages {
totalSize += pkg.downloadSize
if pkg.downloaded {
totalDownloaded += pkg.downloadSize
}
}
}
task.totalSize = totalSize
task.totalDownloadedSize = totalDownloaded
task.totalProgress = Double(totalDownloaded) / Double(totalSize)
task.totalSpeed = 0
let allCompleted = task.dependenciesToDownload.allSatisfy {
product in product.packages.allSatisfy { $0.downloaded }
}
if allCompleted {
task.setStatus(.completed(DownloadStatus.CompletionInfo(
timestamp: Date(),
totalTime: Date().timeIntervalSince(task.createAt),
totalSize: totalSize
)))
}
product.updateCompletedPackages()
await globalNetworkManager?.saveTask(task)
globalNetworkManager?.objectWillChange.send()
continuation.resume()
}
},
progressHandler: { [weak globalNetworkManager] (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) in
Task { @MainActor in
let now = Date()
let timeDiff = now.timeIntervalSince(lastUpdateTime)
if timeDiff >= 1.0 {
let bytesDiff = totalBytesWritten - lastBytes
let speed = Double(bytesDiff) / timeDiff
package.updateProgress(
downloadedSize: totalBytesWritten,
speed: speed
)
var totalDownloaded: Int64 = 0
var totalSize: Int64 = 0
var currentSpeed: Double = 0
for prod in task.dependenciesToDownload {
for pkg in prod.packages {
totalSize += pkg.downloadSize
if pkg.downloaded {
totalDownloaded += pkg.downloadSize
} else if pkg.id == package.id {
totalDownloaded += totalBytesWritten
currentSpeed = speed
}
}
}
task.totalSize = totalSize
task.totalDownloadedSize = totalDownloaded
task.totalProgress = totalSize > 0 ? Double(totalDownloaded) / Double(totalSize) : 0
task.totalSpeed = currentSpeed
lastUpdateTime = now
lastBytes = totalBytesWritten
globalNetworkManager?.objectWillChange.send()
}
}
}
)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
Task {
let downloadTask: URLSessionDownloadTask
if let url = url {
var request = URLRequest(url: url)
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
downloadTask = session.downloadTask(with: request)
} else {
continuation.resume(throwing: NetworkError.invalidData("No URL provided"))
return
}
await globalCancelTracker.registerTask(task.id, task: downloadTask, session: session)
downloadTask.resume()
}
}
}
func generateDriverXML(version: String, language: String, productInfo: Product, displayName: String, modules: [[String: Any]] = []) -> String {
// platform languageSet
guard let platform = globalProducts.first(where: { $0.id == productInfo.id && $0.version == version })?.platforms.first?.id,
let languageSet = globalProducts.first(where: { $0.id == productInfo.id && $0.version == version })?.platforms.first?.languageSet else {
return ""
}
//
let dependencies = (languageSet.first?.dependencies.map { dependency in
"""
<Dependency>
<BuildGuid>\(dependency.buildGuid)</BuildGuid>
<BuildVersion>\(dependency.productVersion)</BuildVersion>
<CodexVersion>\(dependency.baseVersion)</CodexVersion>
<Platform>\(dependency.selectedPlatform)</Platform>
<SAPCode>\(dependency.sapCode)</SAPCode>
<EsdDirectory>\(dependency.sapCode)</EsdDirectory>
</Dependency>
"""
}.joined(separator: "\n")) ?? ""
// PSPR
let moduleXml = modules.compactMap { module in
if let moduleId = module["Id"] as? String {
return """
<Module>
<Id>\(moduleId)</Id>
<Baseline>false</Baseline>
</Module>
"""
}
return nil
}.joined(separator: "\n ")
let buildGuid = productInfo.platforms.first?.languageSet.first?.buildGuid ?? ""
let buildVersion = languageSet.first?.productVersion ?? ""
return """
<DriverInfo>
<ProductInfo>
<Dependencies>
\(dependencies)
</Dependencies>
<Modules>
\(moduleXml.isEmpty ? "" : moduleXml)
</Modules>
<BuildGuid>\(buildGuid)</BuildGuid>
<BuildVersion>\(buildVersion)</BuildVersion>
<CodexVersion>\(productInfo.version)</CodexVersion>
<Platform>\(platform)</Platform>
<EsdDirectory>\(productInfo.id)</EsdDirectory>
<SAPCode>\(productInfo.id)</SAPCode>
</ProductInfo>
<RequestInfo>
<InstallDir>/Applications</InstallDir>
<InstallLanguage>\(language)</InstallLanguage>
</RequestInfo>
</DriverInfo>
"""
}
func downloadAPRO(task: NewDownloadTask, productInfo: Product) async throws {
let firstPlatform = productInfo.platforms.first
let productManifestURL = firstPlatform?.languageSet.first?.manifestURL ?? ""
let manifestURL = globalCdn + productManifestURL
print("manifestURL")
print(manifestURL)
guard let url = URL(string: manifestURL) else {
throw NetworkError.invalidURL(manifestURL)
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
let headers = NetworkConstants.adobeRequestHeaders
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
let (manifestData, _) = try await URLSession.shared.data(for: request)
let manifestDoc = try XMLDocument(data: manifestData)
guard let downloadPath = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue,
let assetSizeStr = try manifestDoc.nodes(forXPath: "//asset_list/asset/asset_size").first?.stringValue,
let assetSize = Int64(assetSizeStr) else {
throw NetworkError.invalidData("无法从manifest中获取下载信息")
}
guard let downloadURL = URL(string: downloadPath) else {
throw NetworkError.invalidURL(downloadPath)
}
print("downloadURL \(downloadURL)")
let aproPackage = Package(
type: "dmg",
fullPackageName: "Adobe Downloader \(task.productId)_\(firstPlatform?.languageSet.first?.productVersion ?? "unknown")_\(firstPlatform?.id ?? "unknown").dmg",
downloadSize: assetSize,
downloadURL: downloadPath,
packageVersion: ""
)
print(aproPackage)
await MainActor.run {
let product = DependenciesToDownload(sapCode: task.productId, version: firstPlatform?.languageSet.first?.productVersion ?? "unknown", buildGuid: "")
product.packages = [aproPackage]
task.dependenciesToDownload = [product]
task.totalSize = assetSize
task.currentPackage = aproPackage
task.setStatus(.downloading(DownloadStatus.DownloadInfo(
fileName: aproPackage.fullPackageName,
currentPackageIndex: 0,
totalPackages: 1,
startTime: Date(),
estimatedTimeRemaining: nil
)))
}
let tempDownloadDir = task.directory.deletingLastPathComponent()
var lastUpdateTime = Date()
var lastBytes: Int64 = 0
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) -> Void in
let delegate = DownloadDelegate(
destinationDirectory: tempDownloadDir,
fileName: aproPackage.fullPackageName,
completionHandler: { [weak globalNetworkManager] (localURL: URL?, response: URLResponse?, error: Error?) in
if let error = error {
if (error as NSError).code == NSURLErrorCancelled {
continuation.resume()
} else {
print("Download error:", error)
continuation.resume(throwing: error)
}
return
}
Task { @MainActor in
aproPackage.downloadedSize = aproPackage.downloadSize
aproPackage.progress = 1.0
aproPackage.status = .completed
aproPackage.downloaded = true
var totalDownloaded: Int64 = 0
var totalSize: Int64 = 0
totalSize += aproPackage.downloadSize
if aproPackage.downloaded {
totalDownloaded += aproPackage.downloadSize
}
task.totalSize = totalSize
task.totalDownloadedSize = totalDownloaded
task.totalProgress = Double(totalDownloaded) / Double(totalSize)
task.totalSpeed = 0
task.setStatus(.completed(DownloadStatus.CompletionInfo(
timestamp: Date(),
totalTime: Date().timeIntervalSince(task.createAt),
totalSize: totalSize
)))
task.objectWillChange.send()
await globalNetworkManager?.saveTask(task)
globalNetworkManager?.updateDockBadge()
globalNetworkManager?.objectWillChange.send()
continuation.resume()
}
},
progressHandler: { [weak globalNetworkManager] (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) in
Task { @MainActor in
let now = Date()
let timeDiff = now.timeIntervalSince(lastUpdateTime)
if timeDiff >= 1.0 {
let bytesDiff = totalBytesWritten - lastBytes
let speed = Double(bytesDiff) / timeDiff
aproPackage.updateProgress(
downloadedSize: totalBytesWritten,
speed: speed
)
task.totalDownloadedSize = totalBytesWritten
task.totalProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
task.totalSpeed = speed
lastUpdateTime = now
lastBytes = totalBytesWritten
task.objectWillChange.send()
globalNetworkManager?.objectWillChange.send()
Task {
await globalNetworkManager?.saveTask(task)
}
}
}
}
)
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
var downloadRequest = URLRequest(url: downloadURL)
NetworkConstants.downloadHeaders.forEach { downloadRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
let downloadTask = session.downloadTask(with: downloadRequest)
Task {
await globalCancelTracker.registerTask(task.id, task: downloadTask, session: session)
if await globalCancelTracker.isCancelled(task.id) {
continuation.resume(throwing: NetworkError.cancelled)
return
}
downloadTask.resume()
}
}
}
func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
await globalCancelTracker.pause(taskId)
guard let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) else {
return
}
for dependency in task.dependenciesToDownload {
for package in dependency.packages {
if let packageIdentifier = generatePackageIdentifier(package: package, task: task, dependency: dependency) {
ChunkedDownloadManager.shared.pauseDownload(packageIdentifier: packageIdentifier)
}
}
}
let taskPackageMap = await globalCancelTracker.getTaskPackageMap()
for (_, (downloadTask, _, _)) in taskPackageMap {
downloadTask.cancel()
}
try? await Task.sleep(nanoseconds: 200_000_000) // 0.5
await globalCancelTracker.cleanupCompletedTasks()
await MainActor.run {
task.setStatus(.paused(DownloadStatus.PauseInfo(
reason: reason,
timestamp: Date(),
resumable: true
)))
for dependency in task.dependenciesToDownload {
for package in dependency.packages {
if package.status == .downloading {
package.status = .paused
}
}
}
globalNetworkManager.objectWillChange.send()
}
await globalNetworkManager.saveTask(task)
}
func getApplicationInfo(buildGuid: String) async throws -> String {
guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
var headers = NetworkConstants.adobeRequestHeaders
headers["x-adobe-build-guid"] = buildGuid
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
}
guard let jsonString = String(data: data, encoding: .utf8) else {
throw NetworkError.invalidData(String(localized: "无法将响应数据转换为json字符串"))
}
return jsonString
}
private func compareVersions(current: Double, required: Double, operator: String) -> Bool {
switch `operator` {
case ">=":
return current >= required
case "<=":
return current <= required
case ">":
return current > required
case "<":
return current < required
case "==":
return current == required
default:
return false
}
}
private func executePrivilegedCommand(_ command: String) async -> String {
return await withCheckedContinuation { continuation in
PrivilegedHelperAdapter.shared.executeCommand(command) { result in
if result.starts(with: "Error:") {
print("命令执行失败: \(command)")
print("错误信息: \(result)")
}
continuation.resume(returning: result)
}
}
}
func downloadX1a0HeCCPackages(
progressHandler: @escaping (Double, String) -> Void,
cancellationHandler: @escaping () -> Bool,
shouldProcess: Bool = true
) async throws {
let baseUrl = "https://cdn-ffc.oobesaas.adobe.com/core/v1/applications?name=CreativeCloud&platform=\(AppStatics.isAppleSilicon ? "macarm64" : "osx10")"
let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 300
configuration.httpAdditionalHeaders = NetworkConstants.downloadHeaders
let session = URLSession(configuration: configuration)
do {
var request = URLRequest(url: URL(string: baseUrl)!)
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
let xmlDoc = try XMLDocument(data: data)
let packageSets = try xmlDoc.nodes(forXPath: "//packageSet[name='ADC']")
guard let adcPackageSet = packageSets.first else {
throw NetworkError.invalidData("找不到ADC包集")
}
let targetPackages = ["HDBox", "IPCBox"]
var packagesToDownload: [(name: String, url: URL, size: Int64)] = []
for packageName in targetPackages {
let packageNodes = try adcPackageSet.nodes(forXPath: ".//package[name='\(packageName)']")
guard let package = packageNodes.first else {
print("未找到包: \(packageName)")
continue
}
guard let manifestUrl = try package.nodes(forXPath: ".//manifestUrl").first?.stringValue,
let cdnBase = try xmlDoc.nodes(forXPath: "//cdn/secure").first?.stringValue else {
print("无法获取manifest URL或CDN基础URL")
continue
}
let manifestFullUrl = cdnBase + manifestUrl
var manifestRequest = URLRequest(url: URL(string: manifestFullUrl)!)
NetworkConstants.downloadHeaders.forEach { manifestRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
let (manifestData, manifestResponse) = try await session.data(for: manifestRequest)
guard let manifestHttpResponse = manifestResponse as? HTTPURLResponse,
(200...299).contains(manifestHttpResponse.statusCode) else {
print("获取manifest失败: HTTP \(String(describing: (manifestResponse as? HTTPURLResponse)?.statusCode))")
continue
}
let manifestDoc = try XMLDocument(data: manifestData)
let assetPathNodes = try manifestDoc.nodes(forXPath: "//asset_path")
let sizeNodes = try manifestDoc.nodes(forXPath: "//asset_size")
guard let assetPath = assetPathNodes.first?.stringValue,
let sizeStr = sizeNodes.first?.stringValue,
let size = Int64(sizeStr),
let downloadUrl = URL(string: assetPath) else {
continue
}
packagesToDownload.append((packageName, downloadUrl, size))
}
guard !packagesToDownload.isEmpty else {
throw NetworkError.invalidData("没有找到可下载的包")
}
let totalCount = packagesToDownload.count
for (index, package) in packagesToDownload.enumerated() {
if cancellationHandler() {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.cancelled
}
await MainActor.run {
progressHandler(Double(index) / Double(totalCount), "正在下载 \(package.name)...")
}
let destinationURL = tempDirectory.appendingPathComponent("\(package.name).zip")
var downloadRequest = URLRequest(url: package.url)
print(downloadRequest)
NetworkConstants.downloadHeaders.forEach { downloadRequest.setValue($0.value, forHTTPHeaderField: $0.key) }
let (downloadURL, downloadResponse) = try await session.download(for: downloadRequest)
guard let httpResponse = downloadResponse as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("下载失败: HTTP \(String(describing: (downloadResponse as? HTTPURLResponse)?.statusCode))")
continue
}
try FileManager.default.moveItem(at: downloadURL, to: destinationURL)
}
await MainActor.run {
progressHandler(0.9, shouldProcess ? "正在安装组件..." : "正在完成下载...")
}
let targetDirectory = "/Library/Application\\ Support/Adobe/Adobe\\ Desktop\\ Common"
let rawTargetDirectory = "/Library/Application Support/Adobe/Adobe Desktop Common"
if !FileManager.default.fileExists(atPath: rawTargetDirectory) {
let createDirResult = await executePrivilegedCommand("/bin/mkdir -p \(targetDirectory)")
if createDirResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("创建目录失败: \(createDirResult)")
}
let chmodResult = await executePrivilegedCommand("/bin/chmod 755 \(targetDirectory)")
if chmodResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("设置权限失败: \(chmodResult)")
}
}
for package in packagesToDownload {
let packageDir = "\(targetDirectory)/\(package.name)"
let removeResult = await executePrivilegedCommand("/bin/rm -rf \(packageDir)")
if removeResult.starts(with: "Error:") {
print("移除旧目录失败: \(removeResult)")
}
let mkdirResult = await executePrivilegedCommand("/bin/mkdir -p \(packageDir)")
if mkdirResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("创建 \(package.name) 目录失败")
}
let unzipResult = await executePrivilegedCommand("cd \(packageDir) && /usr/bin/unzip -o '\(tempDirectory.path)/\(package.name).zip'")
if unzipResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("解压 \(package.name) 失败: \(unzipResult)")
}
let chmodResult = await executePrivilegedCommand("/bin/chmod -R 755 \(packageDir)")
if chmodResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("设置 \(package.name) 权限失败: \(chmodResult)")
}
let chownResult = await executePrivilegedCommand("/usr/sbin/chown -R root:wheel \(packageDir)")
if chownResult.starts(with: "Error:") {
try? FileManager.default.removeItem(at: tempDirectory)
throw NetworkError.installError("设置 \(package.name) 所有者失败: \(chownResult)")
}
}
try await Task.sleep(nanoseconds: 1_000_000_000)
if shouldProcess {
try await withCheckedThrowingContinuation { continuation in
ModifySetup.backupAndModifySetupFile { success, message in
if success {
continuation.resume()
} else {
continuation.resume(throwing: NetworkError.installError(message))
}
}
}
ModifySetup.clearVersionCache()
}
try? FileManager.default.removeItem(at: tempDirectory)
await MainActor.run {
progressHandler(1.0, shouldProcess ? "安装完成" : "下载完成")
}
} catch {
print("发生错误: \(error.localizedDescription)")
throw error
}
}
func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
await globalCancelTracker.cancel(taskId)
if let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) {
for dependency in task.dependenciesToDownload {
for package in dependency.packages {
if let packageIdentifier = generatePackageIdentifier(package: package, task: task, dependency: dependency) {
ChunkedDownloadManager.shared.cancelDownload(packageIdentifier: packageIdentifier)
}
}
}
}
if let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) {
if removeFiles {
try? FileManager.default.removeItem(at: task.directory)
}
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: String(localized: "下载已取消"),
error: NetworkError.downloadCancelled,
timestamp: Date(),
recoverable: false
)))
await globalNetworkManager.saveTask(task)
await MainActor.run {
globalNetworkManager.updateDockBadge()
globalNetworkManager.objectWillChange.send()
}
}
}
}