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
|
|
|
|
|
@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
|
|
|
internal var progressObservers: [UUID: NSKeyValueObservation] = [:]
|
|
|
|
|
internal var activeDownloadTaskId: UUID?
|
|
|
|
|
internal var monitor = NWPathMonitor()
|
|
|
|
|
internal var isFetchingProducts = false
|
|
|
|
|
private let installManager = InstallManager()
|
2025-02-06 17:57:06 +08:00
|
|
|
private var hasLoadedSavedTasks = false
|
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
|
2025-03-27 01:05:20 +08:00
|
|
|
case failed(Error, String? = nil)
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-27 23:02:40 +08:00
|
|
|
init() {
|
|
|
|
|
TaskPersistenceManager.shared.setCancelTracker(globalCancelTracker)
|
2024-11-09 23:15:50 +08:00
|
|
|
configureNetworkMonitor()
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchProducts() async {
|
2024-11-13 13:20:25 +08:00
|
|
|
loadingState = .loading
|
|
|
|
|
do {
|
2025-03-05 21:09:14 +08:00
|
|
|
let (products, uniqueProducts) = try await globalNetworkService.fetchProductsData()
|
2024-11-13 13:20:25 +08:00
|
|
|
await MainActor.run {
|
2025-03-05 21:09:14 +08:00
|
|
|
globalProducts = products
|
|
|
|
|
globalUniqueProducts = uniqueProducts.sorted { $0.displayName < $1.displayName }
|
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
|
|
|
}
|
2025-02-27 23:02:40 +08:00
|
|
|
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
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
2025-02-27 23:02:40 +08:00
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
let task = NewDownloadTask(
|
2025-02-27 23:02:40 +08:00
|
|
|
productId: productInfo.id,
|
|
|
|
|
productVersion: selectedVersion,
|
2024-10-31 22:35:22 +08:00
|
|
|
language: language,
|
2025-02-27 23:02:40 +08:00
|
|
|
displayName: productInfo.displayName,
|
2024-11-03 00:12:38 +08:00
|
|
|
directory: destinationURL,
|
2025-02-27 23:02:40 +08:00
|
|
|
dependenciesToDownload: [],
|
2024-11-03 00:12:38 +08:00
|
|
|
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,
|
2025-03-08 02:49:30 +08:00
|
|
|
platform: globalProducts.first(where: { $0.id == productId })?.platforms.first?.id ?? "unknown")
|
2025-02-27 23:02:40 +08:00
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
downloadTasks.append(task)
|
2024-11-04 14:44:52 +08:00
|
|
|
updateDockBadge()
|
2025-02-06 16:29:59 +08:00
|
|
|
await saveTask(task)
|
2024-11-03 00:12:38 +08:00
|
|
|
|
|
|
|
|
do {
|
2025-02-27 23:02:40 +08:00
|
|
|
try await globalNewDownloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform)
|
2024-11-03 00:12:38 +08:00
|
|
|
} catch {
|
2025-02-06 16:29:59 +08:00
|
|
|
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
|
|
|
|
message: error.localizedDescription,
|
|
|
|
|
error: error,
|
|
|
|
|
timestamp: Date(),
|
|
|
|
|
recoverable: true
|
|
|
|
|
)))
|
|
|
|
|
await saveTask(task)
|
2024-11-03 17:13:25 +08:00
|
|
|
await MainActor.run {
|
|
|
|
|
objectWillChange.send()
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
func removeTask(taskId: UUID, removeFiles: Bool = true) {
|
|
|
|
|
Task {
|
2025-02-27 23:02:40 +08:00
|
|
|
await globalCancelTracker.cancel(taskId)
|
|
|
|
|
|
2024-11-03 00:12:38 +08:00
|
|
|
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
|
|
|
|
|
)))
|
2025-02-06 16:29:59 +08:00
|
|
|
await saveTask(task)
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
|
|
|
|
|
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 {
|
2025-03-05 21:09:14 +08:00
|
|
|
let (products, uniqueProducts) = try await globalNetworkService.fetchProductsData()
|
2024-10-31 22:35:22 +08:00
|
|
|
await MainActor.run {
|
2025-03-05 21:09:14 +08:00
|
|
|
globalProducts = products
|
|
|
|
|
globalUniqueProducts = uniqueProducts
|
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
|
|
|
|
|
|
2025-03-27 01:05:20 +08:00
|
|
|
var errorDetails: String? = nil
|
|
|
|
|
var mainError = error
|
|
|
|
|
|
2024-11-01 17:28:23 +08:00
|
|
|
if let installError = error as? InstallManager.InstallError {
|
|
|
|
|
switch installError {
|
2025-03-27 01:05:20 +08:00
|
|
|
case .installationFailedWithDetails(let message, let details):
|
|
|
|
|
errorDetails = details
|
|
|
|
|
mainError = InstallManager.InstallError.installationFailed(message)
|
|
|
|
|
default:
|
|
|
|
|
break
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-27 01:05:20 +08:00
|
|
|
|
|
|
|
|
installationState = .failed(mainError, errorDetails)
|
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 {
|
2025-03-27 01:05:20 +08:00
|
|
|
await MainActor.run {
|
|
|
|
|
var errorDetails: String? = nil
|
|
|
|
|
var mainError = error
|
|
|
|
|
|
|
|
|
|
if let installError = error as? InstallManager.InstallError {
|
|
|
|
|
if case .installationFailedWithDetails(let message, let details) = installError {
|
|
|
|
|
errorDetails = details
|
|
|
|
|
mainError = InstallManager.InstallError.installationFailed(message)
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-27 01:05:20 +08:00
|
|
|
|
|
|
|
|
installationState = .failed(mainError, errorDetails)
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-03 00:12:38 +08:00
|
|
|
|
|
|
|
|
func getApplicationInfo(buildGuid: String) async throws -> String {
|
2025-02-27 23:02:40 +08:00
|
|
|
return try await globalNetworkService.getApplicationInfo(buildGuid: buildGuid)
|
2024-11-03 00:12:38 +08:00
|
|
|
}
|
|
|
|
|
|
2025-03-05 21:09:14 +08:00
|
|
|
func isVersionDownloaded(productId: String, version: String, language: String) -> URL? {
|
2024-11-09 23:15:50 +08:00
|
|
|
if let task = downloadTasks.first(where: {
|
2025-03-05 21:09:14 +08:00
|
|
|
$0.productId == productId &&
|
|
|
|
|
$0.productVersion == version &&
|
2024-11-09 23:15:50 +08:00
|
|
|
$0.language == language &&
|
|
|
|
|
!$0.status.isCompleted
|
|
|
|
|
}) { return task.directory }
|
|
|
|
|
|
2025-03-05 21:09:14 +08:00
|
|
|
let platform = globalProducts.first(where: { $0.id == productId })?.platforms.first?.id ?? "unknown"
|
|
|
|
|
let fileName = productId == "APRO"
|
|
|
|
|
? "Adobe Downloader \(productId)_\(version)_\(platform).dmg"
|
|
|
|
|
: "Adobe Downloader \(productId)_\(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
|
|
|
|
2025-02-06 16:29:59 +08:00
|
|
|
func loadSavedTasks() {
|
2025-02-06 17:57:06 +08:00
|
|
|
guard !hasLoadedSavedTasks else { return }
|
|
|
|
|
|
2025-02-06 16:29:59 +08:00
|
|
|
Task {
|
2025-02-06 23:42:49 +08:00
|
|
|
let savedTasks = await TaskPersistenceManager.shared.loadTasks()
|
2025-02-06 16:29:59 +08:00
|
|
|
await MainActor.run {
|
|
|
|
|
for task in savedTasks {
|
2025-03-05 21:09:14 +08:00
|
|
|
for product in task.dependenciesToDownload {
|
2025-02-06 16:29:59 +08:00
|
|
|
product.updateCompletedPackages()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
downloadTasks.append(contentsOf: savedTasks)
|
|
|
|
|
updateDockBadge()
|
2025-02-06 17:57:06 +08:00
|
|
|
hasLoadedSavedTasks = true
|
2025-02-06 16:29:59 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveTask(_ task: NewDownloadTask) async {
|
|
|
|
|
await TaskPersistenceManager.shared.saveTask(task)
|
2024-11-15 17:47:15 +08:00
|
|
|
objectWillChange.send()
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-06 16:29:59 +08:00
|
|
|
func configureNetworkMonitor() {
|
|
|
|
|
monitor.pathUpdateHandler = { [weak self] path in
|
|
|
|
|
let task = { @MainActor @Sendable [weak self] in
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
let wasConnected = self.isConnected
|
|
|
|
|
self.isConnected = path.status == .satisfied
|
|
|
|
|
switch (wasConnected, self.isConnected) {
|
|
|
|
|
case (false, true):
|
|
|
|
|
await self.resumePausedTasks()
|
|
|
|
|
case (true, false):
|
|
|
|
|
await self.pauseActiveTasks()
|
|
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Task(operation: task)
|
|
|
|
|
}
|
|
|
|
|
monitor.start(queue: .global(qos: .utility))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func resumePausedTasks() async {
|
|
|
|
|
for task in downloadTasks {
|
|
|
|
|
if case .paused(let info) = task.status,
|
|
|
|
|
info.reason == .networkIssue {
|
2025-03-05 21:09:14 +08:00
|
|
|
await globalNewDownloadUtils.resumeDownloadTask(taskId: task.id)
|
2025-02-06 16:29:59 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func pauseActiveTasks() async {
|
|
|
|
|
for task in downloadTasks {
|
|
|
|
|
if case .downloading = task.status {
|
2025-03-05 21:09:14 +08:00
|
|
|
await globalNewDownloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue)
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|