2024-10-31 22:35:22 +08:00
|
|
|
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
|
2024-11-03 00:12:38 +08:00
|
|
|
@Published var saps: [String: Sap] = [:]
|
2024-10-31 22:35:22 +08:00
|
|
|
@Published var cdn: String = ""
|
2024-11-03 00:12:38 +08:00
|
|
|
@Published var sapCodes: [SapCodes] = []
|
2024-10-31 22:35:22 +08:00
|
|
|
@Published var loadingState: LoadingState = .idle
|
2024-11-03 00:12:38 +08:00
|
|
|
@Published var downloadTasks: [NewDownloadTask] = []
|
2024-10-31 22:35:22 +08:00
|
|
|
@Published var installationState: InstallationState = .idle
|
2024-11-07 16:14:42 +08:00
|
|
|
@Published var installCommand: String = ""
|
2024-10-31 22:35:22 +08:00
|
|
|
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()
|
2024-11-15 17:47:15 +08:00
|
|
|
|
|
|
|
|
private var defaultDirectory: String {
|
|
|
|
|
get { StorageData.shared.defaultDirectory }
|
|
|
|
|
set { StorageData.shared.defaultDirectory = newValue }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var useDefaultDirectory: Bool {
|
|
|
|
|
get { StorageData.shared.useDefaultDirectory }
|
|
|
|
|
set { StorageData.shared.useDefaultDirectory = newValue }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var apiVersion: String {
|
|
|
|
|
get { StorageData.shared.apiVersion }
|
|
|
|
|
set { StorageData.shared.apiVersion = newValue }
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
|
|
|
|
|
enum InstallationState {
|
|
|
|
|
case idle
|
|
|
|
|
case installing(progress: Double, status: String)
|
|
|
|
|
case completed
|
|
|
|
|
case failed(Error)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
private let networkService: NetworkService
|
|
|
|
|
|
|
|
|
|
init(networkService: NetworkService = NetworkService(),
|
|
|
|
|
downloadUtils: DownloadUtils? = nil) {
|
2024-11-06 10:09:16 +08:00
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
self.networkService = networkService
|
|
|
|
|
self.downloadUtils = downloadUtils ?? DownloadUtils(networkManager: self, cancelTracker: cancelTracker)
|
|
|
|
|
|
|
|
|
|
TaskPersistenceManager.shared.setCancelTracker(cancelTracker)
|
|
|
|
|
configureNetworkMonitor()
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchProducts() async {
|
2024-11-13 13:20:25 +08:00
|
|
|
loadingState = .loading
|
|
|
|
|
do {
|
2024-11-18 20:33:45 +08:00
|
|
|
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData()
|
2024-11-13 13:20:25 +08:00
|
|
|
await MainActor.run {
|
2024-11-13 15:54:25 +08:00
|
|
|
self.saps = saps
|
|
|
|
|
self.cdn = cdn
|
|
|
|
|
self.sapCodes = sapCodes
|
2024-11-13 13:20:25 +08:00
|
|
|
self.loadingState = .success
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
await MainActor.run {
|
|
|
|
|
self.loadingState = .failed(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
func startDownload(sap: Sap, selectedVersion: String, language: String, destinationURL: URL) async throws {
|
2024-11-05 20:30:18 +08:00
|
|
|
guard let productInfo = self.saps[sap.sapCode]?.versions[selectedVersion] else {
|
|
|
|
|
throw NetworkError.invalidData("无法获取产品信息")
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
let task = NewDownloadTask(
|
|
|
|
|
sapCode: sap.sapCode,
|
|
|
|
|
version: selectedVersion,
|
2024-10-31 22:35:22 +08:00
|
|
|
language: language,
|
2024-11-03 00:12:38 +08:00
|
|
|
displayName: sap.displayName,
|
|
|
|
|
directory: destinationURL,
|
|
|
|
|
productsToDownload: [],
|
|
|
|
|
createAt: Date(),
|
2024-11-09 23:15:50 +08:00
|
|
|
totalStatus: .preparing(DownloadStatus.PrepareInfo(
|
|
|
|
|
message: "正在准备下载...",
|
|
|
|
|
timestamp: Date(),
|
|
|
|
|
stage: .initializing
|
|
|
|
|
)),
|
2024-11-03 00:12:38 +08:00
|
|
|
totalProgress: 0,
|
|
|
|
|
totalDownloadedSize: 0,
|
2024-10-31 22:35:22 +08:00
|
|
|
totalSize: 0,
|
2024-11-09 23:15:50 +08:00
|
|
|
totalSpeed: 0,
|
|
|
|
|
platform: productInfo.apPlatform
|
2024-10-31 22:35:22 +08:00
|
|
|
)
|
2024-11-03 00:12:38 +08:00
|
|
|
|
|
|
|
|
downloadTasks.append(task)
|
2024-11-04 14:44:52 +08:00
|
|
|
updateDockBadge()
|
2024-11-09 23:15:50 +08:00
|
|
|
saveTask(task)
|
2024-11-03 00:12:38 +08:00
|
|
|
|
|
|
|
|
do {
|
2024-11-18 20:33:45 +08:00
|
|
|
try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform, saps: saps)
|
2024-11-03 00:12:38 +08:00
|
|
|
} catch {
|
2024-11-03 17:13:25 +08:00
|
|
|
await MainActor.run {
|
|
|
|
|
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
|
|
|
|
message: error.localizedDescription,
|
|
|
|
|
error: error,
|
|
|
|
|
timestamp: Date(),
|
|
|
|
|
recoverable: true
|
|
|
|
|
)))
|
2024-11-09 23:15:50 +08:00
|
|
|
saveTask(task)
|
2024-11-03 17:13:25 +08:00
|
|
|
objectWillChange.send()
|
|
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
throw error
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
|
|
|
|
|
var cdnUrl: String {
|
|
|
|
|
get async {
|
|
|
|
|
await MainActor.run { cdn }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
func removeTask(taskId: UUID, removeFiles: Bool = true) {
|
|
|
|
|
Task {
|
|
|
|
|
await cancelTracker.cancel(taskId)
|
|
|
|
|
|
|
|
|
|
if let task = downloadTasks.first(where: { $0.id == taskId }) {
|
2024-11-09 23:15:50 +08:00
|
|
|
if task.status.isActive {
|
|
|
|
|
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
2025-01-13 16:40:52 +08:00
|
|
|
message: String(localized: "下载已取消"),
|
2024-11-09 23:15:50 +08:00
|
|
|
error: NetworkError.downloadCancelled,
|
|
|
|
|
timestamp: Date(),
|
|
|
|
|
recoverable: false
|
|
|
|
|
)))
|
|
|
|
|
saveTask(task)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
if removeFiles {
|
2024-11-04 14:44:52 +08:00
|
|
|
try? FileManager.default.removeItem(at: task.directory)
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
TaskPersistenceManager.shared.removeTask(task)
|
|
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
await MainActor.run {
|
|
|
|
|
downloadTasks.removeAll { $0.id == taskId }
|
|
|
|
|
updateDockBadge()
|
|
|
|
|
objectWillChange.send()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
|
|
|
|
|
private func fetchProductsWithRetry() async {
|
|
|
|
|
guard !isFetchingProducts else { return }
|
|
|
|
|
|
|
|
|
|
isFetchingProducts = true
|
|
|
|
|
loadingState = .loading
|
|
|
|
|
|
|
|
|
|
let maxRetries = 3
|
|
|
|
|
var retryCount = 0
|
|
|
|
|
|
|
|
|
|
while retryCount < maxRetries {
|
|
|
|
|
do {
|
2024-11-18 20:33:45 +08:00
|
|
|
let (saps, cdn, sapCodes) = try await networkService.fetchProductsData()
|
2024-10-31 22:35:22 +08:00
|
|
|
await MainActor.run {
|
2024-11-03 00:12:38 +08:00
|
|
|
self.saps = saps
|
2024-10-31 22:35:22 +08:00
|
|
|
self.cdn = cdn
|
2024-11-03 00:12:38 +08:00
|
|
|
self.sapCodes = sapCodes
|
2024-10-31 22:35:22 +08:00
|
|
|
self.loadingState = .success
|
|
|
|
|
self.isFetchingProducts = false
|
|
|
|
|
}
|
2024-11-18 20:33:45 +08:00
|
|
|
|
2024-10-31 22:35:22 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
private func clearCompletedDownloadTasks() async {
|
|
|
|
|
await MainActor.run {
|
|
|
|
|
downloadTasks.removeAll { task in
|
2024-11-04 00:29:08 +08:00
|
|
|
if task.status.isCompleted || task.status.isFailed {
|
|
|
|
|
try? FileManager.default.removeItem(at: task.directory)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
updateDockBadge()
|
|
|
|
|
objectWillChange.send()
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
|
|
|
|
|
func installProduct(at path: URL) async {
|
|
|
|
|
await MainActor.run {
|
|
|
|
|
installationState = .installing(progress: 0, status: "准备安装...")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
do {
|
2024-11-04 17:40:01 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-04 14:44:52 +08:00
|
|
|
}
|
2024-11-04 17:40:01 +08:00
|
|
|
)
|
2024-11-04 14:44:52 +08:00
|
|
|
|
2024-10-31 22:35:22 +08:00
|
|
|
await MainActor.run {
|
|
|
|
|
installationState = .completed
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
2024-11-07 16:14:42 +08:00
|
|
|
let command = await installManager.getInstallCommand(
|
|
|
|
|
for: path.appendingPathComponent("driver.xml").path
|
|
|
|
|
)
|
|
|
|
|
|
2024-10-31 22:35:22 +08:00
|
|
|
await MainActor.run {
|
2024-11-07 16:14:42 +08:00
|
|
|
self.installCommand = command
|
|
|
|
|
|
2024-11-01 17:28:23 +08:00
|
|
|
if let installError = error as? InstallManager.InstallError {
|
|
|
|
|
switch installError {
|
|
|
|
|
case .installationFailed(let message):
|
2024-11-04 14:44:52 +08:00
|
|
|
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)
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
2024-11-04 14:44:52 +08:00
|
|
|
installationState = .failed(InstallManager.InstallError.installationFailed(error.localizedDescription))
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cancelInstallation() {
|
|
|
|
|
Task {
|
|
|
|
|
await installManager.cancel()
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-01 17:28:23 +08:00
|
|
|
|
|
|
|
|
func retryInstallation(at path: URL) async {
|
|
|
|
|
await MainActor.run {
|
|
|
|
|
installationState = .installing(progress: 0, status: "正在重试安装...")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
do {
|
2024-11-04 17:40:01 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
2024-11-04 17:40:01 +08:00
|
|
|
)
|
2024-11-01 17:28:23 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
|
|
|
|
|
func getApplicationInfo(buildGuid: String) async throws -> String {
|
2024-11-09 23:15:50 +08:00
|
|
|
return try await networkService.getApplicationInfo(buildGuid: buildGuid)
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
|
2024-11-04 00:29:08 +08:00
|
|
|
func isVersionDownloaded(sap: Sap, version: String, language: String) -> URL? {
|
2024-11-09 23:15:50 +08:00
|
|
|
if let task = downloadTasks.first(where: {
|
|
|
|
|
$0.sapCode == sap.sapCode &&
|
|
|
|
|
$0.version == version &&
|
|
|
|
|
$0.language == language &&
|
|
|
|
|
!$0.status.isCompleted
|
|
|
|
|
}) { return task.directory }
|
|
|
|
|
|
2024-11-04 00:29:08 +08:00
|
|
|
let platform = sap.versions[version]?.apPlatform ?? "unknown"
|
2024-11-07 21:31:29 +08:00
|
|
|
let fileName = sap.sapCode == "APRO"
|
|
|
|
|
? "Adobe Downloader \(sap.sapCode)_\(version)_\(platform).dmg"
|
|
|
|
|
: "Adobe Downloader \(sap.sapCode)_\(version)-\(language)-\(platform)"
|
2024-11-04 00:29:08 +08:00
|
|
|
|
2024-11-06 10:09:16 +08:00
|
|
|
if useDefaultDirectory && !defaultDirectory.isEmpty {
|
2024-11-04 00:29:08 +08:00
|
|
|
let defaultPath = URL(fileURLWithPath: defaultDirectory)
|
|
|
|
|
.appendingPathComponent(fileName)
|
|
|
|
|
if FileManager.default.fileExists(atPath: defaultPath.path) {
|
|
|
|
|
return defaultPath
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2024-11-05 09:22:40 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-05 20:30:18 +08:00
|
|
|
|
|
|
|
|
func retryFetchData() {
|
|
|
|
|
Task {
|
|
|
|
|
isFetchingProducts = false
|
|
|
|
|
loadingState = .idle
|
|
|
|
|
await fetchProducts()
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-06 10:09:16 +08:00
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
func saveTask(_ task: NewDownloadTask) {
|
|
|
|
|
TaskPersistenceManager.shared.saveTask(task)
|
2024-11-15 17:47:15 +08:00
|
|
|
objectWillChange.send()
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadSavedTasks() {
|
|
|
|
|
let savedTasks = TaskPersistenceManager.shared.loadTasks()
|
|
|
|
|
for task in savedTasks {
|
|
|
|
|
for product in task.productsToDownload {
|
|
|
|
|
product.updateCompletedPackages()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
downloadTasks.append(contentsOf: savedTasks)
|
|
|
|
|
updateDockBadge()
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|