mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 11:18:53 +08:00
423 lines
15 KiB
Swift
423 lines
15 KiB
Swift
import Foundation
|
|
import Network
|
|
import Combine
|
|
import AppKit
|
|
import SwiftUI
|
|
|
|
@MainActor
|
|
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 allowedPlatform: [String]
|
|
@Published var sapCodes: [SapCodes] = []
|
|
@Published var loadingState: LoadingState = .idle
|
|
@Published var downloadTasks: [NewDownloadTask] = []
|
|
@Published var installationState: InstallationState = .idle
|
|
@Published var installationLogs: [String] = [] {
|
|
didSet {
|
|
if installationLogs.count > 1000 {
|
|
installationLogs = Array(installationLogs.suffix(1000))
|
|
}
|
|
}
|
|
}
|
|
@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()
|
|
internal var isFetchingProducts = false
|
|
private let installManager = InstallManager()
|
|
@AppStorage("defaultDirectory") private var defaultDirectory: String = ""
|
|
@AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true
|
|
@AppStorage("apiVersion") private var apiVersion: String = "6"
|
|
|
|
enum InstallationState {
|
|
case idle
|
|
case installing(progress: Double, status: String)
|
|
case completed
|
|
case failed(Error)
|
|
}
|
|
|
|
private let networkService: NetworkService
|
|
|
|
init(networkService: NetworkService = NetworkService(),
|
|
downloadUtils: DownloadUtils? = nil) {
|
|
let useAppleSilicon = UserDefaults.standard.bool(forKey: "downloadAppleSilicon")
|
|
self.allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
|
|
|
self.networkService = networkService
|
|
self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
|
|
|
|
TaskPersistenceManager.shared.setCancelTracker(cancelTracker)
|
|
configureNetworkMonitor()
|
|
}
|
|
|
|
func fetchProducts() async {
|
|
loadingState = .loading
|
|
do {
|
|
let products = try await fetchProductsWithVersion(apiVersion)
|
|
await MainActor.run {
|
|
self.saps = products
|
|
self.loadingState = .success
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
self.loadingState = .failed(error)
|
|
}
|
|
}
|
|
}
|
|
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("无法获取产品信息")
|
|
}
|
|
|
|
let task = NewDownloadTask(
|
|
sapCode: sap.sapCode,
|
|
version: selectedVersion,
|
|
language: language,
|
|
displayName: sap.displayName,
|
|
directory: destinationURL,
|
|
productsToDownload: [],
|
|
createAt: Date(),
|
|
totalStatus: .preparing(DownloadStatus.PrepareInfo(
|
|
message: "正在准备下载...",
|
|
timestamp: Date(),
|
|
stage: .initializing
|
|
)),
|
|
totalProgress: 0,
|
|
totalDownloadedSize: 0,
|
|
totalSize: 0,
|
|
totalSpeed: 0,
|
|
platform: productInfo.apPlatform
|
|
)
|
|
|
|
downloadTasks.append(task)
|
|
updateDockBadge()
|
|
saveTask(task)
|
|
|
|
do {
|
|
try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: allowedPlatform, saps: saps)
|
|
} catch {
|
|
await MainActor.run {
|
|
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
|
message: error.localizedDescription,
|
|
error: error,
|
|
timestamp: Date(),
|
|
recoverable: true
|
|
)))
|
|
saveTask(task)
|
|
objectWillChange.send()
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
var cdnUrl: String {
|
|
get async {
|
|
await MainActor.run { cdn }
|
|
}
|
|
}
|
|
|
|
func removeTask(taskId: UUID, removeFiles: Bool = true) {
|
|
Task {
|
|
await cancelTracker.cancel(taskId)
|
|
|
|
if let task = downloadTasks.first(where: { $0.id == taskId }) {
|
|
if task.status.isActive {
|
|
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
|
message: "下载已取消",
|
|
error: NetworkError.downloadCancelled,
|
|
timestamp: Date(),
|
|
recoverable: false
|
|
)))
|
|
saveTask(task)
|
|
}
|
|
|
|
if removeFiles {
|
|
try? FileManager.default.removeItem(at: task.directory)
|
|
}
|
|
|
|
TaskPersistenceManager.shared.removeTask(task)
|
|
|
|
await MainActor.run {
|
|
downloadTasks.removeAll { $0.id == taskId }
|
|
updateDockBadge()
|
|
objectWillChange.send()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func fetchProductsWithRetry() async {
|
|
guard !isFetchingProducts else { return }
|
|
|
|
isFetchingProducts = true
|
|
loadingState = .loading
|
|
|
|
let maxRetries = 3
|
|
var retryCount = 0
|
|
|
|
while retryCount < maxRetries {
|
|
do {
|
|
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData()
|
|
|
|
await MainActor.run {
|
|
self.saps = saps
|
|
self.cdn = cdn
|
|
self.sapCodes = sapCodes
|
|
self.loadingState = .success
|
|
self.isFetchingProducts = false
|
|
}
|
|
return
|
|
} catch {
|
|
retryCount += 1
|
|
if retryCount == maxRetries {
|
|
await MainActor.run {
|
|
self.loadingState = .failed(error)
|
|
self.isFetchingProducts = false
|
|
}
|
|
} else {
|
|
try? await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(retryCount))) * 1_000_000_000)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func clearCompletedDownloadTasks() async {
|
|
await MainActor.run {
|
|
downloadTasks.removeAll { task in
|
|
if task.status.isCompleted || task.status.isFailed {
|
|
try? FileManager.default.removeItem(at: task.directory)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
updateDockBadge()
|
|
objectWillChange.send()
|
|
}
|
|
}
|
|
|
|
func installProduct(at path: URL) async {
|
|
await MainActor.run {
|
|
installationState = .installing(progress: 0, status: "准备安装...")
|
|
installationLogs.removeAll()
|
|
}
|
|
|
|
do {
|
|
try await installManager.install(
|
|
at: path,
|
|
progressHandler: { progress, status in
|
|
Task { @MainActor in
|
|
if status.contains("完成") || status.contains("成功") {
|
|
self.installationState = .completed
|
|
} else {
|
|
self.installationState = .installing(progress: progress, status: status)
|
|
}
|
|
}
|
|
},
|
|
logHandler: { log in
|
|
Task { @MainActor in
|
|
self.installationLogs.append(log)
|
|
}
|
|
}
|
|
)
|
|
|
|
await MainActor.run {
|
|
installationState = .completed
|
|
}
|
|
} catch {
|
|
let command = await installManager.getInstallCommand(
|
|
for: path.appendingPathComponent("driver.xml").path
|
|
)
|
|
|
|
await MainActor.run {
|
|
self.installCommand = command
|
|
|
|
if let installError = error as? InstallManager.InstallError {
|
|
switch installError {
|
|
case .installationFailed(let message):
|
|
if message.contains("需要重新输入密码") {
|
|
Task {
|
|
await installProduct(at: path)
|
|
}
|
|
} else {
|
|
installationState = .failed(InstallManager.InstallError.installationFailed(message))
|
|
}
|
|
case .cancelled:
|
|
installationState = .failed(InstallManager.InstallError.cancelled)
|
|
case .setupNotFound:
|
|
installationState = .failed(InstallManager.InstallError.setupNotFound)
|
|
case .permissionDenied:
|
|
installationState = .failed(InstallManager.InstallError.permissionDenied)
|
|
}
|
|
} else {
|
|
installationState = .failed(InstallManager.InstallError.installationFailed(error.localizedDescription))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func cancelInstallation() {
|
|
Task {
|
|
await installManager.cancel()
|
|
}
|
|
}
|
|
|
|
func retryInstallation(at path: URL) async {
|
|
await MainActor.run {
|
|
installationState = .installing(progress: 0, status: "正在重试安装...")
|
|
}
|
|
|
|
do {
|
|
try await installManager.retry(
|
|
at: path,
|
|
progressHandler: { progress, status in
|
|
Task { @MainActor in
|
|
if status.contains("完成") || status.contains("成功") {
|
|
self.installationState = .completed
|
|
} else {
|
|
self.installationState = .installing(progress: progress, status: status)
|
|
}
|
|
}
|
|
},
|
|
logHandler: { log in
|
|
Task { @MainActor in
|
|
self.installationLogs.append(log)
|
|
}
|
|
}
|
|
)
|
|
|
|
await MainActor.run {
|
|
installationState = .completed
|
|
}
|
|
} catch {
|
|
if case InstallManager.InstallError.installationFailed(let message) = error,
|
|
message.contains("需要重新输入密码") {
|
|
await installProduct(at: path)
|
|
} else {
|
|
await MainActor.run {
|
|
if let installError = error as? InstallManager.InstallError {
|
|
installationState = .failed(installError)
|
|
} else {
|
|
installationState = .failed(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getApplicationInfo(buildGuid: String) async throws -> String {
|
|
return try await networkService.getApplicationInfo(buildGuid: buildGuid)
|
|
}
|
|
|
|
func isVersionDownloaded(sap: Sap, version: String, language: String) -> URL? {
|
|
if let task = downloadTasks.first(where: {
|
|
$0.sapCode == sap.sapCode &&
|
|
$0.version == version &&
|
|
$0.language == language &&
|
|
!$0.status.isCompleted
|
|
}) { return task.directory }
|
|
|
|
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
|
let fileName = sap.sapCode == "APRO"
|
|
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
|
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)"
|
|
|
|
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
|
let defaultPath = URL(fileURLWithPath: defaultDirectory)
|
|
.appendingPathComponent(fileName)
|
|
if FileManager.default.fileExists(atPath: defaultPath.path) {
|
|
return defaultPath
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updateDockBadge() {
|
|
let activeCount = downloadTasks.filter { task in
|
|
if case .completed = task.totalStatus {
|
|
return false
|
|
}
|
|
return true
|
|
}.count
|
|
|
|
if activeCount > 0 {
|
|
NSApplication.shared.dockTile.badgeLabel = "\(activeCount)"
|
|
} else {
|
|
NSApplication.shared.dockTile.badgeLabel = nil
|
|
}
|
|
}
|
|
|
|
func retryFetchData() {
|
|
Task {
|
|
isFetchingProducts = false
|
|
loadingState = .idle
|
|
await fetchProducts()
|
|
}
|
|
}
|
|
|
|
func updateAllowedPlatform(useAppleSilicon: Bool) {
|
|
allowedPlatform = useAppleSilicon ? ["macuniversal", "macarm64"] : ["macuniversal", "osx10-64"]
|
|
}
|
|
|
|
func saveTask(_ task: NewDownloadTask) {
|
|
TaskPersistenceManager.shared.saveTask(task)
|
|
}
|
|
|
|
func loadSavedTasks() {
|
|
let savedTasks = TaskPersistenceManager.shared.loadTasks()
|
|
for task in savedTasks {
|
|
for product in task.productsToDownload {
|
|
product.updateCompletedPackages()
|
|
}
|
|
}
|
|
downloadTasks.append(contentsOf: savedTasks)
|
|
updateDockBadge()
|
|
}
|
|
|
|
private func fetchProductsWithVersion(_ version: String) async throws -> [String: Sap] {
|
|
var components = URLComponents(string: NetworkConstants.productsXmlURL)
|
|
components?.queryItems = [
|
|
URLQueryItem(name: "_type", value: "xml"),
|
|
URLQueryItem(name: "channel", value: "ccm"),
|
|
URLQueryItem(name: "channel", value: "sti"),
|
|
URLQueryItem(name: "platform", value: "osx10-64,osx10,macarm64,macuniversal"),
|
|
URLQueryItem(name: "productType", value: "Desktop"),
|
|
URLQueryItem(name: "version", value: version)
|
|
]
|
|
|
|
guard let url = components?.url else {
|
|
throw NetworkError.invalidURL(NetworkConstants.productsXmlURL)
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
NetworkConstants.adobeRequestHeaders.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, nil)
|
|
}
|
|
|
|
guard let xmlString = String(data: data, encoding: .utf8) else {
|
|
throw NetworkError.invalidData("无法解码XML数据")
|
|
}
|
|
|
|
let result = try await Task.detached(priority: .userInitiated) {
|
|
let parseResult = try XHXMLParser.parse(xmlString: xmlString)
|
|
return parseResult.products
|
|
}.value
|
|
|
|
return result
|
|
}
|
|
}
|