// // Adobe Downloader // // Created by X1a0He on 2024/10/30. // import Foundation import Network import Combine import AppKit class DownloadUtils { typealias ProgressUpdate = (bytesWritten: Int64, totalWritten: Int64, expectedToWrite: Int64) private weak var networkManager: NetworkManager? private let cancelTracker: CancelTracker init(networkManager: NetworkManager, cancelTracker: CancelTracker) { self.networkManager = networkManager self.cancelTracker = cancelTracker } 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 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 } 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 } progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) lastUpdateTime = now lastBytes = totalBytesWritten } } func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async { let task = await cancelTracker.downloadTasks[taskId] if let downloadTask = task { let data = await withCheckedContinuation { continuation in downloadTask.cancel(byProducingResumeData: { data in continuation.resume(returning: data) }) } if let data = data { await cancelTracker.storeResumeData(taskId, data: data) } } await MainActor.run { if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) { task.setStatus(.paused(DownloadStatus.PauseInfo( reason: reason, timestamp: Date(), resumable: true ))) networkManager?.saveTask(task) } } } func resumeDownloadTask(taskId: UUID) async { if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { await MainActor.run { task.setStatus(.downloading(DownloadStatus.DownloadInfo( fileName: task.currentPackage?.fullPackageName ?? "", currentPackageIndex: 0, totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count }, startTime: Date(), estimatedTimeRemaining: nil ))) networkManager?.saveTask(task) } if task.sapCode == "APRO" { if let resumeData = await cancelTracker.getResumeData(taskId), let currentPackage = task.currentPackage, let product = task.productsToDownload.first { try? await downloadPackage( package: currentPackage, task: task, product: product, resumeData: resumeData ) } } else { await startDownloadProcess(task: task) } } } func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async { await cancelTracker.cancel(taskId) await MainActor.run { if let task = networkManager?.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 ))) networkManager?.updateDockBadge() networkManager?.objectWillChange.send() } } } func generateDriverXML(sapCode: String, version: String, language: String, productInfo: Sap.Versions, displayName: String) -> String { let dependencies = productInfo.dependencies.map { dependency in """ \(dependency.sapCode) \(dependency.version) \(dependency.sapCode) """ }.joined(separator: "\n") return """ Adobe \(displayName) \(sapCode) \(version) \(productInfo.apPlatform) \(sapCode) \(dependencies) /Applications \(language) """ } private func executePrivilegedCommand(_ command: String) async -> String { return await withCheckedContinuation { continuation in PrivilegedHelperManager.shared.executeCommand(command) { result in if result.starts(with: "Error:") { print("命令执行失败: \(command)") print("错误信息: \(result)") } continuation.resume(returning: result) } } } private func downloadPackage(package: Package, task: NewDownloadTask, product: ProductsToDownload, url: URL? = nil, resumeData: Data? = nil) async throws { var lastUpdateTime = Date() var lastBytes: Int64 = 0 return try await withCheckedThrowingContinuation { continuation in let delegate = DownloadDelegate( destinationDirectory: task.directory.appendingPathComponent(product.sapCode), fileName: package.fullPackageName, completionHandler: { [weak networkManager] localURL, response, 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.productsToDownload { 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.productsToDownload.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() networkManager?.saveTask(task) networkManager?.objectWillChange.send() } continuation.resume() }, progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite 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.productsToDownload { 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 networkManager?.objectWillChange.send() } } } ) let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) Task { let downloadTask: URLSessionDownloadTask if let resumeData = resumeData { downloadTask = session.downloadTask(withResumeData: resumeData) } else 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("Neither URL nor resume data provided")) return } await cancelTracker.registerTask(task.id, task: downloadTask, session: session) await cancelTracker.clearResumeData(task.id) downloadTask.resume() } } } 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.productsToDownload.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 = await networkManager?.saps[task.sapCode]?.versions[task.version] { let driverXml = generateDriverXML( sapCode: task.sapCode, version: task.version, language: task.language, productInfo: productInfo, displayName: task.displayName ) do { try driverXml.write(to: driverPath, atomically: true, 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 product in task.productsToDownload { let productDir = task.directory.appendingPathComponent(product.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 \(product.sapCode): \(error)") continue } } } for product in task.productsToDownload { for package in product.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.productsToDownload.reduce(0) { $0 + $1.packages.count }, startTime: Date(), estimatedTimeRemaining: nil ))) networkManager?.saveTask(task) } await progress.increment() guard !package.fullPackageName.isEmpty, !package.downloadURL.isEmpty, package.downloadSize > 0 else { continue } let cdn = await networkManager?.cdn ?? "" let cleanCdn = cdn.hasSuffix("/") ? String(cdn.dropLast()) : cdn let cleanPath = package.downloadURL.hasPrefix("/") ? package.downloadURL : "/\(package.downloadURL)" let downloadURL = cleanCdn + cleanPath guard let url = URL(string: downloadURL) else { continue } do { if let resumeData = await cancelTracker.getResumeData(task.id) { try await downloadPackage(package: package, task: task, product: product, resumeData: resumeData) } else { try await downloadPackage(package: package, task: task, product: product, url: url) } } catch { print("Error downloading package \(package.fullPackageName): \(error.localizedDescription)") await handleError(task.id, error) return } } } let allPackagesDownloaded = task.productsToDownload.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 ))) networkManager?.saveTask(task) } } } func retryPackage(task: NewDownloadTask, package: Package) async throws { guard package.canRetry else { return } package.prepareForRetry() if let product = task.productsToDownload.first(where: { $0.packages.contains(where: { $0.id == package.id }) }) { await MainActor.run { task.currentPackage = package } if let cdn = await networkManager?.cdnUrl { try await downloadPackage(package: package, task: task, product: product, url: URL(string: cdn + package.downloadURL)!) } else { throw NetworkError.invalidData("无法取 CDN 地址") } } } func downloadAPRO(task: NewDownloadTask, productInfo: Sap.Versions) async throws { guard let networkManager = networkManager else { return } let manifestURL = await networkManager.cdnUrl + productInfo.buildGuid guard let url = URL(string: manifestURL) else { throw NetworkError.invalidURL(manifestURL) } var request = URLRequest(url: url) NetworkConstants.adobeRequestHeaders.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) } let aproPackage = Package( type: "dmg", fullPackageName: "Adobe Downloader \(task.sapCode)_\(productInfo.productVersion)_\(productInfo.apPlatform).dmg", downloadSize: assetSize, downloadURL: downloadPath, packageVersion: "" ) await MainActor.run { let product = ProductsToDownload( sapCode: task.sapCode, version: task.version, buildGuid: productInfo.buildGuid ) product.packages = [aproPackage] task.productsToDownload = [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) in let delegate = DownloadDelegate( destinationDirectory: tempDownloadDir, fileName: aproPackage.fullPackageName, completionHandler: { [weak networkManager] (localURL: URL?, response: URLResponse?, error: Error?) in if let error = error { if (error as NSError).code == NSURLErrorCancelled { continuation.resume(throwing: NetworkError.cancelled) return } 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 let allCompleted = task.productsToDownload.allSatisfy { product in product.packages.allSatisfy { $0.downloaded } } if allCompleted { task.setStatus(.completed(DownloadStatus.CompletionInfo( timestamp: Date(), totalTime: Date().timeIntervalSince(task.createAt), totalSize: totalSize ))) } task.objectWillChange.send() networkManager?.objectWillChange.send() continuation.resume() } }, progressHandler: { [weak networkManager] (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() networkManager?.objectWillChange.send() } } } ) 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 self.cancelTracker.registerTask(task.id, task: downloadTask, session: session) if await self.cancelTracker.isCancelled(task.id) { continuation.resume(throwing: NetworkError.cancelled) return } downloadTask.resume() } } } func handleDownload(task: NewDownloadTask, productInfo: Sap.Versions, allowedPlatform: [String], saps: [String: Sap]) async throws { if task.sapCode == "APRO" { try await downloadAPRO(task: task, productInfo: productInfo) return } var productsToDownload: [ProductsToDownload] = [] productsToDownload.append(ProductsToDownload( sapCode: task.sapCode, version: task.version, buildGuid: productInfo.buildGuid )) for dependency in productInfo.dependencies { if let dependencyVersions = saps[dependency.sapCode]?.versions { var processedVersions = Set() var firstGuid: String? var buildGuid: String? let sortedVersions = dependencyVersions.sorted { version1, version2 in let v1Components = version1.key.split(separator: ".").compactMap { Int($0) } let v2Components = version2.key.split(separator: ".").compactMap { Int($0) } for i in 0.. v2Components[i] } } return v1Components.count > v2Components.count } for version in sortedVersions { if buildGuid != nil { break } if processedVersions.contains(version.key) { continue } processedVersions.insert(version.key) if version.value.baseVersion == dependency.version { if firstGuid == nil { firstGuid = version.value.buildGuid } print("\(version.value.sapCode), \(version.key), \(allowedPlatform), \(version.value.apPlatform), \(allowedPlatform.contains(version.value.apPlatform))") if allowedPlatform.contains(version.value.apPlatform) { buildGuid = version.value.buildGuid break } } } if buildGuid == nil { buildGuid = firstGuid } if let finalBuildGuid = buildGuid { let alreadyAdded = productsToDownload.contains { product in product.sapCode == dependency.sapCode && product.version == dependency.version } if !alreadyAdded { productsToDownload.append(ProductsToDownload( sapCode: dependency.sapCode, version: dependency.version, buildGuid: finalBuildGuid )) } } } } for product in productsToDownload { print("\(product.sapCode), \(product.version), \(product.buildGuid)") } for product in productsToDownload { await MainActor.run { task.setStatus(.preparing(DownloadStatus.PrepareInfo( message: String(localized: "正在处理 \(product.sapCode) 的包信息..."), timestamp: Date(), stage: .fetchingInfo ))) } let jsonString = try await getApplicationInfo(buildGuid: product.buildGuid) let productDir = task.directory.appendingPathComponent("\(product.sapCode)") if !FileManager.default.fileExists(atPath: productDir.path) { try FileManager.default.createDirectory(at: productDir, withIntermediateDirectories: true) } let jsonURL = productDir.appendingPathComponent("application.json") try jsonString.write(to: jsonURL, atomically: true, encoding: .utf8) guard let jsonData = jsonString.data(using: .utf8), let appInfo = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any], let packages = appInfo["Packages"] as? [String: Any], let packageArray = packages["Package"] as? [[String: Any]] else { throw NetworkError.invalidData("无法解析产品信息") } var corePackageCount = 0 var nonCorePackageCount = 0 /* 这里是对包的过滤,一般规则在 1. 如果没有Condition,那么就视为需要下载的包 2. 如果存在Condition,那么按照以下规则下载 [OSVersion]>=10.15 : 系统版本大于等于10.15就下载,所以需要一个函数来获取系统版本号 [OSArchitecture]==arm64 : 系统架构为arm64的就下载,官方并没有下载另外一个架构的包 [OSArchitecture]==x64 : 同上 [installLanguage]==zh_CN : 目标安装语言为 zh_CN 的就下载 PS: 下面是留给看源码的人的 哪怕是官方的ACC下载任何一款App,都是这个逻辑,不信自己去翻,你可能会说,为什么官方能下通用的,你问这个问题之前,可以自己去拿正版的看看他是怎么下载的,他下载的包数量跟我的是不是一致的,他也只是下载了对应架构的包 其实要下载通用的也很简单,不是判断架构吗,那下载通用的时候,两个架构同时成立不就好了,但我并没有在官方的下载逻辑中看到,也没尝试过,如果你尝试之后发现可以,请你告诉我 */ for package in packageArray { var shouldDownload = false let packageType = package["Type"] as? String ?? "non-core" let isCore = packageType == "core" guard let downloadURL = package["Path"] as? String, !downloadURL.isEmpty else { continue } let fullPackageName: String let packageVersion: String if let name = package["fullPackageName"] as? String, !name.isEmpty { fullPackageName = name packageVersion = package["PackageVersion"] as? String ?? "" } else if let name = package["PackageName"] as? String, !name.isEmpty { fullPackageName = "\(name).zip" packageVersion = package["PackageVersion"] as? String ?? "" } else { continue } let downloadSize: Int64 if let sizeNumber = package["DownloadSize"] as? NSNumber { downloadSize = sizeNumber.int64Value } else if let sizeString = package["DownloadSize"] as? String, let parsedSize = Int64(sizeString) { downloadSize = parsedSize } else if let sizeInt = package["DownloadSize"] as? Int { downloadSize = Int64(sizeInt) } else { continue } let installLanguage = "[installLanguage]==\(task.language)" if let condition = package["Condition"] as? String { if condition.isEmpty { shouldDownload = true } else { if condition.contains("[OSVersion]") { let osVersion = ProcessInfo.processInfo.operatingSystemVersion let currentVersion = Double("\(osVersion.majorVersion).\(osVersion.minorVersion)") ?? 0.0 let versionPattern = #"\[OSVersion\](>=|<=|<|>|==)([\d.]+)"# let regex = try? NSRegularExpression(pattern: versionPattern) let range = NSRange(condition.startIndex.. 0 ? pkg.downloadSize : 0) } } await MainActor.run { task.productsToDownload = finalProducts task.totalSize = totalSize } await startDownloadProcess(task: 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 } func handleError(_ taskId: UUID, _ error: Error) async { guard let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) 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 !cancelTracker.isCancelled(taskId) { await resumeDownloadTask(taskId: taskId) } } catch { print("Retry cancelled for task: \(taskId)") } } } else { await MainActor.run { task.setStatus(.failed(DownloadStatus.FailureInfo( message: errorMessage, error: error, timestamp: Date(), recoverable: isRecoverable ))) if let currentPackage = task.currentPackage { let destinationDir = task.directory .appendingPathComponent("\(task.sapCode)") let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName) try? FileManager.default.removeItem(at: fileURL) } networkManager?.saveTask(task) networkManager?.updateDockBadge() networkManager?.objectWillChange.send() } } } 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: return (String(localized: "网络连接已断开"), true) case .timedOut: return (String(localized: "连接超时"), true) case .cancelled: return (String(localized: "下载已取消"), false) default: return (urlError.localizedDescription, true) } default: return (error.localizedDescription, false) } } @MainActor func updateProgress(for taskId: UUID, progress: ProgressUpdate) { guard let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }), let currentPackage = task.currentPackage else { return } let now = Date() let timeDiff = now.timeIntervalSince(currentPackage.lastUpdated) if timeDiff >= NetworkConstants.progressUpdateInterval { currentPackage.updateProgress( downloadedSize: progress.totalWritten, speed: Double(progress.bytesWritten) ) let totalDownloaded = task.productsToDownload.reduce(Int64(0)) { sum, prod in sum + prod.packages.reduce(Int64(0)) { sum, pkg in if pkg.downloaded { return sum + pkg.downloadSize } else if pkg.id == currentPackage.id { return sum + progress.totalWritten } return sum } } task.totalDownloadedSize = totalDownloaded task.totalProgress = Double(totalDownloaded) / Double(task.totalSize) task.totalSpeed = currentPackage.speed currentPackage.lastRecordedSize = progress.totalWritten currentPackage.lastUpdated = now task.objectWillChange.send() networkManager?.objectWillChange.send() } } @MainActor func updateTaskStatus(_ taskId: UUID, _ status: DownloadStatus) async { guard let networkManager = networkManager else { return } if let index = networkManager.downloadTasks.firstIndex(where: { $0.id == taskId }) { networkManager.downloadTasks[index].setStatus(status) switch status { case .completed, .failed: networkManager.progressObservers[taskId]?.invalidate() networkManager.progressObservers.removeValue(forKey: taskId) if networkManager.activeDownloadTaskId == taskId { networkManager.activeDownloadTaskId = nil } case .downloading: networkManager.activeDownloadTaskId = taskId case .paused: if networkManager.activeDownloadTaskId == taskId { networkManager.activeDownloadTaskId = nil } default: break } networkManager.updateDockBadge() networkManager.objectWillChange.send() } } 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 } } private func handleDownloadError(_ error: Error, task: URLSessionTask) -> Error { let nsError = error as NSError switch nsError.code { case NSURLErrorCancelled: return NetworkError.cancelled case NSURLErrorTimedOut: return NetworkError.timeout case NSURLErrorNotConnectedToInternet: return NetworkError.noConnection case NSURLErrorCannotWriteToFile: if let expectedSize = task.response?.expectedContentLength { let fileManager = FileManager.default if let availableSpace = try? fileManager.attributesOfFileSystem(forPath: NSHomeDirectory())[.systemFreeSize] as? Int64 { return NetworkError.insufficientStorage(expectedSize, availableSpace) } } return NetworkError.downloadError("存储空间不足", error) default: return NetworkError.downloadError("下载失败: \(error.localizedDescription)", error) } } private func moveDownloadedFile(from location: URL, to destination: URL) throws { let fileManager = FileManager.default let destinationDirectory = destination.deletingLastPathComponent() do { if !fileManager.fileExists(atPath: destinationDirectory.path) { try fileManager.createDirectory(at: destinationDirectory, withIntermediateDirectories: true) } if fileManager.fileExists(atPath: destination.path) { try fileManager.removeItem(at: destination) } try fileManager.moveItem(at: location, to: destination) } catch { switch (error as NSError).code { case NSFileWriteNoPermissionError: throw NetworkError.filePermissionDenied(destination.path) case NSFileWriteOutOfSpaceError: throw NetworkError.insufficientStorage( try fileManager.attributesOfItem(atPath: location.path)[.size] as? Int64 ?? 0, try fileManager.attributesOfFileSystem(forPath: NSHomeDirectory())[.systemFreeSize] as? Int64 ?? 0 ) default: throw NetworkError.fileSystemError("移动文件失败", error) } } } private func createDownloadTask(url: URL?, resumeData: Data?, session: URLSession) throws -> URLSessionDownloadTask { if let resumeData = resumeData { return session.downloadTask(withResumeData: resumeData) } else if let url = url { var request = URLRequest(url: url) NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } return session.downloadTask(with: request) } else { throw NetworkError.invalidData("Neither URL nor resume data provided") } } 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 } } }