2024-11-09 23:15:50 +08:00
|
|
|
import Foundation
|
|
|
|
|
|
2025-07-19 23:14:19 +08:00
|
|
|
class TaskPersistenceManager: @unchecked Sendable {
|
2024-11-09 23:15:50 +08:00
|
|
|
static let shared = TaskPersistenceManager()
|
|
|
|
|
|
|
|
|
|
private let fileManager = FileManager.default
|
|
|
|
|
private var tasksDirectory: URL
|
|
|
|
|
private weak var cancelTracker: CancelTracker?
|
2025-02-06 17:57:06 +08:00
|
|
|
private var taskCache: [String: NewDownloadTask] = [:]
|
2025-07-13 00:05:01 +08:00
|
|
|
private let taskCacheQueue = DispatchQueue(label: "com.x1a0he.macOS.Adobe-Downloader.taskCache", attributes: .concurrent)
|
|
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
private init() {
|
|
|
|
|
let containerURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
|
|
|
|
tasksDirectory = containerURL.appendingPathComponent("Adobe Downloader/tasks", isDirectory: true)
|
|
|
|
|
try? fileManager.createDirectory(at: tasksDirectory, withIntermediateDirectories: true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setCancelTracker(_ tracker: CancelTracker) {
|
|
|
|
|
self.cancelTracker = tracker
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-05 21:09:14 +08:00
|
|
|
private func getTaskFileName(productId: String, version: String, language: String, platform: String) -> String {
|
|
|
|
|
return productId == "APRO"
|
|
|
|
|
? "Adobe Downloader \(productId)_\(version)_\(platform)-task.json"
|
|
|
|
|
: "Adobe Downloader \(productId)_\(version)-\(language)-\(platform)-task.json"
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-06 16:29:59 +08:00
|
|
|
func saveTask(_ task: NewDownloadTask) async {
|
2024-11-09 23:15:50 +08:00
|
|
|
let fileName = getTaskFileName(
|
2025-03-05 21:09:14 +08:00
|
|
|
productId: task.productId,
|
2025-02-27 23:02:40 +08:00
|
|
|
version: task.productVersion,
|
2024-11-09 23:15:50 +08:00
|
|
|
language: task.language,
|
|
|
|
|
platform: task.platform
|
|
|
|
|
)
|
2025-07-13 00:05:01 +08:00
|
|
|
|
2025-07-19 23:14:19 +08:00
|
|
|
await withCheckedContinuation { [weak self] continuation in
|
|
|
|
|
self?.taskCacheQueue.async(flags: .barrier) { [weak self] in
|
|
|
|
|
self?.taskCache[fileName] = task
|
2025-07-13 00:05:01 +08:00
|
|
|
continuation.resume()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
let fileURL = tasksDirectory.appendingPathComponent(fileName)
|
|
|
|
|
|
|
|
|
|
let taskData = TaskData(
|
2025-02-27 23:02:40 +08:00
|
|
|
sapCode: task.productId,
|
|
|
|
|
version: task.productVersion,
|
2024-11-09 23:15:50 +08:00
|
|
|
language: task.language,
|
|
|
|
|
displayName: task.displayName,
|
|
|
|
|
directory: task.directory,
|
2025-02-27 23:02:40 +08:00
|
|
|
productsToDownload: task.dependenciesToDownload.map { product in
|
2024-11-09 23:15:50 +08:00
|
|
|
ProductData(
|
|
|
|
|
sapCode: product.sapCode,
|
|
|
|
|
version: product.version,
|
|
|
|
|
buildGuid: product.buildGuid,
|
|
|
|
|
applicationJson: product.applicationJson,
|
|
|
|
|
packages: product.packages.map { package in
|
|
|
|
|
PackageData(
|
|
|
|
|
type: package.type,
|
|
|
|
|
fullPackageName: package.fullPackageName,
|
|
|
|
|
downloadSize: package.downloadSize,
|
|
|
|
|
downloadURL: package.downloadURL,
|
|
|
|
|
downloadedSize: package.downloadedSize,
|
|
|
|
|
progress: package.progress,
|
|
|
|
|
speed: package.speed,
|
|
|
|
|
status: package.status,
|
2024-11-18 20:33:45 +08:00
|
|
|
downloaded: package.downloaded,
|
|
|
|
|
packageVersion: package.packageVersion
|
2024-11-09 23:15:50 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
retryCount: task.retryCount,
|
|
|
|
|
createAt: task.createAt,
|
|
|
|
|
totalStatus: task.totalStatus ?? .waiting,
|
|
|
|
|
totalProgress: task.totalProgress,
|
|
|
|
|
totalDownloadedSize: task.totalDownloadedSize,
|
|
|
|
|
totalSize: task.totalSize,
|
|
|
|
|
totalSpeed: task.totalSpeed,
|
|
|
|
|
displayInstallButton: task.displayInstallButton,
|
2025-07-13 16:19:57 +08:00
|
|
|
platform: task.platform
|
2024-11-09 23:15:50 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
let encoder = JSONEncoder()
|
|
|
|
|
encoder.outputFormatting = .prettyPrinted
|
|
|
|
|
let data = try encoder.encode(taskData)
|
|
|
|
|
// print("保存数据")
|
|
|
|
|
try data.write(to: fileURL)
|
|
|
|
|
} catch {
|
|
|
|
|
print("Error saving task: \(error)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-06 23:42:49 +08:00
|
|
|
func loadTasks() async -> [NewDownloadTask] {
|
2024-11-09 23:15:50 +08:00
|
|
|
var tasks: [NewDownloadTask] = []
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
let files = try fileManager.contentsOfDirectory(at: tasksDirectory, includingPropertiesForKeys: nil)
|
|
|
|
|
for file in files where file.pathExtension == "json" {
|
2025-02-06 17:57:06 +08:00
|
|
|
let fileName = file.lastPathComponent
|
2025-07-13 00:05:01 +08:00
|
|
|
|
2025-07-19 23:14:19 +08:00
|
|
|
let cachedTask = await withCheckedContinuation { [weak self] continuation in
|
|
|
|
|
self?.taskCacheQueue.sync { [weak self] in
|
|
|
|
|
continuation.resume(returning: self?.taskCache[fileName])
|
2025-07-13 00:05:01 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let cachedTask = cachedTask {
|
2025-02-06 17:57:06 +08:00
|
|
|
tasks.append(cachedTask)
|
2025-02-06 23:42:49 +08:00
|
|
|
} else if let task = await loadTask(from: file) {
|
2025-07-19 23:14:19 +08:00
|
|
|
await withCheckedContinuation { [weak self] continuation in
|
|
|
|
|
self?.taskCacheQueue.async(flags: .barrier) { [weak self] in
|
|
|
|
|
self?.taskCache[fileName] = task
|
2025-07-13 00:05:01 +08:00
|
|
|
continuation.resume()
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-09 23:15:50 +08:00
|
|
|
tasks.append(task)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
print("Error loading tasks: \(error)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tasks
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-06 23:42:49 +08:00
|
|
|
private func loadTask(from url: URL) async -> NewDownloadTask? {
|
2024-11-09 23:15:50 +08:00
|
|
|
do {
|
|
|
|
|
let data = try Data(contentsOf: url)
|
|
|
|
|
let decoder = JSONDecoder()
|
|
|
|
|
let taskData = try decoder.decode(TaskData.self, from: data)
|
|
|
|
|
|
2025-02-27 23:02:40 +08:00
|
|
|
let products = taskData.productsToDownload.map { productData -> DependenciesToDownload in
|
|
|
|
|
let product = DependenciesToDownload(
|
2024-11-09 23:15:50 +08:00
|
|
|
sapCode: productData.sapCode,
|
|
|
|
|
version: productData.version,
|
|
|
|
|
buildGuid: productData.buildGuid,
|
|
|
|
|
applicationJson: productData.applicationJson ?? ""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
product.packages = productData.packages.map { packageData -> Package in
|
|
|
|
|
let package = Package(
|
|
|
|
|
type: packageData.type,
|
|
|
|
|
fullPackageName: packageData.fullPackageName,
|
|
|
|
|
downloadSize: packageData.downloadSize,
|
2024-11-18 20:33:45 +08:00
|
|
|
downloadURL: packageData.downloadURL,
|
|
|
|
|
packageVersion: packageData.packageVersion
|
2024-11-09 23:15:50 +08:00
|
|
|
)
|
|
|
|
|
package.downloadedSize = packageData.downloadedSize
|
|
|
|
|
package.progress = packageData.progress
|
|
|
|
|
package.speed = packageData.speed
|
|
|
|
|
package.status = packageData.status
|
|
|
|
|
package.downloaded = packageData.downloaded
|
|
|
|
|
return package
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return product
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for product in products {
|
|
|
|
|
for package in product.packages {
|
|
|
|
|
package.speed = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let initialStatus: DownloadStatus
|
|
|
|
|
switch taskData.totalStatus {
|
2025-02-06 16:29:59 +08:00
|
|
|
case .completed(let info):
|
|
|
|
|
initialStatus = .completed(info)
|
|
|
|
|
case .failed(let info):
|
|
|
|
|
initialStatus = .failed(info)
|
2024-11-09 23:15:50 +08:00
|
|
|
case .downloading:
|
|
|
|
|
initialStatus = .paused(DownloadStatus.PauseInfo(
|
2025-02-06 16:29:59 +08:00
|
|
|
reason: .other(String(localized: "程序退出")),
|
2024-11-09 23:15:50 +08:00
|
|
|
timestamp: Date(),
|
|
|
|
|
resumable: true
|
|
|
|
|
))
|
|
|
|
|
default:
|
|
|
|
|
initialStatus = .paused(DownloadStatus.PauseInfo(
|
|
|
|
|
reason: .other(String(localized: "程序重启后自动暂停")),
|
|
|
|
|
timestamp: Date(),
|
|
|
|
|
resumable: true
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let task = NewDownloadTask(
|
2025-02-27 23:02:40 +08:00
|
|
|
productId: taskData.sapCode,
|
|
|
|
|
productVersion: taskData.version,
|
2024-11-09 23:15:50 +08:00
|
|
|
language: taskData.language,
|
|
|
|
|
displayName: taskData.displayName,
|
|
|
|
|
directory: taskData.directory,
|
2025-02-27 23:02:40 +08:00
|
|
|
dependenciesToDownload: products,
|
2024-11-09 23:15:50 +08:00
|
|
|
retryCount: taskData.retryCount,
|
|
|
|
|
createAt: taskData.createAt,
|
|
|
|
|
totalStatus: initialStatus,
|
|
|
|
|
totalProgress: taskData.totalProgress,
|
|
|
|
|
totalDownloadedSize: taskData.totalDownloadedSize,
|
|
|
|
|
totalSize: taskData.totalSize,
|
|
|
|
|
totalSpeed: 0,
|
|
|
|
|
currentPackage: products.first?.packages.first,
|
|
|
|
|
platform: taskData.platform
|
|
|
|
|
)
|
|
|
|
|
task.displayInstallButton = taskData.displayInstallButton
|
|
|
|
|
|
|
|
|
|
return task
|
|
|
|
|
} catch {
|
|
|
|
|
print("Error loading task from \(url): \(error)")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func removeTask(_ task: NewDownloadTask) {
|
|
|
|
|
let fileName = getTaskFileName(
|
2025-03-05 21:09:14 +08:00
|
|
|
productId: task.productId,
|
2025-02-27 23:02:40 +08:00
|
|
|
version: task.productVersion,
|
2024-11-09 23:15:50 +08:00
|
|
|
language: task.language,
|
|
|
|
|
platform: task.platform
|
|
|
|
|
)
|
|
|
|
|
let fileURL = tasksDirectory.appendingPathComponent(fileName)
|
2025-07-13 00:05:01 +08:00
|
|
|
|
2025-07-19 23:14:19 +08:00
|
|
|
taskCacheQueue.async(flags: .barrier) { [weak self] in
|
|
|
|
|
self?.taskCache.removeValue(forKey: fileName)
|
2025-07-13 00:05:01 +08:00
|
|
|
}
|
2024-11-09 23:15:50 +08:00
|
|
|
|
|
|
|
|
try? fileManager.removeItem(at: fileURL)
|
|
|
|
|
}
|
2025-02-06 23:42:49 +08:00
|
|
|
|
2025-03-05 21:09:14 +08:00
|
|
|
func createExistingProgramTask(productId: String, version: String, language: String, displayName: String, platform: String, directory: URL) async {
|
2025-02-06 23:42:49 +08:00
|
|
|
let fileName = getTaskFileName(
|
2025-03-05 21:09:14 +08:00
|
|
|
productId: productId,
|
2025-02-06 23:42:49 +08:00
|
|
|
version: version,
|
|
|
|
|
language: language,
|
|
|
|
|
platform: platform
|
|
|
|
|
)
|
|
|
|
|
|
2025-02-27 23:02:40 +08:00
|
|
|
let product = DependenciesToDownload(
|
2025-03-05 21:09:14 +08:00
|
|
|
sapCode: productId,
|
2025-02-06 23:42:49 +08:00
|
|
|
version: version,
|
|
|
|
|
buildGuid: "",
|
|
|
|
|
applicationJson: ""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let package = Package(
|
|
|
|
|
type: "",
|
|
|
|
|
fullPackageName: "",
|
|
|
|
|
downloadSize: 0,
|
|
|
|
|
downloadURL: "",
|
|
|
|
|
packageVersion: version
|
|
|
|
|
)
|
|
|
|
|
package.downloaded = true
|
|
|
|
|
package.progress = 1.0
|
|
|
|
|
package.status = .completed
|
|
|
|
|
|
|
|
|
|
product.packages = [package]
|
|
|
|
|
|
|
|
|
|
let task = NewDownloadTask(
|
2025-03-05 21:09:14 +08:00
|
|
|
productId: productId,
|
2025-02-27 23:02:40 +08:00
|
|
|
productVersion: version,
|
2025-02-06 23:42:49 +08:00
|
|
|
language: language,
|
|
|
|
|
displayName: displayName,
|
|
|
|
|
directory: directory,
|
2025-02-27 23:02:40 +08:00
|
|
|
dependenciesToDownload: [product],
|
2025-02-06 23:42:49 +08:00
|
|
|
retryCount: 0,
|
|
|
|
|
createAt: Date(),
|
|
|
|
|
totalStatus: .completed(DownloadStatus.CompletionInfo(
|
|
|
|
|
timestamp: Date(),
|
|
|
|
|
totalTime: 0,
|
|
|
|
|
totalSize: 0
|
|
|
|
|
)),
|
|
|
|
|
totalProgress: 1.0,
|
|
|
|
|
totalDownloadedSize: 0,
|
|
|
|
|
totalSize: 0,
|
|
|
|
|
totalSpeed: 0,
|
|
|
|
|
currentPackage: package,
|
|
|
|
|
platform: platform
|
|
|
|
|
)
|
|
|
|
|
task.displayInstallButton = true
|
2025-07-13 00:05:01 +08:00
|
|
|
|
2025-07-19 23:14:19 +08:00
|
|
|
await withCheckedContinuation { [weak self] continuation in
|
|
|
|
|
self?.taskCacheQueue.async(flags: .barrier) { [weak self] in
|
|
|
|
|
self?.taskCache[fileName] = task
|
2025-07-13 00:05:01 +08:00
|
|
|
continuation.resume()
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-06 23:42:49 +08:00
|
|
|
|
|
|
|
|
await saveTask(task)
|
|
|
|
|
}
|
2025-07-13 00:05:01 +08:00
|
|
|
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct TaskData: Codable {
|
|
|
|
|
let sapCode: String
|
|
|
|
|
let version: String
|
|
|
|
|
let language: String
|
|
|
|
|
let displayName: String
|
|
|
|
|
let directory: URL
|
|
|
|
|
let productsToDownload: [ProductData]
|
|
|
|
|
let retryCount: Int
|
|
|
|
|
let createAt: Date
|
|
|
|
|
let totalStatus: DownloadStatus
|
|
|
|
|
let totalProgress: Double
|
|
|
|
|
let totalDownloadedSize: Int64
|
|
|
|
|
let totalSize: Int64
|
|
|
|
|
let totalSpeed: Double
|
|
|
|
|
let displayInstallButton: Bool
|
|
|
|
|
let platform: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct ProductData: Codable {
|
|
|
|
|
let sapCode: String
|
|
|
|
|
let version: String
|
|
|
|
|
let buildGuid: String
|
|
|
|
|
let applicationJson: String?
|
|
|
|
|
let packages: [PackageData]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct PackageData: Codable {
|
|
|
|
|
let type: String
|
|
|
|
|
let fullPackageName: String
|
|
|
|
|
let downloadSize: Int64
|
|
|
|
|
let downloadURL: String
|
|
|
|
|
let downloadedSize: Int64
|
|
|
|
|
let progress: Double
|
|
|
|
|
let speed: Double
|
|
|
|
|
let status: PackageStatus
|
|
|
|
|
let downloaded: Bool
|
2024-11-18 20:33:45 +08:00
|
|
|
let packageVersion: String
|
2024-11-09 23:15:50 +08:00
|
|
|
}
|