Files
Adobe-Downloader/Adobe Downloader/Commons/Extensions.swift

239 lines
9.3 KiB
Swift

//
// Adobe-Downloader
//
// Created by X1a0He on 2024/10/30.
//
import Foundation
import AppKit
extension FileManager {
func volumeAvailableCapacity(for url: URL) throws -> Int64 {
let resourceValues = try url.resourceValues(forKeys: [.volumeAvailableCapacityKey])
return Int64(resourceValues.volumeAvailableCapacity ?? 0)
}
}
extension Product.ProductVersion {
var size: Int64 {
return 0
}
}
extension DownloadTask {
var startTime: Date {
switch status {
case .downloading(let info):
return info.startTime
case .completed(let info):
return info.timestamp.addingTimeInterval(-info.totalTime)
case .preparing(let info):
return info.timestamp
case .paused(let info):
return info.timestamp
case .retrying(let info):
return info.nextRetryDate.addingTimeInterval(-60)
case .failed(let info):
return info.timestamp
case .waiting:
return Date()
}
}
}
extension NetworkManager {
func handleDownloadCompletion(taskId: UUID, packageIndex: Int) async {
await MainActor.run {
guard let taskIndex = downloadTasks.firstIndex(where: { $0.id == taskId }) else { return }
downloadTasks[taskIndex].packages[packageIndex].downloaded = true
downloadTasks[taskIndex].packages[packageIndex].progress = 1.0
downloadTasks[taskIndex].packages[packageIndex].status = .completed
if let nextPackageIndex = downloadTasks[taskIndex].packages.firstIndex(where: { !$0.downloaded }) {
downloadTasks[taskIndex].status = .downloading(DownloadTask.DownloadStatus.DownloadInfo(
fileName: downloadTasks[taskIndex].packages[nextPackageIndex].name,
currentPackageIndex: nextPackageIndex,
totalPackages: downloadTasks[taskIndex].packages.count,
startTime: Date(),
estimatedTimeRemaining: nil
))
Task {
await resumeDownload(taskId: taskId)
}
} else {
let startTime = downloadTasks[taskIndex].startTime
let totalTime = Date().timeIntervalSince(startTime)
downloadTasks[taskIndex].status = .completed(DownloadTask.DownloadStatus.CompletionInfo(
timestamp: Date(),
totalTime: totalTime,
totalSize: downloadTasks[taskIndex].totalSize
))
downloadTasks[taskIndex].progress = 1.0
progressObservers[taskId]?.invalidate()
progressObservers.removeValue(forKey: taskId)
if activeDownloadTaskId == taskId {
activeDownloadTaskId = nil
}
updateDockBadge()
objectWillChange.send()
Task {
do {
try await downloadUtils.clearExtendedAttributes(at: downloadTasks[taskIndex].destinationURL)
print("Successfully cleared extended attributes for \(downloadTasks[taskIndex].destinationURL.path)")
} catch {
print("Failed to clear extended attributes: \(error.localizedDescription)")
}
}
}
}
}
}
extension NetworkManager {
func getApplicationInfo(buildGuid: String) async throws -> ApplicationInfo {
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["Accept"] = "application/json"
headers["Connection"] = "keep-alive"
headers["Cookie"] = 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))
}
do {
let decoder = JSONDecoder()
let applicationInfo: ApplicationInfo = try decoder.decode(ApplicationInfo.self, from: data)
return applicationInfo
} catch {
throw NetworkError.parsingError(error, "Failed to parse application info")
}
}
func fetchProductsData() async throws -> ([String: Product], String) {
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")
]
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: ([String: Product], String) = try await Task.detached(priority: .userInitiated) {
let parseResult = try XHXMLParser.parse(
xmlString: xmlString,
urlVersion: 6,
allowedPlatforms: Set(["osx10-64", "osx10", "macuniversal", "macarm64"])
)
return (parseResult.products, parseResult.cdn)
}.value
return result
}
func getDownloadPath(for fileName: String) async throws -> URL {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.main.async {
let panel = NSOpenPanel()
panel.title = "选择保存位置"
panel.canCreateDirectories = true
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
if panel.runModal() == .OK {
if let baseURL = panel.url {
continuation.resume(returning: baseURL)
} else {
continuation.resume(throwing: NetworkError.fileSystemError("未选择保存位置", nil))
}
} else {
continuation.resume(throwing: NetworkError.fileSystemError("用户取消了操作", nil))
}
}
}
}
func configureNetworkMonitor() {
monitor.pathUpdateHandler = { [weak self] path in
Task { @MainActor in
guard let self = self else { return }
let wasConnected = self.isConnected
self.isConnected = path.status == .satisfied
if !wasConnected && self.isConnected {
for task in self.downloadTasks where task.status.isPaused {
if case .paused(let info) = task.status,
info.reason == .networkIssue {
await self.resumeDownload(taskId: task.id)
}
}
} else if wasConnected && !self.isConnected {
for task in self.downloadTasks where task.status.isActive {
await self.downloadUtils.pauseDownloadTask(
taskId: task.id,
reason: DownloadTask.DownloadStatus.PauseInfo.PauseReason.networkIssue
)
}
}
}
}
monitor.start(queue: DispatchQueue.global(qos: .utility))
}
func generateCookie() -> String {
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let randomString = String((0..<26).map { _ in letters.randomElement()! })
return "fg=\(randomString)======"
}
func updateDockBadge() {
let activeCount = downloadTasks.filter { $0.status.isActive }.count
if activeCount > 0 {
NSApplication.shared.dockTile.badgeLabel = "\(activeCount)"
} else {
NSApplication.shared.dockTile.badgeLabel = nil
}
}
}