diff --git a/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 3a6a7bc..e0e91cb 100644 --- a/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Adobe Downloader.xcodeproj/xcuserdata/hejinhui.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -14,10 +14,10 @@ filePath = "Adobe Downloader/Commons/Structs.swift" startingColumnNumber = "9223372036854775807" endingColumnNumber = "9223372036854775807" - startingLineNumber = "152" - endingLineNumber = "152" - landmarkName = "ProductsToDownload" - landmarkType = "3"> + startingLineNumber = "27" + endingLineNumber = "27" + landmarkName = "unknown" + landmarkType = "0"> - - - - diff --git a/Adobe Downloader/Adobe DownloaderApp.swift b/Adobe Downloader/Adobe DownloaderApp.swift index 3453a90..e1fc249 100644 --- a/Adobe Downloader/Adobe DownloaderApp.swift +++ b/Adobe Downloader/Adobe DownloaderApp.swift @@ -4,7 +4,6 @@ import Sparkle @main struct Adobe_DownloaderApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - @StateObject private var networkManager = NetworkManager() @State private var showBackupAlert = false @State private var showTipsSheet = false @State private var showLanguagePicker = false @@ -17,6 +16,9 @@ struct Adobe_DownloaderApp: App { private let updaterController: SPUStandardUpdaterController init() { + globalNetworkService = NewNetworkService() + globalNetworkManager = NetworkManager() + globalNewDownloadUtils = NewDownloadUtils() updaterController = SPUStandardUpdaterController( startingUpdater: true, updaterDelegate: nil, @@ -56,7 +58,7 @@ struct Adobe_DownloaderApp: App { var body: some Scene { WindowGroup { ContentView() - .environmentObject(networkManager) + .environmentObject(globalNetworkManager) .frame(minWidth: 792, minHeight: 600) .tint(.blue) .task { @@ -64,7 +66,7 @@ struct Adobe_DownloaderApp: App { } .sheet(isPresented: $showCreativeCloudAlert) { ShouldExistsSetUpView() - .environmentObject(networkManager) + .environmentObject(globalNetworkManager) } .alert("Setup未备份提示", isPresented: $showBackupAlert) { Button("确定") { @@ -84,7 +86,7 @@ struct Adobe_DownloaderApp: App { showTipsSheet: $showTipsSheet, showLanguagePicker: $showLanguagePicker ) - .environmentObject(networkManager) + .environmentObject(globalNetworkManager) .sheet(isPresented: $showLanguagePicker) { LanguagePickerView(languages: AppStatics.supportedLanguages) { language in storage.defaultLanguage = language @@ -103,7 +105,7 @@ struct Adobe_DownloaderApp: App { Settings { AboutView(updater: updaterController.updater) - .environmentObject(networkManager) + .environmentObject(globalNetworkManager) } } @@ -111,8 +113,7 @@ struct Adobe_DownloaderApp: App { PrivilegedHelperManager.shared.checkInstall() await MainActor.run { - appDelegate.networkManager = networkManager - networkManager.loadSavedTasks() + globalNetworkManager.loadSavedTasks() } let needsBackup = !ModifySetup.isSetupBackup() diff --git a/Adobe Downloader/AppDelegate.swift b/Adobe Downloader/AppDelegate.swift index aba61a4..6dc80c0 100644 --- a/Adobe Downloader/AppDelegate.swift +++ b/Adobe Downloader/AppDelegate.swift @@ -3,7 +3,6 @@ import SwiftUI class AppDelegate: NSObject, NSApplicationDelegate { private var eventMonitor: Any? - var networkManager: NetworkManager? func applicationDidFinishLaunching(_ notification: Notification) { NSApp.mainMenu = nil @@ -20,22 +19,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - guard let manager = networkManager else { return .terminateNow } - - let hasActiveDownloads = manager.downloadTasks.contains { task in + let hasActiveDownloads = globalNetworkManager.downloadTasks.contains { task in if case .downloading = task.totalStatus { return true } return false } if hasActiveDownloads { Task { - for task in manager.downloadTasks { + for task in globalNetworkManager.downloadTasks { if case .downloading = task.totalStatus { - await manager.downloadUtils.pauseDownloadTask( + await globalNewDownloadUtils.pauseDownloadTask( taskId: task.id, reason: .other(String(localized: "程序即将退出")) ) - await manager.saveTask(task) + await globalNetworkManager.saveTask(task) } } @@ -50,9 +47,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { let response = alert.runModal() if response == .alertSecondButtonReturn { Task { - for task in manager.downloadTasks { + for task in globalNetworkManager.downloadTasks { if case .paused = task.totalStatus { - await manager.downloadUtils.resumeDownloadTask(taskId: task.id) + await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id) } } } @@ -71,6 +68,5 @@ class AppDelegate: NSObject, NSApplicationDelegate { if let monitor = eventMonitor { NSEvent.removeMonitor(monitor) } - networkManager = nil } } diff --git a/Adobe Downloader/Commons/Extensions.swift b/Adobe Downloader/Commons/Extensions.swift index 45245c8..f0c2f08 100644 --- a/Adobe Downloader/Commons/Extensions.swift +++ b/Adobe Downloader/Commons/Extensions.swift @@ -5,17 +5,3 @@ // import Foundation import AppKit - -extension NewDownloadTask { - var startTime: Date { - switch totalStatus { - case .downloading(let info): return info.startTime - case .completed(let info): return info.timestamp - info.totalTime - case .preparing(let info): return info.timestamp - case .paused(let info): return info.timestamp - case .failed(let info): return info.timestamp - case .retrying(let info): return info.nextRetryDate - 60 - case .waiting, .none: return createAt - } - } -} diff --git a/Adobe Downloader/Commons/Globals.swift b/Adobe Downloader/Commons/Globals.swift index de715cd..123baa5 100644 --- a/Adobe Downloader/Commons/Globals.swift +++ b/Adobe Downloader/Commons/Globals.swift @@ -5,15 +5,105 @@ // Created by X1a0He on 2/26/25. // -var globalStiResult: NewParseResult? -var globalCcmResult: NewParseResult? +// 下面是所有全局变量的私有存储 +private var _globalStiResult: NewParseResult? +private var _globalCcmResult: NewParseResult? +private var _globalCdn: String = "" +private var _globalNetworkService: NewNetworkService? +private var _globalNetworkManager: NetworkManager? +private var _globalNewDownloadUtils: NewDownloadUtils? +private var _globalCancelTracker: CancelTracker? + +// 计算属性,确保总是返回有效实例 +var globalStiResult: NewParseResult { + get { + if _globalStiResult == nil { + _globalStiResult = NewParseResult(products: [], cdn: "") + } + return _globalStiResult! + } + set { + _globalStiResult = newValue + } +} + +var globalCcmResult: NewParseResult { + get { + if _globalCcmResult == nil { + _globalCcmResult = NewParseResult(products: [], cdn: "") + } + return _globalCcmResult! + } + set { + _globalCcmResult = newValue + } +} + +var globalCdn: String { + get { + return _globalCdn + } + set { + _globalCdn = newValue + } +} + +var globalNetworkService: NewNetworkService { + get { + if _globalNetworkService == nil { + fatalError("NewNetworkService 没有被初始化,请确保在应用启动时初始化") + } + return _globalNetworkService! + } + set { + _globalNetworkService = newValue + } +} + +var globalNetworkManager: NetworkManager { + get { + if _globalNetworkManager == nil { + fatalError("NetworkManager 没有被初始化,请确保在应用启动时初始化") + } + return _globalNetworkManager! + } + set { + _globalNetworkManager = newValue + } +} + +var globalNewDownloadUtils: NewDownloadUtils { + get { + if _globalNewDownloadUtils == nil { + fatalError("NewDownloadUtils 没有被初始化,请确保在应用启动时初始化") + } + return _globalNewDownloadUtils! + } + set { + _globalNewDownloadUtils = newValue + } +} + +var globalCancelTracker: CancelTracker { + get { + if _globalCancelTracker == nil { + _globalCancelTracker = CancelTracker() + } + return _globalCancelTracker! + } + set { + _globalCancelTracker = newValue + } +} func getAllProducts() -> [Product] { var allProducts = [Product]() - if let stiProducts = globalStiResult?.products { + let stiProducts = globalStiResult.products + if !stiProducts.isEmpty { allProducts.append(contentsOf: stiProducts) } - if let ccmProducts = globalCcmResult?.products { + let ccmProducts = globalCcmResult.products + if !ccmProducts.isEmpty { allProducts.append(contentsOf: ccmProducts) } return allProducts diff --git a/Adobe Downloader/Commons/NewStructs.swift b/Adobe Downloader/Commons/NewStructs.swift index b0560cb..c55b513 100644 --- a/Adobe Downloader/Commons/NewStructs.swift +++ b/Adobe Downloader/Commons/NewStructs.swift @@ -1,4 +1,11 @@ -struct Product { +// +// Adobe Downloader +// +// Created by X1a0He on 2/26/25. +// + +import Foundation +struct Product: Codable, Equatable { var type: String var displayName: String var family: String @@ -10,19 +17,34 @@ struct Product { var version: String var id: String var hidden: Bool - - struct ProductIcon { - var value: String - var size: String + + func hasValidVersions(allowedPlatform: [String]) -> Bool { + return platforms.contains { platform in + allowedPlatform.contains(platform.id) && !platform.languageSet.isEmpty + } } - struct Platform { + struct ProductIcon: Codable, Equatable { + var value: String + var size: String + + var dimension: Int { + let components = size.split(separator: "x") + if components.count == 2, + let dimension = Int(components[0]) { + return dimension + } + return 0 + } + } + + struct Platform: Codable, Equatable { var languageSet: [LanguageSet] var modules: [Module] var range: [Range] var id: String - struct LanguageSet { + struct LanguageSet: Codable, Equatable { var manifestURL: String var dependencies: [Dependency] var productCode: String @@ -32,7 +54,7 @@ struct Product { var baseVersion: String var productVersion: String - struct Dependency { + struct Dependency: Codable, Equatable { var sapCode: String var baseVersion: String var productVersion: String @@ -40,25 +62,251 @@ struct Product { } } - struct Module { + struct Module: Codable, Equatable { var displayName: String var deploymentType: String var id: String } - struct Range { + struct Range: Codable, Equatable { var min: String var max: String } } - struct ReferencedProduct { + struct ReferencedProduct: Codable, Equatable { var sapCode: String var version: String } + + func getBestIcon() -> ProductIcon? { + if let icon = productIcons.first(where: { $0.size == "192x192" }) { + return icon + } + return productIcons.max(by: { $0.dimension < $1.dimension }) + } } struct NewParseResult { var products: [Product] var cdn: String } + +/* ========== */ +struct UniqueProduct { + var id: String + var displayName: String +} + +/* ========== */ +class DependenciesToDownload: ObservableObject, Codable { + // 别人依赖就他吗叫sapCode,Adobe也是傻逼,一会id一会sapCode + var sapCode: String + var version: String + var buildGuid: String + var applicationJson: String? + @Published var packages: [Package] = [] + @Published var completedPackages: Int = 0 + + var totalPackages: Int { + packages.count + } + + init(sapCode: String, version: String, buildGuid: String, applicationJson: String = "") { + self.sapCode = sapCode + self.version = version + self.buildGuid = buildGuid + self.applicationJson = applicationJson + } + + func updateCompletedPackages() { + Task { @MainActor in + completedPackages = packages.filter { $0.downloaded }.count + objectWillChange.send() + } + } + + enum CodingKeys: String, CodingKey { + case sapCode, version, buildGuid, applicationJson, packages + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(sapCode, forKey: .sapCode) + try container.encode(version, forKey: .version) + try container.encode(buildGuid, forKey: .buildGuid) + try container.encodeIfPresent(applicationJson, forKey: .applicationJson) + try container.encode(packages, forKey: .packages) + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + sapCode = try container.decode(String.self, forKey: .sapCode) + version = try container.decode(String.self, forKey: .version) + buildGuid = try container.decode(String.self, forKey: .buildGuid) + applicationJson = try container.decodeIfPresent(String.self, forKey: .applicationJson) + packages = try container.decode([Package].self, forKey: .packages) + completedPackages = 0 + } +} + +/* ========== */ +class Package: Identifiable, ObservableObject, Codable { + var id = UUID() + var type: String + var fullPackageName: String + var downloadSize: Int64 + var downloadURL: String + var packageVersion: String + + @Published var downloadedSize: Int64 = 0 { + didSet { + if downloadSize > 0 { + progress = Double(downloadedSize) / Double(downloadSize) + } + } + } + @Published var progress: Double = 0 + @Published var speed: Double = 0 + @Published var status: PackageStatus = .waiting + @Published var downloaded: Bool = false + + var lastUpdated: Date = Date() + var lastRecordedSize: Int64 = 0 + var retryCount: Int = 0 + var lastError: Error? + + var canRetry: Bool { + if case .failed = status { + return retryCount < 3 + } + return false + } + + func markAsFailed(_ error: Error) { + Task { @MainActor in + self.lastError = error + self.status = .failed(error.localizedDescription) + objectWillChange.send() + } + } + + func prepareForRetry() { + Task { @MainActor in + self.retryCount += 1 + self.status = .waiting + self.progress = 0 + self.speed = 0 + self.downloadedSize = 0 + objectWillChange.send() + } + } + + init(type: String, fullPackageName: String, downloadSize: Int64, downloadURL: String, packageVersion: String) { + self.type = type + self.fullPackageName = fullPackageName + self.downloadSize = downloadSize + self.downloadURL = downloadURL + self.packageVersion = packageVersion + } + + func updateProgress(downloadedSize: Int64, speed: Double) { + Task { @MainActor in + self.downloadedSize = downloadedSize + self.speed = speed + self.status = .downloading + objectWillChange.send() + } + } + + func markAsCompleted() { + Task { @MainActor in + self.downloaded = true + self.progress = 1.0 + self.speed = 0 + self.status = .completed + self.downloadedSize = downloadSize + objectWillChange.send() + } + } + + var formattedSize: String { + ByteCountFormatter.string(fromByteCount: downloadSize, countStyle: .file) + } + + var formattedDownloadedSize: String { + ByteCountFormatter.string(fromByteCount: downloadedSize, countStyle: .file) + } + + var formattedSpeed: String { + ByteCountFormatter.string(fromByteCount: Int64(speed), countStyle: .file) + "/s" + } + + var hasValidSize: Bool { + downloadSize > 0 + } + + func updateStatus(_ status: PackageStatus) { + Task { @MainActor in + self.status = status + objectWillChange.send() + } + } + + enum CodingKeys: String, CodingKey { + case id, type, fullPackageName, downloadSize, downloadURL, packageVersion + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(type, forKey: .type) + try container.encode(fullPackageName, forKey: .fullPackageName) + try container.encode(downloadSize, forKey: .downloadSize) + try container.encode(downloadURL, forKey: .downloadURL) + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UUID.self, forKey: .id) + type = try container.decode(String.self, forKey: .type) + fullPackageName = try container.decode(String.self, forKey: .fullPackageName) + downloadSize = try container.decode(Int64.self, forKey: .downloadSize) + downloadURL = try container.decode(String.self, forKey: .downloadURL) + packageVersion = try container.decode(String.self, forKey: .packageVersion) + } +} + +struct NetworkConstants { + static let downloadTimeout: TimeInterval = 300 + static let maxRetryAttempts = 3 + static let retryDelay: UInt64 = 3_000_000_000 + static let bufferSize = 1024 * 1024 + static let maxConcurrentDownloads = 3 + static let progressUpdateInterval: TimeInterval = 1 + + static func generateCookie() -> String { + let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + let randomString = (0..<26).map { _ in chars.randomElement()! } + return "fg=\(String(randomString))======" + } + + static var productsJSONURL: String { + "https://prod-rel-ffc-ccm.oobesaas.adobe.com/adobe-ffc-external/core/v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")/products/all" + } + + static let applicationJsonURL = "https://cdn-ffc.oobesaas.adobe.com/core/v3/applications" + + static var adobeRequestHeaders: [String: String] { + [ + "x-adobe-app-id": "accc-apps-panel-desktop", + "x-api-key": "Creative Cloud_v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")_4", + "User-Agent": "Creative Cloud/6.4.0.361/Mac-15.1", + "Cookie": generateCookie() + ] + } + + static let downloadHeaders = [ + "User-Agent": "Creative Cloud" + ] +} diff --git a/Adobe Downloader/Commons/Structs.swift b/Adobe Downloader/Commons/Structs.swift index 00e0345..39566c0 100644 --- a/Adobe Downloader/Commons/Structs.swift +++ b/Adobe Downloader/Commons/Structs.swift @@ -1,299 +1,141 @@ +//// +//// Adobe Downloader +//// +//// Created by X1a0He on 2024/10/30. +//// +//import Foundation // -// Adobe Downloader // -// Created by X1a0He on 2024/10/30. +//class ProductsToDownload: ObservableObject, Codable { +// var sapCode: String +// var version: String +// var buildGuid: String +// var applicationJson: String? +// @Published var packages: [Package] = [] +// @Published var completedPackages: Int = 0 +// +// var totalPackages: Int { +// packages.count +// } // -import Foundation - -class Package: Identifiable, ObservableObject, Codable { - var id = UUID() - var type: String - var fullPackageName: String - var downloadSize: Int64 - var downloadURL: String - var packageVersion: String - - @Published var downloadedSize: Int64 = 0 { - didSet { - if downloadSize > 0 { - progress = Double(downloadedSize) / Double(downloadSize) - } - } - } - @Published var progress: Double = 0 - @Published var speed: Double = 0 - @Published var status: PackageStatus = .waiting - @Published var downloaded: Bool = false - - var lastUpdated: Date = Date() - var lastRecordedSize: Int64 = 0 - var retryCount: Int = 0 - var lastError: Error? - - var canRetry: Bool { - if case .failed = status { - return retryCount < 3 - } - return false - } - - func markAsFailed(_ error: Error) { - Task { @MainActor in - self.lastError = error - self.status = .failed(error.localizedDescription) - objectWillChange.send() - } - } - - func prepareForRetry() { - Task { @MainActor in - self.retryCount += 1 - self.status = .waiting - self.progress = 0 - self.speed = 0 - self.downloadedSize = 0 - objectWillChange.send() - } - } - - init(type: String, fullPackageName: String, downloadSize: Int64, downloadURL: String, packageVersion: String) { - self.type = type - self.fullPackageName = fullPackageName - self.downloadSize = downloadSize - self.downloadURL = downloadURL - self.packageVersion = packageVersion - } - - func updateProgress(downloadedSize: Int64, speed: Double) { - Task { @MainActor in - self.downloadedSize = downloadedSize - self.speed = speed - self.status = .downloading - objectWillChange.send() - } - } - - func markAsCompleted() { - Task { @MainActor in - self.downloaded = true - self.progress = 1.0 - self.speed = 0 - self.status = .completed - self.downloadedSize = downloadSize - objectWillChange.send() - } - } - - var formattedSize: String { - ByteCountFormatter.string(fromByteCount: downloadSize, countStyle: .file) - } - - var formattedDownloadedSize: String { - ByteCountFormatter.string(fromByteCount: downloadedSize, countStyle: .file) - } - - var formattedSpeed: String { - ByteCountFormatter.string(fromByteCount: Int64(speed), countStyle: .file) + "/s" - } - - var hasValidSize: Bool { - downloadSize > 0 - } - - func updateStatus(_ status: PackageStatus) { - Task { @MainActor in - self.status = status - objectWillChange.send() - } - } - - enum CodingKeys: String, CodingKey { - case id, type, fullPackageName, downloadSize, downloadURL, packageVersion - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(type, forKey: .type) - try container.encode(fullPackageName, forKey: .fullPackageName) - try container.encode(downloadSize, forKey: .downloadSize) - try container.encode(downloadURL, forKey: .downloadURL) - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(UUID.self, forKey: .id) - type = try container.decode(String.self, forKey: .type) - fullPackageName = try container.decode(String.self, forKey: .fullPackageName) - downloadSize = try container.decode(Int64.self, forKey: .downloadSize) - downloadURL = try container.decode(String.self, forKey: .downloadURL) - packageVersion = try container.decode(String.self, forKey: .packageVersion) - } -} - -class ProductsToDownload: ObservableObject, Codable { - var sapCode: String - var version: String - var buildGuid: String - var applicationJson: String? - @Published var packages: [Package] = [] - @Published var completedPackages: Int = 0 - - var totalPackages: Int { - packages.count - } - - init(sapCode: String, version: String, buildGuid: String, applicationJson: String = "") { - self.sapCode = sapCode - self.version = version - self.buildGuid = buildGuid - self.applicationJson = applicationJson - } - - func updateCompletedPackages() { - Task { @MainActor in - completedPackages = packages.filter { $0.downloaded }.count - objectWillChange.send() - } - } - - enum CodingKeys: String, CodingKey { - case sapCode, version, buildGuid, applicationJson, packages - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(sapCode, forKey: .sapCode) - try container.encode(version, forKey: .version) - try container.encode(buildGuid, forKey: .buildGuid) - try container.encodeIfPresent(applicationJson, forKey: .applicationJson) - try container.encode(packages, forKey: .packages) - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - sapCode = try container.decode(String.self, forKey: .sapCode) - version = try container.decode(String.self, forKey: .version) - buildGuid = try container.decode(String.self, forKey: .buildGuid) - applicationJson = try container.decodeIfPresent(String.self, forKey: .applicationJson) - packages = try container.decode([Package].self, forKey: .packages) - completedPackages = 0 - } -} - -struct SapCodes: Identifiable { - var id: String { sapCode } - var sapCode: String - var displayName: String -} - -struct Sap: Codable, Equatable { - var id: String { sapCode } - var hidden: Bool - var displayName: String - var sapCode: String - var versions: [String: Versions] - var icons: [ProductIcon] - var productsToDownload: [ProductsToDownload]? = nil - - enum CodingKeys: String, CodingKey { - case hidden, displayName, sapCode, versions, icons - } - - static func == (lhs: Sap, rhs: Sap) -> Bool { - return lhs.sapCode == rhs.sapCode && - lhs.hidden == rhs.hidden && - lhs.displayName == rhs.displayName && - lhs.versions == rhs.versions && - lhs.icons == rhs.icons - } - - struct Versions: Codable, Equatable { - var sapCode: String - var baseVersion: String - var productVersion: String - var apPlatform: String - var dependencies: [Dependencies] - var buildGuid: String - - struct Dependencies: Codable, Equatable { - var sapCode: String - var version: String - } - } - - struct ProductIcon: Codable, Equatable { - let size: String - let url: String - - var dimension: Int { - let components = size.split(separator: "x") - if components.count == 2, - let dimension = Int(components[0]) { - return dimension - } - return 0 - } - } - - var isValid: Bool { !hidden } - - func getBestIcon() -> ProductIcon? { - if let icon = icons.first(where: { $0.size == "192x192" }) { - return icon - } - return icons.max(by: { $0.dimension < $1.dimension }) - } - - func hasValidVersions(allowedPlatform: [String]) -> Bool { - if hidden { return false } - - for version in Array(versions.values).reversed() { - if !version.buildGuid.isEmpty && - (!version.buildGuid.contains("/") || sapCode == "APRO") && - allowedPlatform.contains(version.apPlatform) { - return true - } - } - return false - } -} - -struct NetworkConstants { - static let downloadTimeout: TimeInterval = 300 - static let maxRetryAttempts = 3 - static let retryDelay: UInt64 = 3_000_000_000 - static let bufferSize = 1024 * 1024 - static let maxConcurrentDownloads = 3 - static let progressUpdateInterval: TimeInterval = 1 - - static func generateCookie() -> String { - let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - let randomString = (0..<26).map { _ in chars.randomElement()! } - return "fg=\(String(randomString))======" - } - - static var productsXmlURL: String { - "https://prod-rel-ffc-ccm.oobesaas.adobe.com/adobe-ffc-external/core/v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")/products/all" - } - - static let applicationJsonURL = "https://cdn-ffc.oobesaas.adobe.com/core/v3/applications" - - static var adobeRequestHeaders: [String: String] { - [ - "x-adobe-app-id": "accc-apps-panel-desktop", - "x-api-key": "Creative Cloud_v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")_4", - "User-Agent": "Creative Cloud/6.4.0.361/Mac-15.1", - "Cookie": generateCookie() - ] - } - - static let downloadHeaders = [ - "User-Agent": "Creative Cloud" - ] -} - -struct ProductsResponse: Codable { - let products: [String: Sap] - let cdn: String -} +// init(sapCode: String, version: String, buildGuid: String, applicationJson: String = "") { +// self.sapCode = sapCode +// self.version = version +// self.buildGuid = buildGuid +// self.applicationJson = applicationJson +// } +// +// func updateCompletedPackages() { +// Task { @MainActor in +// completedPackages = packages.filter { $0.downloaded }.count +// objectWillChange.send() +// } +// } +// +// enum CodingKeys: String, CodingKey { +// case sapCode, version, buildGuid, applicationJson, packages +// } +// +// func encode(to encoder: Encoder) throws { +// var container = encoder.container(keyedBy: CodingKeys.self) +// try container.encode(sapCode, forKey: .sapCode) +// try container.encode(version, forKey: .version) +// try container.encode(buildGuid, forKey: .buildGuid) +// try container.encodeIfPresent(applicationJson, forKey: .applicationJson) +// try container.encode(packages, forKey: .packages) +// } +// +// required init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: CodingKeys.self) +// sapCode = try container.decode(String.self, forKey: .sapCode) +// version = try container.decode(String.self, forKey: .version) +// buildGuid = try container.decode(String.self, forKey: .buildGuid) +// applicationJson = try container.decodeIfPresent(String.self, forKey: .applicationJson) +// packages = try container.decode([Package].self, forKey: .packages) +// completedPackages = 0 +// } +//} +// +//struct SapCodes: Identifiable { +// var id: String { sapCode } +// var sapCode: String +// var displayName: String +//} +// +//struct Sap: Codable, Equatable { +// var id: String { sapCode } +// var hidden: Bool +// var displayName: String +// var sapCode: String +// var versions: [String: Versions] +// var icons: [ProductIcon] +// var productsToDownload: [ProductsToDownload]? = nil +// +// enum CodingKeys: String, CodingKey { +// case hidden, displayName, sapCode, versions, icons +// } +// +// static func == (lhs: Sap, rhs: Sap) -> Bool { +// return lhs.sapCode == rhs.sapCode && +// lhs.hidden == rhs.hidden && +// lhs.displayName == rhs.displayName && +// lhs.versions == rhs.versions && +// lhs.icons == rhs.icons +// } +// +// struct Versions: Codable, Equatable { +// var sapCode: String +// var baseVersion: String +// var productVersion: String +// var apPlatform: String +// var dependencies: [Dependencies] +// var buildGuid: String +// +// struct Dependencies: Codable, Equatable { +// var sapCode: String +// var version: String +// } +// } +// +// struct ProductIcon: Codable, Equatable { +// let size: String +// let url: String +// +// var dimension: Int { +// let components = size.split(separator: "x") +// if components.count == 2, +// let dimension = Int(components[0]) { +// return dimension +// } +// return 0 +// } +// } +// +// var isValid: Bool { !hidden } +// +// func getBestIcon() -> ProductIcon? { +// if let icon = icons.first(where: { $0.size == "192x192" }) { +// return icon +// } +// return icons.max(by: { $0.dimension < $1.dimension }) +// } +// +// func hasValidVersions(allowedPlatform: [String]) -> Bool { +// if hidden { return false } +// +// for version in Array(versions.values).reversed() { +// if !version.buildGuid.isEmpty && +// (!version.buildGuid.contains("/") || sapCode == "APRO") && +// allowedPlatform.contains(version.apPlatform) { +// return true +// } +// } +// return false +// } +//} +// +// +//struct ProductsResponse: Codable { +// let products: [String: Sap] +// let cdn: String +//} diff --git a/Adobe Downloader/ContentView.swift b/Adobe Downloader/ContentView.swift index 703cbaa..5d2ffcf 100644 --- a/Adobe Downloader/ContentView.swift +++ b/Adobe Downloader/ContentView.swift @@ -1,13 +1,12 @@ import SwiftUI struct ContentView: View { - @EnvironmentObject private var networkManager: NetworkManager @State private var isRefreshing = false @State private var errorMessage: String? @State private var showDownloadManager = false @State private var searchText = "" @State private var currentApiVersion = StorageData.shared.apiVersion - @State private var cachedProducts: [Sap] = [] + @State private var cachedProducts: [UniqueProduct] = [] private var apiVersion: String { get { StorageData.shared.apiVersion } @@ -17,14 +16,14 @@ struct ContentView: View { } } - private var filteredProducts: [Sap] { + private var filteredProducts: [UniqueProduct] { if searchText.isEmpty { return cachedProducts } return cachedProducts.filter { $0.displayName.localizedCaseInsensitiveContains(searchText) || - $0.sapCode.localizedCaseInsensitiveContains(searchText) + $0.id.localizedCaseInsensitiveContains(searchText) } } @@ -33,10 +32,21 @@ struct ContentView: View { } private func updateProductsCache() { - let products = networkManager.saps.values + // 先获取有效的产品 + let validProducts = globalCcmResult.products .filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) } + + // 使用字典合并相同ID的产品 + var uniqueProductsDict = [String: UniqueProduct]() + for product in validProducts { + uniqueProductsDict[product.id] = UniqueProduct(id: product.id, displayName: product.displayName) + } + + // 转换为数组并按显示名称排序 + let uniqueProducts = Array(uniqueProductsDict.values) .sorted { $0.displayName < $1.displayName } - cachedProducts = products + + cachedProducts = uniqueProducts } var body: some View { @@ -47,7 +57,7 @@ struct ContentView: View { set: { newValue in StorageData.shared.downloadAppleSilicon = newValue Task { - await networkManager.fetchProducts() + await globalNetworkManager.fetchProducts() } } )) { @@ -106,8 +116,8 @@ struct ContentView: View { .buttonStyle(.borderless) .overlay( Group { - if !networkManager.downloadTasks.isEmpty { - Text("\(networkManager.downloadTasks.count)") + if !globalNetworkManager.downloadTasks.isEmpty { + Text("\(globalNetworkManager.downloadTasks.count)") .font(.caption2) .padding(3) .background(Color.blue) @@ -138,7 +148,7 @@ struct ContentView: View { Color(NSColor.windowBackgroundColor) .ignoresSafeArea() - switch networkManager.loadingState { + switch globalNetworkManager.loadingState { case .idle, .loading: ProgressView("正在加载...") .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -160,7 +170,7 @@ struct ContentView: View { .padding(.bottom, 10) Button(action: { - networkManager.retryFetchData() + globalNetworkManager.retryFetchData() }) { HStack() { Image(systemName: "arrow.clockwise") @@ -216,16 +226,16 @@ struct ContentView: View { } .sheet(isPresented: $showDownloadManager) { DownloadManagerView() - .environmentObject(networkManager) + .environmentObject(globalNetworkManager) } .onAppear { - if networkManager.saps.isEmpty { + if globalCcmResult.products.isEmpty { refreshData() } else { updateProductsCache() } } - .onChange(of: networkManager.saps) { _ in + .onChange(of: globalNetworkManager.saps) { _ in updateProductsCache() } } @@ -235,7 +245,7 @@ struct ContentView: View { errorMessage = nil Task { - await networkManager.fetchProducts() + await globalNetworkManager.fetchProducts() await MainActor.run { updateProductsCache() isRefreshing = false @@ -274,4 +284,3 @@ struct SearchField: View { .environmentObject(networkManager) .frame(width: 792, height: 600) } - diff --git a/Adobe Downloader/Models/DownloadTask.swift b/Adobe Downloader/Models/DownloadTask.swift index 45aea18..1288905 100644 --- a/Adobe Downloader/Models/DownloadTask.swift +++ b/Adobe Downloader/Models/DownloadTask.swift @@ -21,98 +21,98 @@ extension DownloadStatus { } } -class NewDownloadTask: Identifiable, ObservableObject, Equatable { - let id = UUID() - var sapCode: String - let version: String - let language: String - let displayName: String - let directory: URL - var productsToDownload: [ProductsToDownload] - var retryCount: Int - let createAt: Date - var displayInstallButton: Bool - @Published var totalStatus: DownloadStatus? - @Published var totalProgress: Double - @Published var totalDownloadedSize: Int64 - @Published var totalSize: Int64 - @Published var totalSpeed: Double - @Published var currentPackage: Package? { - didSet { - objectWillChange.send() - } - } - let platform: String - - var status: DownloadStatus { - totalStatus ?? .waiting - } - - var destinationURL: URL { directory } - - var downloadedSize: Int64 { - get { totalDownloadedSize } - set { totalDownloadedSize = newValue } - } - - var progress: Double { - get { totalProgress } - set { totalProgress = newValue } - } - - var speed: Double { - get { totalSpeed } - set { totalSpeed = newValue } - } - - var formattedTotalSize: String { - ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file) - } - - var formattedDownloadedSize: String { - ByteCountFormatter.string(fromByteCount: totalDownloadedSize, countStyle: .file) - } - - @Published var completedPackages: Int = 0 - @Published var totalPackages: Int = 0 - - func setStatus(_ newStatus: DownloadStatus) { - DispatchQueue.main.async { - self.totalStatus = newStatus - self.objectWillChange.send() - } - } - - func updateProgress(downloaded: Int64, total: Int64, speed: Double) { - DispatchQueue.main.async { - self.totalDownloadedSize = downloaded - self.totalSize = total - self.totalSpeed = speed - self.totalProgress = total > 0 ? Double(downloaded) / Double(total) : 0 - self.objectWillChange.send() - } - } - - init(sapCode: String, version: String, language: String, displayName: String, directory: URL, productsToDownload: [ProductsToDownload] = [], retryCount: Int = 0, createAt: Date, totalStatus: DownloadStatus? = nil, totalProgress: Double, totalDownloadedSize: Int64 = 0, totalSize: Int64 = 0, totalSpeed: Double = 0, currentPackage: Package? = nil, platform: String) { - self.sapCode = sapCode - self.version = version - self.language = language - self.displayName = displayName - self.directory = directory - self.productsToDownload = productsToDownload - self.retryCount = retryCount - self.createAt = createAt - self.totalStatus = totalStatus - self.totalProgress = totalProgress - self.totalDownloadedSize = totalDownloadedSize - self.totalSize = totalSize - self.totalSpeed = totalSpeed - self.currentPackage = currentPackage - self.displayInstallButton = sapCode != "APRO" - self.platform = platform - } - - static func == (lhs: NewDownloadTask, rhs: NewDownloadTask) -> Bool { - return lhs.id == rhs.id - } -} +//class NewDownloadTask: Identifiable, ObservableObject, Equatable { +// let id = UUID() +// var sapCode: String +// let version: String +// let language: String +// let displayName: String +// let directory: URL +// var productsToDownload: [ProductsToDownload] +// var retryCount: Int +// let createAt: Date +// var displayInstallButton: Bool +// @Published var totalStatus: DownloadStatus? +// @Published var totalProgress: Double +// @Published var totalDownloadedSize: Int64 +// @Published var totalSize: Int64 +// @Published var totalSpeed: Double +// @Published var currentPackage: Package? { +// didSet { +// objectWillChange.send() +// } +// } +// let platform: String +// +// var status: DownloadStatus { +// totalStatus ?? .waiting +// } +// +// var destinationURL: URL { directory } +// +// var downloadedSize: Int64 { +// get { totalDownloadedSize } +// set { totalDownloadedSize = newValue } +// } +// +// var progress: Double { +// get { totalProgress } +// set { totalProgress = newValue } +// } +// +// var speed: Double { +// get { totalSpeed } +// set { totalSpeed = newValue } +// } +// +// var formattedTotalSize: String { +// ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file) +// } +// +// var formattedDownloadedSize: String { +// ByteCountFormatter.string(fromByteCount: totalDownloadedSize, countStyle: .file) +// } +// +// @Published var completedPackages: Int = 0 +// @Published var totalPackages: Int = 0 +// +// func setStatus(_ newStatus: DownloadStatus) { +// DispatchQueue.main.async { +// self.totalStatus = newStatus +// self.objectWillChange.send() +// } +// } +// +// func updateProgress(downloaded: Int64, total: Int64, speed: Double) { +// DispatchQueue.main.async { +// self.totalDownloadedSize = downloaded +// self.totalSize = total +// self.totalSpeed = speed +// self.totalProgress = total > 0 ? Double(downloaded) / Double(total) : 0 +// self.objectWillChange.send() +// } +// } +// +// init(sapCode: String, version: String, language: String, displayName: String, directory: URL, productsToDownload: [ProductsToDownload] = [], retryCount: Int = 0, createAt: Date, totalStatus: DownloadStatus? = nil, totalProgress: Double, totalDownloadedSize: Int64 = 0, totalSize: Int64 = 0, totalSpeed: Double = 0, currentPackage: Package? = nil, platform: String) { +// self.sapCode = sapCode +// self.version = version +// self.language = language +// self.displayName = displayName +// self.directory = directory +// self.productsToDownload = productsToDownload +// self.retryCount = retryCount +// self.createAt = createAt +// self.totalStatus = totalStatus +// self.totalProgress = totalProgress +// self.totalDownloadedSize = totalDownloadedSize +// self.totalSize = totalSize +// self.totalSpeed = totalSpeed +// self.currentPackage = currentPackage +// self.displayInstallButton = sapCode != "APRO" +// self.platform = platform +// } +// +// static func == (lhs: NewDownloadTask, rhs: NewDownloadTask) -> Bool { +// return lhs.id == rhs.id +// } +//} diff --git a/Adobe Downloader/Models/NewDownloadTask.swift b/Adobe Downloader/Models/NewDownloadTask.swift new file mode 100644 index 0000000..818f485 --- /dev/null +++ b/Adobe Downloader/Models/NewDownloadTask.swift @@ -0,0 +1,99 @@ +// +// NewDownloadTask.swift +// Adobe Downloader +// +// Created by X1a0He on 2/26/25. +// +import Foundation + +class NewDownloadTask: Identifiable, ObservableObject { + let id = UUID() + var productId: String + let productVersion: String + let language: String + let displayName: String + let directory: URL + var dependenciesToDownload: [DependenciesToDownload] + var retryCount: Int + let createAt: Date + var displayInstallButton: Bool + + let platform: String + + @Published var totalStatus: DownloadStatus? + @Published var totalProgress: Double + @Published var totalDownloadedSize: Int64 + @Published var totalSize: Int64 + @Published var totalSpeed: Double + @Published var completedPackages: Int = 0 + @Published var totalPackages: Int = 0 + @Published var currentPackage: Package? { + didSet { + objectWillChange.send() + } + } + + var status: DownloadStatus { + totalStatus ?? .waiting + } + + var destinationURL: URL { directory } + + var formattedTotalSize: String { + ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file) + } + + var formattedDownloadedSize: String { + ByteCountFormatter.string(fromByteCount: totalDownloadedSize, countStyle: .file) + } + + var startTime: Date { + switch totalStatus { + case .downloading(let info): return info.startTime + case .completed(let info): return info.timestamp - info.totalTime + case .preparing(let info): return info.timestamp + case .paused(let info): return info.timestamp + case .failed(let info): return info.timestamp + case .retrying(let info): return info.nextRetryDate - 60 + case .waiting, .none: return createAt + } + } + + func setStatus(_ newStatus: DownloadStatus) { + DispatchQueue.main.async { + self.totalStatus = newStatus + self.objectWillChange.send() + } + } + + func updateProgress(downloaded: Int64, total: Int64, speed: Double) { + DispatchQueue.main.async { + self.totalDownloadedSize = downloaded + self.totalSize = total + self.totalSpeed = speed + self.totalProgress = total > 0 ? Double(downloaded) / Double(total) : 0 + self.objectWillChange.send() + } + } + + init(productId: String, productVersion: String, language: String, displayName: String, directory: URL, dependenciesToDownload: [DependenciesToDownload] = [], retryCount: Int = 0, createAt: Date, totalStatus: DownloadStatus? = nil, totalProgress: Double, totalDownloadedSize: Int64 = 0, totalSize: Int64 = 0, totalSpeed: Double = 0, currentPackage: Package? = nil, platform: String) { + self.productId = productId + self.productVersion = productVersion + self.language = language + self.displayName = displayName + self.directory = directory + self.dependenciesToDownload = dependenciesToDownload + self.retryCount = retryCount + self.createAt = createAt + self.totalStatus = totalStatus + self.totalProgress = totalProgress + self.totalDownloadedSize = totalDownloadedSize + self.totalSize = totalSize + self.totalSpeed = totalSpeed + self.currentPackage = currentPackage + self.displayInstallButton = productId != "APRO" + self.platform = platform + } + + +} diff --git a/Adobe Downloader/Services/NetworkService.swift b/Adobe Downloader/Services/NetworkService.swift index 4280815..610e9b4 100644 --- a/Adobe Downloader/Services/NetworkService.swift +++ b/Adobe Downloader/Services/NetworkService.swift @@ -1,92 +1,91 @@ -import Foundation - -class NetworkService { - typealias ProductsData = (products: [String: Sap], cdn: String, sapCodes: [SapCodes]) - - private func makeProductsURL() throws -> URL { - var components = URLComponents(string: NetworkConstants.productsXmlURL) - components?.queryItems = [ - URLQueryItem(name: "channel", value: "ccm"), - URLQueryItem(name: "channel", value: "sti"), - URLQueryItem(name: "platform", value: "macarm64,macuniversal,osx10-64,osx10"), - URLQueryItem(name: "_type", value: "json"), - URLQueryItem(name: "productType", value: "Desktop") - ] - - guard let url = components?.url else { - throw NetworkError.invalidURL(NetworkConstants.productsXmlURL) - } - return url - } - - private func configureRequest(_ request: inout URLRequest, headers: [String: String]) { - headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } - } - - func fetchProductsData() async throws -> ProductsData { - let url = try makeProductsURL() - var request = URLRequest(url: url) - request.httpMethod = "GET" - configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders) - - 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, nil) - } - - guard let jsonString = String(data: data, encoding: .utf8) else { - throw NetworkError.invalidData("无法解码JSON数据") - } - - let result: ProductsData = try await Task.detached(priority: .userInitiated) { - let parseResult = try JSONParser.parse(jsonString: jsonString) - // 测试新API - try NewJSONParser.parseStiProducts(jsonString: jsonString) - try NewJSONParser.parseCcmProducts(jsonString: jsonString) - let products = parseResult.products, cdn = parseResult.cdn - - let sapCodes = products.values - .filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) } - .map { SapCodes(sapCode: $0.sapCode, displayName: $0.displayName) } - - return (products, cdn, sapCodes) - }.value - - return result - } - - 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("无法将响应数据转换为json符串") - } - - return jsonString - } -} +//import Foundation +// +//class NetworkService { +// typealias ProductsData = (products: [String: Sap], sapCodes: [SapCodes]) +// +// private func makeProductsURL() throws -> URL { +// var components = URLComponents(string: NetworkConstants.productsJSONURL) +// components?.queryItems = [ +// URLQueryItem(name: "channel", value: "ccm"), +// URLQueryItem(name: "channel", value: "sti"), +// URLQueryItem(name: "platform", value: "macarm64,macuniversal,osx10-64,osx10"), +// URLQueryItem(name: "_type", value: "json"), +// URLQueryItem(name: "productType", value: "Desktop") +// ] +// +// guard let url = components?.url else { +// throw NetworkError.invalidURL(NetworkConstants.productsJSONURL) +// } +// return url +// } +// +// private func configureRequest(_ request: inout URLRequest, headers: [String: String]) { +// headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } +// } +// +// func fetchProductsData() async throws -> ProductsData { +// let url = try makeProductsURL() +// var request = URLRequest(url: url) +// request.httpMethod = "GET" +// configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders) +// +// 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, nil) +// } +// +// guard let jsonString = String(data: data, encoding: .utf8) else { +// throw NetworkError.invalidData("无法解码JSON数据") +// } +// +// let result: ProductsData = try await Task.detached(priority: .userInitiated) { +// let parseResult = try JSONParser.parse(jsonString: jsonString) +// // 测试新API +// try NewJSONParser.parse(jsonString: jsonString) +// let products = parseResult.products, cdn = parseResult.cdn +// +// let sapCodes = products.values +// .filter { $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) } +// .map { SapCodes(sapCode: $0.sapCode, displayName: $0.displayName) } +// +// return (products, sapCodes) +// }.value +// +// return result +// } +// +// 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("无法将响应数据转换为json符串") +// } +// +// return jsonString +// } +//} diff --git a/Adobe Downloader/Services/NewNetworkService.swift b/Adobe Downloader/Services/NewNetworkService.swift new file mode 100644 index 0000000..bd0bffa --- /dev/null +++ b/Adobe Downloader/Services/NewNetworkService.swift @@ -0,0 +1,107 @@ +// +// NewNetworkService.swift +// Adobe Downloader +// +// Created by X1a0He on 2/26/25. +// + +import Foundation + +class NewNetworkService { + typealias ProductsData = ([Product], [UniqueProduct]) + + private func makeProductsURL() throws -> URL { + var components = URLComponents(string: NetworkConstants.productsJSONURL) + components?.queryItems = [ + URLQueryItem(name: "channel", value: "ccm"), + URLQueryItem(name: "channel", value: "sti"), + URLQueryItem(name: "platform", value: "macarm64,macuniversal,osx10-64,osx10"), + URLQueryItem(name: "_type", value: "json"), + URLQueryItem(name: "productType", value: "Desktop") + ] + + guard let url = components?.url else { + throw NetworkError.invalidURL(NetworkConstants.productsJSONURL) + } + return url + } + + private func configureRequest(_ request: inout URLRequest, headers: [String: String]) { + headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + } + + func fetchProductsData() async throws -> ProductsData { + let url = try makeProductsURL() + var request = URLRequest(url: url) + request.httpMethod = "GET" + configureRequest(&request, headers: NetworkConstants.adobeRequestHeaders) + + 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, nil) + } + + guard let jsonString = String(data: data, encoding: .utf8) else { + throw NetworkError.invalidData("无法解码JSON数据") + } + + let result: ProductsData = try await Task.detached(priority: .userInitiated) { + try NewJSONParser.parse(jsonString: jsonString) + + let products = globalCcmResult.products + + if products.isEmpty { + return ([], []) + } + + let validProducts = products.filter { + $0.hasValidVersions(allowedPlatform: StorageData.shared.allowedPlatform) + } + + var uniqueProductsDict = [String: UniqueProduct]() + for product in validProducts { + uniqueProductsDict[product.id] = UniqueProduct(id: product.id, displayName: product.displayName) + } + let uniqueProducts = Array(uniqueProductsDict.values) + + return (products, uniqueProducts) + }.value + + return result + } + + 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("无法将响应数据转换为json符串") + } + + return jsonString + } +} diff --git a/Adobe Downloader/Utils/DownloadUtils.swift b/Adobe Downloader/Utils/DownloadUtils.swift index dbc1239..094b5e8 100644 --- a/Adobe Downloader/Utils/DownloadUtils.swift +++ b/Adobe Downloader/Utils/DownloadUtils.swift @@ -1,1363 +1,1358 @@ +//// +//// Adobe Downloader +//// +//// Created by X1a0He on 2024/10/30. +//// +//import Foundation +//import Network +//import Combine +//import AppKit // -// Adobe Downloader +//class DownloadUtils { +// typealias ProgressUpdate = (bytesWritten: Int64, totalWritten: Int64, expectedToWrite: Int64) // -// Created by X1a0He on 2024/10/30. +// private weak var networkManager: NetworkManager? +// private let cancelTracker: CancelTracker // -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 } - - Task { - 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) - } - } - - if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { - task.setStatus(.paused(DownloadStatus.PauseInfo( - reason: reason, - timestamp: Date(), - resumable: true - ))) - await networkManager?.saveTask(task) - await MainActor.run { - networkManager?.objectWillChange.send() - } - } - } - - func resumeDownloadTask(taskId: UUID) async { - if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { - task.setStatus(.downloading(DownloadStatus.DownloadInfo( - fileName: task.currentPackage?.fullPackageName ?? "", - currentPackageIndex: 0, - totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count }, - startTime: Date(), - estimatedTimeRemaining: nil - ))) - await networkManager?.saveTask(task) - await MainActor.run { - networkManager?.objectWillChange.send() - } - - 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) - - if let task = await 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 - ))) - - await networkManager?.saveTask(task) - await MainActor.run { - 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 { - await MainActor.run { - 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() - } - await networkManager?.saveTask(task) - await MainActor.run { - 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 - ))) - } - await 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 - ))) - } - await 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 { - await MainActor.run { - 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 networkManager?.saveTask(task) - - await MainActor.run { - networkManager?.updateDockBadge() - 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() - - Task { - await networkManager?.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 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, - let downloadTask = await cancelTracker.downloadTasks[taskId] { - let resumeData = await withCheckedContinuation { continuation in - downloadTask.cancel(byProducingResumeData: { data in - continuation.resume(returning: data) - }) - } - if let resumeData = resumeData { - await cancelTracker.storeResumeData(taskId, data: resumeData) - } - } - - 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 { - 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) - } - - await networkManager?.saveTask(task) - await MainActor.run { - 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, .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) - } - } - - @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 - } - } -} +// 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 } +// +// Task { +// 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) +// } +// } +// +// if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { +// task.setStatus(.paused(DownloadStatus.PauseInfo( +// reason: reason, +// timestamp: Date(), +// resumable: true +// ))) +// await networkManager?.saveTask(task) +// await MainActor.run { +// networkManager?.objectWillChange.send() +// } +// } +// } +// +// func resumeDownloadTask(taskId: UUID) async { +// if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { +// task.setStatus(.downloading(DownloadStatus.DownloadInfo( +// fileName: task.currentPackage?.fullPackageName ?? "", +// currentPackageIndex: 0, +// totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count }, +// startTime: Date(), +// estimatedTimeRemaining: nil +// ))) +// await networkManager?.saveTask(task) +// await MainActor.run { +// networkManager?.objectWillChange.send() +// } +// +// 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) +// +// if let task = await 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 +// ))) +// +// await networkManager?.saveTask(task) +// await MainActor.run { +// 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 { +// await MainActor.run { +// 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() +// } +// await networkManager?.saveTask(task) +// await MainActor.run { +// 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 +// ))) +// } +// await networkManager?.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 { +// 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 +// ))) +// } +// await 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 +// } +// +// try await downloadPackage(package: package, task: task, product: product, url: URL(string: globalCdn + package.downloadURL)!) +// } +// } +// +// func downloadAPRO(task: NewDownloadTask, productInfo: Sap.Versions) async throws { +// guard let networkManager = networkManager else { return } +// +// let manifestURL = globalCdn + 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 { +// await MainActor.run { +// 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 networkManager?.saveTask(task) +// +// await MainActor.run { +// networkManager?.updateDockBadge() +// 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() +// +// Task { +// await networkManager?.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 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, +// let downloadTask = await cancelTracker.downloadTasks[taskId] { +// let resumeData = await withCheckedContinuation { continuation in +// downloadTask.cancel(byProducingResumeData: { data in +// continuation.resume(returning: data) +// }) +// } +// if let resumeData = resumeData { +// await cancelTracker.storeResumeData(taskId, data: resumeData) +// } +// } +// +// 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 { +// 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) +// } +// +// await networkManager?.saveTask(task) +// await MainActor.run { +// 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, .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) +// } +// } +// +// @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 +// } +// } +//} diff --git a/Adobe Downloader/Utils/JSONParser.swift b/Adobe Downloader/Utils/JSONParser.swift index 9c3df63..0cb31c9 100644 --- a/Adobe Downloader/Utils/JSONParser.swift +++ b/Adobe Downloader/Utils/JSONParser.swift @@ -1,172 +1,172 @@ +//// +//// JSONParser.swift +//// Adobe Downloader +//// +//// Created by X1a0He on 11/18/24. +//// // -// JSONParser.swift -// Adobe Downloader +//import Foundation // -// Created by X1a0He on 11/18/24. +// struct ParseResult { +// var products: [String: Sap] +// var cdn: String +// } // - -import Foundation - - struct ParseResult { - var products: [String: Sap] - var cdn: String - } - -class JSONParser { - static func parse(jsonString: String) throws -> ParseResult { - guard let jsonData = jsonString.data(using: .utf8), - let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { - throw ParserError.invalidJSON - } - let apiVersion = Int(StorageData.shared.apiVersion) ?? 6 - - return try parseProductsJSON(jsonObject: jsonObject, apiVersion: apiVersion) - } - - private static func parseProductsJSON(jsonObject: [String: Any], apiVersion: Int) throws -> ParseResult { - let cdnPath: [String] - if apiVersion == 6 { - cdnPath = ["channels", "channel"] - } else { - cdnPath = ["channel"] - } - - func getValue(from dict: [String: Any], path: [String]) -> Any? { - var current: Any = dict - for key in path { - guard let dict = current as? [String: Any], - let value = dict[key] else { - return nil - } - current = value - } - return current - } - - var channelArray: [[String: Any]] = [] - if let channels = getValue(from: jsonObject, path: cdnPath) { - if let array = channels as? [[String: Any]] { - channelArray = array - } else if let dict = channels as? [String: Any], - let array = dict["channel"] as? [[String: Any]] { - channelArray = array - } - } - - guard let firstChannel = channelArray.first, - let cdn = (firstChannel["cdn"] as? [String: Any])?["secure"] as? String else { - throw ParserError.missingCDN - } - - var products = [String: Sap](minimumCapacity: 200) - - for channel in channelArray { - let channelName = channel["name"] as? String - let hidden = channelName != "ccm" - - guard let productsContainer = channel["products"] as? [String: Any], - let productArray = productsContainer["product"] as? [[String: Any]] else { - continue - } - - for product in productArray { - guard let sap = product["id"] as? String, - let displayName = product["displayName"] as? String, - let productVersion = product["version"] as? String else { - continue - } - - if products[sap] == nil { - let icons = (product["productIcons"] as? [String: Any])?["icon"] as? [[String: Any]] ?? [] - let productIcons = icons.compactMap { icon -> Sap.ProductIcon? in - guard let size = icon["size"] as? String, - let value = icon["value"] as? String else { - return nil - } - return Sap.ProductIcon(size: size, url: value) - } - - products[sap] = Sap( - hidden: hidden, - displayName: displayName, - sapCode: sap, - versions: [:], - icons: productIcons - ) - } - - if let platforms = product["platforms"] as? [String: Any], - let platformArray = platforms["platform"] as? [[String: Any]] { - - for platform in platformArray { - guard let platformId = platform["id"] as? String, - let languageSets = platform["languageSet"] as? [[String: Any]], - let languageSet = languageSets.first else { - continue - } - - if let existingVersion = products[sap]?.versions[productVersion], - StorageData.shared.allowedPlatform.contains(existingVersion.apPlatform) { - break - } - - var baseVersion = languageSet["baseVersion"] as? String ?? "" - var buildGuid = languageSet["buildGuid"] as? String ?? "" - var finalProductVersion = productVersion - - if sap == "APRO" { - baseVersion = productVersion - if apiVersion == 4 || apiVersion == 5 { - if let appVersion = (languageSet["nglLicensingInfo"] as? [String: Any])?["appVersion"] as? String { - finalProductVersion = appVersion - } - } else if apiVersion == 6 { - if let builds = jsonObject["builds"] as? [String: Any], - let buildArray = builds["build"] as? [[String: Any]] { - for build in buildArray { - if build["id"] as? String == sap && build["version"] as? String == baseVersion, - let appVersion = (build["nglLicensingInfo"] as? [String: Any])?["appVersion"] as? String { - finalProductVersion = appVersion - break - } - } - } - } - - if let urls = languageSet["urls"] as? [String: Any], - let manifestURL = urls["manifestURL"] as? String { - buildGuid = manifestURL - } - } - - var dependencies: [Sap.Versions.Dependencies] = [] - if let deps = languageSet["dependencies"] as? [String: Any], - let depArray = deps["dependency"] as? [[String: Any]] { - dependencies = depArray.compactMap { dep in - guard let sapCode = dep["sapCode"] as? String, - let version = dep["baseVersion"] as? String else { - return nil - } - return Sap.Versions.Dependencies(sapCode: sapCode, version: version) - } - } - - if !buildGuid.isEmpty { - let version = Sap.Versions( - sapCode: sap, - baseVersion: baseVersion, - productVersion: finalProductVersion, - apPlatform: platformId, - dependencies: dependencies, - buildGuid: buildGuid - ) - products[sap]?.versions[finalProductVersion] = version - } - } - } - } - } - - return ParseResult(products: products, cdn: cdn) - } -} +//class JSONParser { +// static func parse(jsonString: String) throws -> ParseResult { +// guard let jsonData = jsonString.data(using: .utf8), +// let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { +// throw ParserError.invalidJSON +// } +// let apiVersion = Int(StorageData.shared.apiVersion) ?? 6 +// +// return try parseProductsJSON(jsonObject: jsonObject, apiVersion: apiVersion) +// } +// +// private static func parseProductsJSON(jsonObject: [String: Any], apiVersion: Int) throws -> ParseResult { +// let cdnPath: [String] +// if apiVersion == 6 { +// cdnPath = ["channels", "channel"] +// } else { +// cdnPath = ["channel"] +// } +// +// func getValue(from dict: [String: Any], path: [String]) -> Any? { +// var current: Any = dict +// for key in path { +// guard let dict = current as? [String: Any], +// let value = dict[key] else { +// return nil +// } +// current = value +// } +// return current +// } +// +// var channelArray: [[String: Any]] = [] +// if let channels = getValue(from: jsonObject, path: cdnPath) { +// if let array = channels as? [[String: Any]] { +// channelArray = array +// } else if let dict = channels as? [String: Any], +// let array = dict["channel"] as? [[String: Any]] { +// channelArray = array +// } +// } +// +// guard let firstChannel = channelArray.first, +// let cdn = (firstChannel["cdn"] as? [String: Any])?["secure"] as? String else { +// throw ParserError.missingCDN +// } +// +// var products = [String: Sap](minimumCapacity: 200) +// +// for channel in channelArray { +// let channelName = channel["name"] as? String +// let hidden = channelName != "ccm" +// +// guard let productsContainer = channel["products"] as? [String: Any], +// let productArray = productsContainer["product"] as? [[String: Any]] else { +// continue +// } +// +// for product in productArray { +// guard let sap = product["id"] as? String, +// let displayName = product["displayName"] as? String, +// let productVersion = product["version"] as? String else { +// continue +// } +// +// if products[sap] == nil { +// let icons = (product["productIcons"] as? [String: Any])?["icon"] as? [[String: Any]] ?? [] +// let productIcons = icons.compactMap { icon -> Sap.ProductIcon? in +// guard let size = icon["size"] as? String, +// let value = icon["value"] as? String else { +// return nil +// } +// return Sap.ProductIcon(size: size, url: value) +// } +// +// products[sap] = Sap( +// hidden: hidden, +// displayName: displayName, +// sapCode: sap, +// versions: [:], +// icons: productIcons +// ) +// } +// +// if let platforms = product["platforms"] as? [String: Any], +// let platformArray = platforms["platform"] as? [[String: Any]] { +// +// for platform in platformArray { +// guard let platformId = platform["id"] as? String, +// let languageSets = platform["languageSet"] as? [[String: Any]], +// let languageSet = languageSets.first else { +// continue +// } +// +// if let existingVersion = products[sap]?.versions[productVersion], +// StorageData.shared.allowedPlatform.contains(existingVersion.apPlatform) { +// break +// } +// +// var baseVersion = languageSet["baseVersion"] as? String ?? "" +// var buildGuid = languageSet["buildGuid"] as? String ?? "" +// var finalProductVersion = productVersion +// +// if sap == "APRO" { +// baseVersion = productVersion +// if apiVersion == 4 || apiVersion == 5 { +// if let appVersion = (languageSet["nglLicensingInfo"] as? [String: Any])?["appVersion"] as? String { +// finalProductVersion = appVersion +// } +// } else if apiVersion == 6 { +// if let builds = jsonObject["builds"] as? [String: Any], +// let buildArray = builds["build"] as? [[String: Any]] { +// for build in buildArray { +// if build["id"] as? String == sap && build["version"] as? String == baseVersion, +// let appVersion = (build["nglLicensingInfo"] as? [String: Any])?["appVersion"] as? String { +// finalProductVersion = appVersion +// break +// } +// } +// } +// } +// +// if let urls = languageSet["urls"] as? [String: Any], +// let manifestURL = urls["manifestURL"] as? String { +// buildGuid = manifestURL +// } +// } +// +// var dependencies: [Sap.Versions.Dependencies] = [] +// if let deps = languageSet["dependencies"] as? [String: Any], +// let depArray = deps["dependency"] as? [[String: Any]] { +// dependencies = depArray.compactMap { dep in +// guard let sapCode = dep["sapCode"] as? String, +// let version = dep["baseVersion"] as? String else { +// return nil +// } +// return Sap.Versions.Dependencies(sapCode: sapCode, version: version) +// } +// } +// +// if !buildGuid.isEmpty { +// let version = Sap.Versions( +// sapCode: sap, +// baseVersion: baseVersion, +// productVersion: finalProductVersion, +// apPlatform: platformId, +// dependencies: dependencies, +// buildGuid: buildGuid +// ) +// products[sap]?.versions[finalProductVersion] = version +// } +// } +// } +// } +// } +// +// return ParseResult(products: products, cdn: cdn) +// } +//} diff --git a/Adobe Downloader/NetworkManager.swift b/Adobe Downloader/Utils/NetworkManager.swift similarity index 86% rename from Adobe Downloader/NetworkManager.swift rename to Adobe Downloader/Utils/NetworkManager.swift index 838c0eb..924040d 100644 --- a/Adobe Downloader/NetworkManager.swift +++ b/Adobe Downloader/Utils/NetworkManager.swift @@ -8,15 +8,10 @@ import SwiftUI class NetworkManager: ObservableObject { typealias ProgressUpdate = (bytesWritten: Int64, totalWritten: Int64, expectedToWrite: Int64) @Published var isConnected = false - @Published var saps: [String: Sap] = [:] - @Published var cdn: String = "" - @Published var sapCodes: [SapCodes] = [] @Published var loadingState: LoadingState = .idle @Published var downloadTasks: [NewDownloadTask] = [] @Published var installationState: InstallationState = .idle @Published var installCommand: String = "" - private let cancelTracker = CancelTracker() - internal var downloadUtils: DownloadUtils! internal var progressObservers: [UUID: NSKeyValueObservation] = [:] internal var activeDownloadTaskId: UUID? internal var monitor = NWPathMonitor() @@ -46,26 +41,24 @@ class NetworkManager: ObservableObject { case failed(Error) } - private let networkService: NetworkService - - init(networkService: NetworkService = NetworkService(), - downloadUtils: DownloadUtils? = nil) { - - self.networkService = networkService - self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker) - - TaskPersistenceManager.shared.setCancelTracker(cancelTracker) + init() { + TaskPersistenceManager.shared.setCancelTracker(globalCancelTracker) configureNetworkMonitor() } func fetchProducts() async { loadingState = .loading do { - let (saps, cdn, sapCodes) = try await networkService.fetchProductsData() + let (saps, sapCodes) = try await globalNetworkService.fetchProductsData() + + let (newProducts, uniqueProducts) = try await globalNetworkService.fetchProductsData() + print("新产品数量: \(newProducts.count), 唯一产品数量: \(uniqueProducts.count), CDN: \(globalCdn)") + for uniqueProduct in uniqueProducts { + print("新唯一产品: \(uniqueProduct)") + } + + await MainActor.run { - self.saps = saps - self.cdn = cdn - self.sapCodes = sapCodes self.loadingState = .success } } catch { @@ -74,17 +67,19 @@ class NetworkManager: ObservableObject { } } } - func startDownload(sap: Sap, selectedVersion: String, language: String, destinationURL: URL) async throws { - guard let productInfo = self.saps[sap.sapCode]?.versions[selectedVersion] else { - throw NetworkError.invalidData("无法获取产品信息") + func startDownload(productId: String, selectedVersion: String, language: String, destinationURL: URL) async throws { + // 从 globalCcmResult 中获取 productId 对应的 ProductInfo + guard let productInfo = globalCcmResult.products.first(where: { $0.id == productId }) else { + throw NetworkError.productNotFound } + let task = NewDownloadTask( - sapCode: sap.sapCode, - version: selectedVersion, + productId: productInfo.id, + productVersion: selectedVersion, language: language, - displayName: sap.displayName, + displayName: productInfo.displayName, directory: destinationURL, - productsToDownload: [], + dependenciesToDownload: [], createAt: Date(), totalStatus: .preparing(DownloadStatus.PrepareInfo( message: "正在准备下载...", @@ -95,15 +90,14 @@ class NetworkManager: ObservableObject { totalDownloadedSize: 0, totalSize: 0, totalSpeed: 0, - platform: productInfo.apPlatform - ) - + platform: "") + downloadTasks.append(task) updateDockBadge() await saveTask(task) do { - try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform, saps: saps) + try await globalNewDownloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform) } catch { task.setStatus(.failed(DownloadStatus.FailureInfo( message: error.localizedDescription, @@ -118,16 +112,10 @@ class NetworkManager: ObservableObject { } } - var cdnUrl: String { - get async { - await MainActor.run { cdn } - } - } - func removeTask(taskId: UUID, removeFiles: Bool = true) { Task { - await cancelTracker.cancel(taskId) - + await globalCancelTracker.cancel(taskId) + if let task = downloadTasks.first(where: { $0.id == taskId }) { if task.status.isActive { task.setStatus(.failed(DownloadStatus.FailureInfo( @@ -165,11 +153,8 @@ class NetworkManager: ObservableObject { while retryCount < maxRetries { do { - let (saps, cdn, sapCodes) = try await networkService.fetchProductsData() + let (saps, sapCodes) = try await globalNetworkService.fetchProductsData() await MainActor.run { - self.saps = saps - self.cdn = cdn - self.sapCodes = sapCodes self.loadingState = .success self.isFetchingProducts = false } @@ -302,10 +287,10 @@ class NetworkManager: ObservableObject { } func getApplicationInfo(buildGuid: String) async throws -> String { - return try await networkService.getApplicationInfo(buildGuid: buildGuid) + return try await globalNetworkService.getApplicationInfo(buildGuid: buildGuid) } - func isVersionDownloaded(sap: Sap, version: String, language: String) -> URL? { + func isVersionDownloaded(product: Product, version: String, language: String) -> URL? { if let task = downloadTasks.first(where: { $0.sapCode == sap.sapCode && $0.version == version && diff --git a/Adobe Downloader/Utils/NewDownloadUtils.swift b/Adobe Downloader/Utils/NewDownloadUtils.swift new file mode 100644 index 0000000..cc34a57 --- /dev/null +++ b/Adobe Downloader/Utils/NewDownloadUtils.swift @@ -0,0 +1,947 @@ +// +// NewDownloadUtils.swift +// Adobe Downloader +// +// Created by X1a0He on 2/26/25. +// +import Foundation +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 + + 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 } + + Task { + progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) + } + + lastUpdateTime = now + lastBytes = totalBytesWritten + } + } + + func handleDownload(task: NewDownloadTask, productInfo: Product, allowedPlatform: [String]) async throws { + if productInfo.id == "APRO" { + try await downloadAPRO(task: task, productInfo: productInfo) + return + } + + var dependenciesToDownload: [DependenciesToDownload] = [] + let firstPlatform = productInfo.platforms.first + let buildGuid = firstPlatform?.languageSet.first?.buildGuid ?? "" + + dependenciesToDownload.append(DependenciesToDownload(sapCode: productInfo.id, version: productInfo.version, buildGuid: buildGuid)) + + let dependencies = firstPlatform?.languageSet.first?.dependencies + + if let dependencies = dependencies { + for dependency in dependencies { + dependenciesToDownload.append(DependenciesToDownload(sapCode: dependency.sapCode, version: dependency.productVersion, buildGuid: dependency.buildGuid)) + } + } + + for dependencyToDownload in dependenciesToDownload { + print("\(dependencyToDownload.sapCode), \(dependencyToDownload.version), \(dependencyToDownload.buildGuid)") + } + + for dependencyToDownload in dependenciesToDownload { + await MainActor.run { + task.setStatus(.preparing(DownloadStatus.PrepareInfo( + message: String(localized: "正在处理 \(dependencyToDownload.sapCode) 的包信息..."), + timestamp: Date(), + stage: .fetchingInfo + ))) + } + + let jsonString = try await getApplicationInfo(buildGuid: dependencyToDownload.buildGuid) + let productDir = task.directory.appendingPathComponent("\(dependencyToDownload.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: String.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.dependenciesToDownload = finalProducts + task.totalSize = totalSize + } + + await startDownloadProcess(task: 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 }) { + let driverXml = generateDriverXML( + sapCode: task.productId, + version: task.productVersion, + language: task.language, + productInfo: productInfo, + displayName: task.displayName + ) + 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 { + if let resumeData = await globalCancelTracker.getResumeData(task.id) { + try await downloadPackage(package: package, task: task, product: dependencyToDownload, resumeData: resumeData) + } else { + 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 task != nil else { return } + + let (errorMessage, isRecoverable) = classifyError(error) + + if isRecoverable, + let downloadTask = await globalCancelTracker?.downloadTasks[taskId] { + let resumeData = await withCheckedContinuation { continuation in + downloadTask.cancel(byProducingResumeData: { data in + continuation.resume(returning: data) + }) + } + if let resumeData = resumeData { + await globalCancelTracker?.storeResumeData(taskId, data: resumeData) + } + } + + 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 let currentPackage = task.currentPackage { + let destinationDir = task.directory + .appendingPathComponent("\(task.productId)") + let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName) + try? FileManager.default.removeItem(at: fileURL) + } + + 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 task != nil else { return } + + await MainActor.run { + task.setStatus(.downloading(DownloadStatus.DownloadInfo( + fileName: task.currentPackage?.fullPackageName ?? "", + currentPackageIndex: 0, + totalPackages: task.dependenciesToDownload.reduce(0) { $0 + $1.packages.count }, + startTime: Date(), + estimatedTimeRemaining: nil + ))) + } + + await globalNetworkManager.saveTask(task) + await MainActor.run { + globalNetworkManager.objectWillChange.send() + } + + if task.productId == "APRO" { + if let resumeData = await globalCancelTracker?.getResumeData(taskId), + let currentPackage = task.currentPackage, + let product = task.dependenciesToDownload.first { + try? await downloadPackage( + package: currentPackage, + task: task, + product: product, + resumeData: resumeData + ) + } + } else { + await startDownloadProcess(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, resumeData: Data? = nil) async throws { + var lastUpdateTime = Date() + var lastBytes: Int64 = 0 + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) 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 { + await MainActor.run { + 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) + await MainActor.run { + 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 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 globalCancelTracker?.registerTask(task.id, task: downloadTask, session: session) + await globalCancelTracker?.clearResumeData(task.id) + downloadTask.resume() + } + } + } + + func generateDriverXML(sapCode: String, version: String, language: String, productInfo: Product, displayName: String) -> String { + // 获取匹配的 platform 和 languageSet + guard let platform = productInfo.platforms.first(where: { $0.id == "mac" }), + let languageSet = platform.languageSet.first else { + return "" + } + + // 构建依赖列表 + let dependencies = languageSet.dependencies.map { dependency in + """ + + \(dependency.sapCode) + \(dependency.baseVersion) + \(dependency.sapCode) + + """ + }.joined(separator: "\n") + + return """ + + + Adobe \(displayName) + \(sapCode) + \(version) + mac + \(sapCode) + + \(dependencies) + + + + /Applications + \(language) + + + """ + } + + func downloadAPRO(task: NewDownloadTask, productInfo: Product) async throws { + let firstPlatform = productInfo.platforms.first + let buildGuid = firstPlatform?.languageSet.first?.buildGuid ?? "" + + let manifestURL = globalCdn + buildGuid + guard let url = URL(string: manifestURL) else { + throw NetworkError.invalidURL(manifestURL) + } + + 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 (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.productId)_\(firstPlatform?.languageSet.first?.productVersion ?? "unknown")_\(firstPlatform?.id ?? "unknown").dmg", + downloadSize: assetSize, + downloadURL: downloadPath, + packageVersion: "" + ) + + await MainActor.run { + let product = DependenciesToDownload(sapCode: task.productId, version: firstPlatform?.languageSet.first?.productVersion ?? "unknown", buildGuid: 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) 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 { + await MainActor.run { + 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) + + await MainActor.run { + 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) ?? false) { + continuation.resume(throwing: NetworkError.cancelled) + return + } + + downloadTask.resume() + } + } + } + + func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async { + let task = await globalCancelTracker?.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 globalCancelTracker?.storeResumeData(taskId, data: data) + } + } + + if let task = await globalNetworkManager.downloadTasks.first(where: { $0.id == taskId }) { + task.setStatus(.paused(DownloadStatus.PauseInfo( + reason: reason, + timestamp: Date(), + resumable: true + ))) + await globalNetworkManager.saveTask(task) + await MainActor.run { + globalNetworkManager.objectWillChange.send() + } + } + } + + 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 + } + } +} diff --git a/Adobe Downloader/Utils/NewJSONParser.swift b/Adobe Downloader/Utils/NewJSONParser.swift index ee4957d..4c385b2 100644 --- a/Adobe Downloader/Utils/NewJSONParser.swift +++ b/Adobe Downloader/Utils/NewJSONParser.swift @@ -14,14 +14,21 @@ import Foundation */ class NewJSONParser { - static func parse(jsonString: String) throws -> NewParseResult { + static func parse(jsonString: String) throws { guard let jsonData = jsonString.data(using: .utf8), let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { throw ParserError.invalidJSON } let apiVersion = Int(StorageData.shared.apiVersion) ?? 6 - return try parseSti(jsonObject: jsonObject, apiVersion: apiVersion) -// return try parseCcm(jsonObject: jsonObject, apiVersion: apiVersion) + globalStiResult = try parseSti(jsonObject: jsonObject, apiVersion: apiVersion) + globalCcmResult = try parseCcm(jsonObject: jsonObject, apiVersion: apiVersion) + + // 更新全局 CDN + if !globalCcmResult.cdn.isEmpty { + globalCdn = globalCcmResult.cdn + } else if !globalStiResult.cdn.isEmpty { + globalCdn = globalStiResult.cdn + } } static func parseStiProducts(jsonString: String) throws { @@ -32,6 +39,9 @@ class NewJSONParser { let apiVersion = Int(StorageData.shared.apiVersion) ?? 6 let result = try parseSti(jsonObject: jsonObject, apiVersion: apiVersion) globalStiResult = result + + // 更新全局 CDN + globalCdn = result.cdn } static func parseCcmProducts(jsonString: String) throws { @@ -42,6 +52,9 @@ class NewJSONParser { let apiVersion = Int(StorageData.shared.apiVersion) ?? 6 let result = try parseCcm(jsonObject: jsonObject, apiVersion: apiVersion) globalCcmResult = result + + // 更新全局 CDN + globalCdn = result.cdn } private static func parseSti(jsonObject: [String: Any], apiVersion: Int) throws -> NewParseResult { @@ -273,8 +286,8 @@ class NewJSONParser { var productVersion = "" var buildGuid = "" - if let stiProducts = globalStiResult?.products { - let matchingProducts = stiProducts.filter { $0.id == sapCode } + if !globalStiResult.products.isEmpty { + let matchingProducts = globalStiResult.products.filter { $0.id == sapCode } if let latestProduct = matchingProducts.sorted(by: { return AppStatics.compareVersions($0.version, $1.version) < 0 diff --git a/Adobe Downloader/Utils/TaskPersistenceManager.swift b/Adobe Downloader/Utils/TaskPersistenceManager.swift index 23c46e6..d6cfbe5 100644 --- a/Adobe Downloader/Utils/TaskPersistenceManager.swift +++ b/Adobe Downloader/Utils/TaskPersistenceManager.swift @@ -26,8 +26,8 @@ class TaskPersistenceManager { func saveTask(_ task: NewDownloadTask) async { let fileName = getTaskFileName( - sapCode: task.sapCode, - version: task.version, + sapCode: task.productId, + version: task.productVersion, language: task.language, platform: task.platform ) @@ -43,12 +43,12 @@ class TaskPersistenceManager { } let taskData = TaskData( - sapCode: task.sapCode, - version: task.version, + sapCode: task.productId, + version: task.productVersion, language: task.language, displayName: task.displayName, directory: task.directory, - productsToDownload: task.productsToDownload.map { product in + productsToDownload: task.dependenciesToDownload.map { product in ProductData( sapCode: product.sapCode, version: product.version, @@ -120,8 +120,8 @@ class TaskPersistenceManager { let decoder = JSONDecoder() let taskData = try decoder.decode(TaskData.self, from: data) - let products = taskData.productsToDownload.map { productData -> ProductsToDownload in - let product = ProductsToDownload( + let products = taskData.productsToDownload.map { productData -> DependenciesToDownload in + let product = DependenciesToDownload( sapCode: productData.sapCode, version: productData.version, buildGuid: productData.buildGuid, @@ -174,12 +174,12 @@ class TaskPersistenceManager { } let task = NewDownloadTask( - sapCode: taskData.sapCode, - version: taskData.version, + productId: taskData.sapCode, + productVersion: taskData.version, language: taskData.language, displayName: taskData.displayName, directory: taskData.directory, - productsToDownload: products, + dependenciesToDownload: products, retryCount: taskData.retryCount, createAt: taskData.createAt, totalStatus: initialStatus, @@ -209,8 +209,8 @@ class TaskPersistenceManager { func removeTask(_ task: NewDownloadTask) { let fileName = getTaskFileName( - sapCode: task.sapCode, - version: task.version, + sapCode: task.productId, + version: task.productVersion, language: task.language, platform: task.platform ) @@ -228,7 +228,7 @@ class TaskPersistenceManager { platform: platform ) - let product = ProductsToDownload( + let product = DependenciesToDownload( sapCode: sapCode, version: version, buildGuid: "", @@ -249,12 +249,12 @@ class TaskPersistenceManager { product.packages = [package] let task = NewDownloadTask( - sapCode: sapCode, - version: version, + productId: sapCode, + productVersion: version, language: language, displayName: displayName, directory: directory, - productsToDownload: [product], + dependenciesToDownload: [product], retryCount: 0, createAt: Date(), totalStatus: .completed(DownloadStatus.CompletionInfo( diff --git a/Adobe Downloader/Views/AppCardView.swift b/Adobe Downloader/Views/AppCardView.swift index 5fc88c7..498e05e 100644 --- a/Adobe Downloader/Views/AppCardView.swift +++ b/Adobe Downloader/Views/AppCardView.swift @@ -468,38 +468,6 @@ private struct SheetModifier: ViewModifier { } } -#Preview { - let networkManager = NetworkManager() - let sap = Sap( - hidden: false, - displayName: "Photoshop", - sapCode: "PHSP", - versions: [ - "25.0.0": Sap.Versions( - sapCode: "PHSP", - baseVersion: "25.0.0", - productVersion: "25.0.0", - apPlatform: "macuniversal", - dependencies: [ - Sap.Versions.Dependencies(sapCode: "ACR", version: "9.6"), - Sap.Versions.Dependencies(sapCode: "COCM", version: "1.0"), - Sap.Versions.Dependencies(sapCode: "COSY", version: "2.4.1") - ], - buildGuid: "" - ) - ], - icons: [ - Sap.ProductIcon( - size: "192x192", - url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/25.0.0/192x192.png" - ) - ] - ) - - return AppCardView(sap: sap) - .environmentObject(networkManager) -} - struct AlertModifier: ViewModifier { @ObservedObject var viewModel: AppCardViewModel let confirmRedownload: Bool diff --git a/Adobe Downloader/Views/DownloadProgressView.swift b/Adobe Downloader/Views/DownloadProgressView.swift index 87ab6df..8f7c96d 100644 --- a/Adobe Downloader/Views/DownloadProgressView.swift +++ b/Adobe Downloader/Views/DownloadProgressView.swift @@ -301,43 +301,45 @@ struct DownloadProgressView: View { } private func loadIcon() { - if let sap = networkManager.saps[task.sapCode], - let bestIcon = sap.getBestIcon(), - let iconURL = URL(string: bestIcon.url) { - - if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) { - self.iconImage = cachedImage - return - } - - Task { - do { - var request = URLRequest(url: iconURL) - request.timeoutInterval = 10 - - let (data, response) = try await URLSession.shared.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode), - let image = NSImage(data: data) else { - throw URLError(.badServerResponse) - } - - IconCache.shared.setIcon(image, for: bestIcon.url) - - await MainActor.run { - self.iconImage = image - } - } catch { - if let localImage = NSImage(named: task.sapCode) { + let product = globalCcmResult.products.first { $0.id == task.productId } + if product != nil { + if let bestIcon = product.getBestIcon(), + let iconURL = URL(string: bestIcon.url) { + + if let cachedImage = IconCache.shared.getIcon(for: bestIcon.url) { + self.iconImage = cachedImage + return + } + + Task { + do { + var request = URLRequest(url: iconURL) + request.timeoutInterval = 10 + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode), + let image = NSImage(data: data) else { + throw URLError(.badServerResponse) + } + + IconCache.shared.setIcon(image, for: bestIcon.url) + await MainActor.run { - self.iconImage = localImage + self.iconImage = image + } + } catch { + if let localImage = NSImage(named: task.sapCode) { + await MainActor.run { + self.iconImage = localImage + } } } } + } else if let localImage = NSImage(named: task.productId) { + self.iconImage = localImage } - } else if let localImage = NSImage(named: task.sapCode) { - self.iconImage = localImage } } @@ -377,7 +379,7 @@ struct DownloadProgressView: View { HStack(spacing: 4) { Text(task.displayName) .font(.headline) - Text(task.version) + Text(task.productVersion) .foregroundColor(.secondary) } @@ -430,7 +432,7 @@ struct DownloadProgressView: View { .progressViewStyle(.linear) } - if !task.productsToDownload.isEmpty { + if !task.dependenciesToDownload.isEmpty { Divider() VStack(alignment: .leading, spacing: 6) { @@ -547,7 +549,7 @@ struct DownloadProgressView: View { } struct ProductRow: View { - @ObservedObject var product: ProductsToDownload + @ObservedObject var dependencies: DependenciesToDownload let isCurrentProduct: Bool @Binding var expandedProducts: Set @@ -688,145 +690,3 @@ struct PackageRow: View { .cornerRadius(6) } } - -#Preview("下载中") { - let product = ProductsToDownload( - sapCode: "AUDT", - version: "25.0", - buildGuid: "123" - ) - product.packages = [ - Package( - type: "Application", - fullPackageName: "AdobeAudition25All", - downloadSize: 878454797, - downloadURL: "https://example.com/download", - packageVersion: "25.0.0.1" - ) - ] - - return DownloadProgressView( - task: NewDownloadTask( - sapCode: "AUDT", - version: "25.0", - language: "zh_CN", - displayName: "Adobe Audition", - directory: URL(fileURLWithPath: "/Users/test/Downloads/Adobe Audition_25.0-zh_CN-macuniversal"), - productsToDownload: [product], - createAt: Date(), - totalStatus: .downloading(DownloadStatus.DownloadInfo( - fileName: "AdobeAudition25All_stripped.zip", - currentPackageIndex: 0, - totalPackages: 2, - startTime: Date(), - estimatedTimeRemaining: nil - )), - totalProgress: 0.45, - totalDownloadedSize: 457424883, - totalSize: 878454797, - totalSpeed: 1024 * 1024 * 2, - platform: "macuniversal" - ), - onCancel: {}, - onPause: {}, - onResume: {}, - onRetry: {}, - onRemove: {} - ) - .environmentObject(NetworkManager()) -} - -#Preview("已完成") { - let product = ProductsToDownload( - sapCode: "AUDT", - version: "25.0", - buildGuid: "123" - ) - let package = Package( - type: "Application", - fullPackageName: "AdobeAudition25All", - downloadSize: 878454797, - downloadURL: "https://example.com/download", - packageVersion: "25.0.0.1" - ) - package.status = .completed - package.progress = 1.0 - package.downloadedSize = 878454797 - package.downloaded = true - product.packages = [package] - - return DownloadProgressView( - task: NewDownloadTask( - sapCode: "AUDT", - version: "25.0", - language: "zh_CN", - displayName: "Adobe Audition", - directory: URL(fileURLWithPath: "/Users/test/Downloads/Adobe Audition_25.0-zh_CN-macuniversal"), - productsToDownload: [product], - createAt: Date(), - totalStatus: .completed(DownloadStatus.CompletionInfo( - timestamp: Date(), - totalTime: 120, - totalSize: 878454797 - )), - totalProgress: 1.0, - totalDownloadedSize: 878454797, - totalSize: 878454797, - totalSpeed: 0, - platform: "macuniversal" - ), - onCancel: {}, - onPause: {}, - onResume: {}, - onRetry: {}, - onRemove: {} - ) - .environmentObject(NetworkManager()) -} - -#Preview("暂停") { - let product = ProductsToDownload( - sapCode: "AUDT", - version: "25.0", - buildGuid: "123" - ) - let package = Package( - type: "Application", - fullPackageName: "AdobeAudition25All", - downloadSize: 878454797, - downloadURL: "https://example.com/download", - packageVersion: "25.0.0.1" - ) - package.status = .paused - package.progress = 0.52 - package.downloadedSize = 457424883 - product.packages = [package] - - return DownloadProgressView( - task: NewDownloadTask( - sapCode: "AUDT", - version: "25.0", - language: "zh_CN", - displayName: "Adobe Audition", - directory: URL(fileURLWithPath: "/Users/test/Downloads/Adobe Audition_25.0-zh_CN-macuniversal"), - productsToDownload: [product], - createAt: Date(), - totalStatus: .paused(DownloadStatus.PauseInfo( - reason: .userRequested, - timestamp: Date(), - resumable: true - )), - totalProgress: 0.52, - totalDownloadedSize: 457424883, - totalSize: 878454797, - totalSpeed: 0, - platform: "macuniversal" - ), - onCancel: {}, - onPause: {}, - onResume: {}, - onRetry: {}, - onRemove: {} - ) - .environmentObject(NetworkManager()) -} diff --git a/Adobe Downloader/Views/VersionPickerView.swift b/Adobe Downloader/Views/VersionPickerView.swift index cedf386..fd56948 100644 --- a/Adobe Downloader/Views/VersionPickerView.swift +++ b/Adobe Downloader/Views/VersionPickerView.swift @@ -27,19 +27,19 @@ struct VersionPickerView: View { @StorageValue(\.downloadAppleSilicon) private var downloadAppleSilicon @State private var expandedVersions: Set = [] - private let sap: Sap + private let product: Product private let onSelect: (String) -> Void - init(sap: Sap, onSelect: @escaping (String) -> Void) { - self.sap = sap + init(product: Product, onSelect: @escaping (String) -> Void) { + self.product = product self.onSelect = onSelect } var body: some View { VStack(spacing: 0) { - HeaderView(sap: sap, downloadAppleSilicon: downloadAppleSilicon) + HeaderView(product: product, downloadAppleSilicon: downloadAppleSilicon) VersionListView( - sap: sap, + product: product, expandedVersions: $expandedVersions, onSelect: onSelect, dismiss: dismiss @@ -50,7 +50,7 @@ struct VersionPickerView: View { } private struct HeaderView: View { - let sap: Sap + let product: Product let downloadAppleSilicon: Bool @Environment(\.dismiss) private var dismiss @EnvironmentObject private var networkManager: NetworkManager @@ -58,7 +58,7 @@ private struct HeaderView: View { var body: some View { VStack { HStack { - Text("\(sap.displayName)") + Text("\(product.displayName)") .font(.headline) Text("选择版本") .foregroundColor(.secondary) @@ -86,7 +86,7 @@ private struct HeaderView: View { private struct VersionListView: View { @EnvironmentObject private var networkManager: NetworkManager - let sap: Sap + let product: Product @Binding var expandedVersions: Set let onSelect: (String) -> Void let dismiss: DismissAction @@ -96,7 +96,7 @@ private struct VersionListView: View { LazyVStack(spacing: VersionPickerConstants.verticalSpacing) { ForEach(filteredVersions, id: \.key) { version, info in VersionRow( - sap: sap, + product: product, version: version, info: info, isExpanded: expandedVersions.contains(version), @@ -110,10 +110,26 @@ private struct VersionListView: View { .background(Color(NSColor.windowBackgroundColor)) } - private var filteredVersions: [(key: String, value: Sap.Versions)] { - sap.versions - .filter { StorageData.shared.allowedPlatform.contains($0.value.apPlatform) } - .sorted { AppStatics.compareVersions($0.key, $1.key) > 0 } + private var filteredVersions: [(key: String, value: Product.Platform)] { + // 获取支持的平台 + let platforms = product.platforms.filter { platform in + StorageData.shared.allowedPlatform.contains(platform.id) && + platform.languageSet.first != nil + } + + // 如果没有支持的平台,返回空数组 + if platforms.isEmpty { + return [] + } + + // 将平台按版本号降序排序 + return platforms.map { platform in + // 使用第一个语言集的 productVersion 作为版本号 + (key: platform.languageSet.first?.productVersion ?? "", value: platform) + }.sorted { pair1, pair2 in + // 按版本号降序排序 + AppStatics.compareVersions(pair1.key, pair2.key) > 0 + } } private func handleVersionSelect(_ version: String) { @@ -133,19 +149,18 @@ private struct VersionListView: View { } private struct VersionRow: View { - @EnvironmentObject private var networkManager: NetworkManager @StorageValue(\.defaultLanguage) private var defaultLanguage - let sap: Sap + let product: Product let version: String - let info: Sap.Versions + let info: Product.Platform let isExpanded: Bool let onSelect: (String) -> Void let onToggle: (String) -> Void private var existingPath: URL? { - networkManager.isVersionDownloaded( - sap: sap, + globalNetworkManager.isVersionDownloaded( + product: product, version: version, language: defaultLanguage ) @@ -176,7 +191,8 @@ private struct VersionRow: View { } private func handleSelect() { - if info.dependencies.isEmpty { + let dependencies = info.languageSet.first?.dependencies ?? [] + if dependencies.isEmpty { onSelect(version) } else { onToggle(version) @@ -186,7 +202,7 @@ private struct VersionRow: View { private struct VersionHeader: View { let version: String - let info: Sap.Versions + let info: Product.Platform let isExpanded: Bool let hasExistingPath: Bool let onSelect: () -> Void @@ -195,12 +211,13 @@ private struct VersionHeader: View { var body: some View { Button(action: onSelect) { HStack { - VersionInfo(version: version, platform: info.apPlatform) + VersionInfo(version: version, platform: info.id) Spacer() ExistingPathButton(isVisible: hasExistingPath) ExpandButton( isExpanded: isExpanded, - hasDependencies: !info.dependencies.isEmpty + onToggle: onToggle, + hasDependencies: !(info.languageSet.first?.dependencies.isEmpty ?? true) ) } .padding(.vertical, VersionPickerConstants.buttonPadding) @@ -243,11 +260,14 @@ private struct ExistingPathButton: View { private struct ExpandButton: View { let isExpanded: Bool + let onToggle: () -> Void let hasDependencies: Bool var body: some View { - Image(systemName: iconName) - .foregroundColor(.secondary) + Button(action: onToggle) { + Image(systemName: iconName) + .foregroundColor(.secondary) + } } private var iconName: String { @@ -259,7 +279,7 @@ private struct ExpandButton: View { } private struct VersionDetails: View { - let info: Sap.Versions + let info: Product.Platform let version: String let onSelect: (String) -> Void @@ -271,7 +291,7 @@ private struct VersionDetails: View { .padding(.top, 8) .padding(.leading, 16) - DependenciesList(dependencies: info.dependencies) + DependenciesList(dependencies: info.languageSet.first?.dependencies ?? []) DownloadButton(version: version, onSelect: onSelect) } @@ -281,15 +301,15 @@ private struct VersionDetails: View { } private struct DependenciesList: View { - let dependencies: [Sap.Versions.Dependencies] - + let dependencies: [Product.Platform.LanguageSet.Dependency] + var body: some View { ForEach(dependencies, id: \.sapCode) { dependency in HStack(spacing: 8) { Image(systemName: "cube.box") .foregroundColor(.blue) .frame(width: 16) - Text("\(dependency.sapCode) (\(dependency.version))") + Text("\(dependency.sapCode) (\(dependency.baseVersion))") .font(.caption) Spacer() } @@ -311,54 +331,3 @@ private struct DownloadButton: View { .padding(.leading, 16) } } - -struct VersionPickerView_Previews: PreviewProvider { - static var previews: some View { - let networkManager = NetworkManager() - networkManager.cdn = "https://example.cdn.adobe.com" - - let previewSap = Sap( - hidden: false, - displayName: "Photoshop", - sapCode: "PHSP", - versions: [ - "26.0.0": Sap.Versions( - sapCode: "PHSP", - baseVersion: "26.0.0", - productVersion: "26.0.0", - apPlatform: "macuniversal", - dependencies: [ - Sap.Versions.Dependencies(sapCode: "ACR", version: "9.6"), - Sap.Versions.Dependencies(sapCode: "COCM", version: "1.0"), - Sap.Versions.Dependencies(sapCode: "COSY", version: "2.4.1") - ], - buildGuid: "b382ef03-c44a-4fd4-a9a1-3119ab0474b4" - ), - "25.0.0": Sap.Versions( - sapCode: "PHSP", - baseVersion: "25.0.0", - productVersion: "25.0.0", - apPlatform: "macuniversal", - dependencies: [ - Sap.Versions.Dependencies(sapCode: "ACR", version: "9.5"), - Sap.Versions.Dependencies(sapCode: "COCM", version: "1.0") - ], - buildGuid: "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" - ), - "24.0.0": Sap.Versions( - sapCode: "PHSP", - baseVersion: "24.0.0", - productVersion: "24.0.0", - apPlatform: "macuniversal", - dependencies: [], - buildGuid: "q1w2e3r4-t5y6-u7i8-o9p0-a1s2d3f4g5h6" - ) - ], - icons: [] - ) - - return VersionPickerView(sap: previewSap) { _ in } - .environmentObject(networkManager) - .previewDisplayName("Version Picker") - } -}