mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 11:18:53 +08:00
940 lines
38 KiB
Swift
940 lines
38 KiB
Swift
//
|
|
// Adobe Downloader
|
|
//
|
|
// Created by X1a0He on 2024/10/30.
|
|
//
|
|
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()
|
|
|
|
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 }
|
|
|
|
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
|
}
|
|
|
|
func cleanup() {
|
|
completionHandler = { _, _, _ in }
|
|
progressHandler = nil
|
|
}
|
|
}
|
|
|
|
func pauseDownloadTask(taskId: UUID, reason: DownloadStatus.PauseInfo.PauseReason) async {
|
|
await MainActor.run {
|
|
if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
|
task.setStatus(.paused(DownloadStatus.PauseInfo(
|
|
reason: reason,
|
|
timestamp: Date(),
|
|
resumable: true
|
|
)))
|
|
}
|
|
}
|
|
await cancelTracker.pause(taskId)
|
|
}
|
|
|
|
func resumeDownloadTask(taskId: UUID) async {
|
|
await MainActor.run {
|
|
if let task = 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
|
|
)))
|
|
}
|
|
}
|
|
|
|
if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
|
await startDownloadProcess(task: task)
|
|
}
|
|
}
|
|
|
|
func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
|
|
await cancelTracker.cancel(taskId)
|
|
|
|
await MainActor.run {
|
|
if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
|
|
if removeFiles {
|
|
try? FileManager.default.removeItem(at: task.directory)
|
|
}
|
|
|
|
task.setStatus(.failed(DownloadStatus.FailureInfo(
|
|
message: "下载已取消",
|
|
error: NetworkError.downloadCancelled,
|
|
timestamp: Date(),
|
|
recoverable: false
|
|
)))
|
|
|
|
networkManager?.updateDockBadge()
|
|
networkManager?.objectWillChange.send()
|
|
}
|
|
}
|
|
}
|
|
|
|
func signApp(at url: URL) async throws {
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/codesign")
|
|
process.arguments = ["--force", "--deep", "--sign", "-", url.path]
|
|
try process.run()
|
|
process.waitUntilExit()
|
|
}
|
|
|
|
func generateDriverXML(sapCode: String, version: String, language: String, productInfo: Sap.Versions, displayName: String) -> String {
|
|
let dependencies = productInfo.dependencies.map { dependency in
|
|
"""
|
|
<Dependency>
|
|
<SAPCode>\(dependency.sapCode)</SAPCode>
|
|
<BaseVersion>\(dependency.version)</BaseVersion>
|
|
<EsdDirectory>\(dependency.sapCode)</EsdDirectory>
|
|
</Dependency>
|
|
"""
|
|
}.joined(separator: "\n")
|
|
|
|
return """
|
|
<DriverInfo>
|
|
<ProductInfo>
|
|
<Name>Adobe \(displayName)</Name>
|
|
<SAPCode>\(sapCode)</SAPCode>
|
|
<CodexVersion>\(version)</CodexVersion>
|
|
<Platform>\(productInfo.apPlatform)</Platform>
|
|
<EsdDirectory>\(sapCode)</EsdDirectory>
|
|
<Dependencies>
|
|
\(dependencies)
|
|
</Dependencies>
|
|
</ProductInfo>
|
|
<RequestInfo>
|
|
<InstallDir>/Applications</InstallDir>
|
|
<InstallLanguage>\(language)</InstallLanguage>
|
|
</RequestInfo>
|
|
</DriverInfo>
|
|
"""
|
|
}
|
|
|
|
func clearExtendedAttributes(at url: URL) async throws {
|
|
let escapedPath = url.path.replacingOccurrences(of: "'", with: "'\\''")
|
|
let script = """
|
|
do shell script "sudo xattr -cr '\(escapedPath)'" with administrator privileges
|
|
"""
|
|
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
process.arguments = ["-e", script]
|
|
|
|
let pipe = Pipe()
|
|
process.standardOutput = pipe
|
|
process.standardError = pipe
|
|
|
|
do {
|
|
try process.run()
|
|
process.waitUntilExit()
|
|
|
|
if process.terminationStatus != 0 {
|
|
let data = try pipe.fileHandleForReading.readToEnd() ?? Data()
|
|
if let output = String(data: data, encoding: .utf8) {
|
|
print("xattr command output:", output)
|
|
}
|
|
}
|
|
} catch {
|
|
print("Error executing xattr command:", error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
internal 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 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 {
|
|
try await downloadPackage(package: package, task: task, product: product, url: url)
|
|
} catch {
|
|
print("Error downloading \(package.fullPackageName): \(error.localizedDescription)")
|
|
await self.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
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func downloadPackage(package: Package, task: NewDownloadTask, product: ProductsToDownload, url: URL) 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 { @MainActor in
|
|
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
|
|
)))
|
|
}
|
|
|
|
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 completedSize: Int64 = 0
|
|
var totalSize: Int64 = 0
|
|
|
|
for prod in task.productsToDownload {
|
|
for pkg in prod.packages {
|
|
totalSize += pkg.downloadSize
|
|
if pkg.downloaded {
|
|
completedSize += pkg.downloadSize
|
|
} else if pkg.id == package.id {
|
|
completedSize += totalBytesWritten
|
|
}
|
|
}
|
|
}
|
|
|
|
task.totalSize = totalSize
|
|
task.totalDownloadedSize = completedSize
|
|
task.totalProgress = Double(completedSize) / Double(totalSize)
|
|
task.totalSpeed = speed
|
|
|
|
lastUpdateTime = now
|
|
lastBytes = totalBytesWritten
|
|
|
|
networkManager?.objectWillChange.send()
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
var request = URLRequest(url: url)
|
|
NetworkConstants.downloadHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
|
|
|
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
|
|
|
|
Task {
|
|
if let resumeData = await cancelTracker.getResumeData(task.id) {
|
|
let downloadTask = session.downloadTask(withResumeData: resumeData)
|
|
await cancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
|
await cancelTracker.clearResumeData(task.id)
|
|
downloadTask.resume()
|
|
} else {
|
|
let downloadTask = session.downloadTask(with: request)
|
|
await cancelTracker.registerTask(task.id, task: downloadTask, session: session)
|
|
downloadTask.resume()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 manifestXML = try XMLDocument(data: manifestData)
|
|
|
|
guard let downloadPath = try manifestXML.nodes(forXPath: "//asset_list/asset/asset_path").first?.stringValue,
|
|
let assetSizeStr = try manifestXML.nodes(forXPath: "//asset_list/asset/asset_size").first?.stringValue,
|
|
let assetSize = Int64(assetSizeStr) else {
|
|
throw NetworkError.invalidData("无法从manifest中获取下载信息")
|
|
}
|
|
|
|
let aproPackage = Package(
|
|
type: "dmg",
|
|
fullPackageName: "Adobe Downloader \(task.sapCode)_\(productInfo.productVersion)_\(productInfo.apPlatform).dmg",
|
|
downloadSize: assetSize,
|
|
downloadURL: downloadPath
|
|
)
|
|
|
|
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
|
|
|
|
let delegate = DownloadDelegate(
|
|
destinationDirectory: tempDownloadDir,
|
|
fileName: aproPackage.fullPackageName,
|
|
completionHandler: { [weak networkManager] (localURL: URL?, response: URLResponse?, error: Error?) in
|
|
if let error = error {
|
|
print("Download error:", error)
|
|
return
|
|
}
|
|
Task { @MainActor in
|
|
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
|
|
|
|
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
|
|
)))
|
|
}
|
|
|
|
task.objectWillChange.send()
|
|
networkManager?.objectWillChange.send()
|
|
}
|
|
},
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
guard let fullURL = URL(string: downloadPath) else {
|
|
throw NetworkError.invalidURL(downloadPath)
|
|
}
|
|
|
|
var request2 = URLRequest(url: fullURL)
|
|
NetworkConstants.downloadHeaders.forEach { request2.setValue($0.value, forHTTPHeaderField: $0.key) }
|
|
|
|
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
|
|
let downloadTask = session.downloadTask(with: request2)
|
|
downloadTask.resume()
|
|
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
let originalCompletionHandler = delegate.completionHandler
|
|
|
|
delegate.completionHandler = { (url: URL?, response: URLResponse?, error: Error?) in
|
|
originalCompletionHandler(url, response, error)
|
|
|
|
if let error = error {
|
|
continuation.resume(throwing: error)
|
|
} else {
|
|
continuation.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 {
|
|
let sortedVersions = dependencyVersions.sorted { first, second in
|
|
first.value.productVersion.compare(second.value.productVersion, options: .numeric) == .orderedDescending
|
|
}
|
|
|
|
var firstGuid = "", buildGuid = ""
|
|
|
|
for (_, versionInfo) in sortedVersions where versionInfo.baseVersion == dependency.version {
|
|
if firstGuid.isEmpty { firstGuid = versionInfo.buildGuid }
|
|
|
|
if allowedPlatform.contains(versionInfo.apPlatform) {
|
|
buildGuid = versionInfo.buildGuid
|
|
break
|
|
}
|
|
}
|
|
|
|
if buildGuid.isEmpty { buildGuid = firstGuid }
|
|
|
|
if !buildGuid.isEmpty {
|
|
productsToDownload.append(ProductsToDownload(
|
|
sapCode: dependency.sapCode,
|
|
version: dependency.version,
|
|
buildGuid: buildGuid
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
for product in productsToDownload {
|
|
await MainActor.run {
|
|
task.setStatus(.preparing(DownloadStatus.PrepareInfo(
|
|
message: String(localized: "正在处理 \(product.sapCode) 的包信息..."),
|
|
timestamp: Date(),
|
|
stage: .fetchingInfo
|
|
)))
|
|
}
|
|
|
|
let productDir = task.directory.appendingPathComponent("\(product.sapCode)")
|
|
if !FileManager.default.fileExists(atPath: productDir.path) {
|
|
try FileManager.default.createDirectory(at: productDir, withIntermediateDirectories: true)
|
|
}
|
|
let jsonString = try await getApplicationInfo(buildGuid: product.buildGuid)
|
|
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("无法解析产品信息")
|
|
}
|
|
|
|
for package in packageArray {
|
|
let fullPackageName: String
|
|
if let name = package["fullPackageName"] as? String, !name.isEmpty {
|
|
fullPackageName = name
|
|
} else if let name = package["PackageName"] as? String, !name.isEmpty {
|
|
fullPackageName = "\(name).zip"
|
|
} else { continue }
|
|
|
|
let packageType = package["Type"] as? String ?? "non-core"
|
|
let isLanguageSuitable: Bool
|
|
if packageType == "core" {
|
|
isLanguageSuitable = true
|
|
} else {
|
|
let condition = package["Condition"] as? String ?? ""
|
|
let osLang = Locale.current.identifier
|
|
isLanguageSuitable = (
|
|
task.language == "ALL" || condition.isEmpty ||
|
|
!condition.contains("[installLanguage]") || condition.contains("[installLanguage]==\(task.language)") ||
|
|
condition.contains("[installLanguage]==\(osLang)")
|
|
)
|
|
}
|
|
|
|
if isLanguageSuitable {
|
|
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 }
|
|
|
|
guard let downloadURL = package["Path"] as? String, !downloadURL.isEmpty else { continue }
|
|
|
|
let newPackage = Package(
|
|
type: packageType,
|
|
fullPackageName: fullPackageName,
|
|
downloadSize: downloadSize,
|
|
downloadURL: downloadURL
|
|
)
|
|
product.packages.append(newPackage)
|
|
}
|
|
}
|
|
}
|
|
|
|
let finalProducts = productsToDownload
|
|
let totalSize = finalProducts.reduce(0) { productSum, product in
|
|
productSum + product.packages.reduce(0) { packageSum, pkg in
|
|
packageSum + (pkg.downloadSize > 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["Cookie"] = await networkManager?.generateCookie() ?? ""
|
|
|
|
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
|
|
}
|
|
|
|
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 && 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 {
|
|
await MainActor.run {
|
|
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)
|
|
}
|
|
|
|
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 ("网络连接已断开", true)
|
|
case .timeout:
|
|
return ("下载超时", true)
|
|
case .serverUnreachable:
|
|
return ("服务器无法访问", true)
|
|
case .insufficientStorage:
|
|
return ("存储空间不足", false)
|
|
case .filePermissionDenied:
|
|
return ("没有入权限", false)
|
|
default:
|
|
return (networkError.localizedDescription, false)
|
|
}
|
|
case let urlError as URLError:
|
|
switch urlError.code {
|
|
case .notConnectedToInternet:
|
|
return ("网络连接已开", true)
|
|
case .timedOut:
|
|
return ("连接超时", true)
|
|
case .cancelled:
|
|
return ("下载已取消", false)
|
|
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()
|
|
}
|
|
}
|
|
}
|