// // CloudTransferManager.swift // suixinkan // import Foundation @MainActor /// 云盘上传/下载传输管理器,负责本地任务、进度和服务端登记。 final class CloudTransferManager { static let shared = CloudTransferManager( store: CloudTransferJSONStore(), uploader: NetworkServices.shared.ossUploadService, api: NetworkServices.shared.cloudDriveAPI, downloader: CloudURLSessionDownloader(), photoSaver: CloudPhotoLibrarySaver(), scenicIdProvider: { AppStore.shared.currentScenicId } ) private(set) var uploadTasks: [CloudTransferTask] = [] private(set) var downloadTasks: [CloudTransferTask] = [] var onTasksChange: (() -> Void)? var onUploadCompleted: ((Int) -> Void)? var onShowMessage: ((String) -> Void)? private let store: CloudTransferTaskStoring private let uploader: any CloudDriveUploading private let api: any CloudDriveServing private let downloader: any CloudFileDownloading private let photoSaver: any CloudPhotoSaving private let scenicIdProvider: () -> Int private var runningTasks: [String: Task] = [:] /// 初始化传输管理器。 init( store: CloudTransferTaskStoring, uploader: any CloudDriveUploading, api: any CloudDriveServing, downloader: any CloudFileDownloading, photoSaver: any CloudPhotoSaving, scenicIdProvider: @escaping () -> Int ) { self.store = store self.uploader = uploader self.api = api self.downloader = downloader self.photoSaver = photoSaver self.scenicIdProvider = scenicIdProvider let tasks = store.loadTasks() uploadTasks = tasks.filter { $0.kind == .upload } downloadTasks = tasks.filter { $0.kind == .download } } /// 添加上传任务并立即开始。 func addUploadTask( localFileURL: URL, fileName: String, fileType: Int, fileSize: Int64, parentFolderId: Int ) { let task = CloudTransferTask( kind: .upload, fileName: fileName, fileType: fileType, fileSize: fileSize, parentFolderId: parentFolderId, localPath: localFileURL.path, status: .pending ) uploadTasks.insert(task, at: 0) persistAndNotify() startUpload(task.id) onShowMessage?("已添加上传任务") } /// 添加下载任务并立即开始。 func addDownloadTask(fileURL: String, fileName: String, fileType: Int, fileSize: Int64 = 0) { let task = CloudTransferTask( kind: .download, fileName: fileName, fileType: fileType, fileSize: fileSize, remoteURL: fileURL, status: .pending ) downloadTasks.insert(task, at: 0) persistAndNotify() startDownload(task.id) onShowMessage?("已添加下载任务,可在传输管理查看") } /// 暂停上传任务。 func pauseUpload(id: String) { runningTasks[id]?.cancel() runningTasks[id] = nil updateUploadTask(id: id) { task in task.status = .paused task.updatedAt = Date().timeIntervalSince1970 } } /// 继续上传任务。 func resumeUpload(id: String) { startUpload(id) } /// 取消上传任务。 func cancelUpload(id: String) { runningTasks[id]?.cancel() runningTasks[id] = nil if let task = uploadTasks.first(where: { $0.id == id }), !task.localPath.isEmpty { try? FileManager.default.removeItem(atPath: task.localPath) } uploadTasks.removeAll { $0.id == id } persistAndNotify() } /// 暂停下载任务。 func pauseDownload(id: String) { runningTasks[id]?.cancel() runningTasks[id] = nil updateDownloadTask(id: id) { task in task.status = .paused task.updatedAt = Date().timeIntervalSince1970 } } /// 继续下载任务。 func resumeDownload(id: String) { startDownload(id) } /// 取消下载任务。 func cancelDownload(id: String) { runningTasks[id]?.cancel() runningTasks[id] = nil downloadTasks.removeAll { $0.id == id } persistAndNotify() } private func startUpload(_ id: String) { guard runningTasks[id] == nil, let index = uploadTasks.firstIndex(where: { $0.id == id }) else { return } uploadTasks[index].status = .uploading uploadTasks[index].errorMessage = "" uploadTasks[index].progress = max(uploadTasks[index].progress, 1) uploadTasks[index].updatedAt = Date().timeIntervalSince1970 let seed = uploadTasks[index] persistAndNotify() runningTasks[id] = Task { [weak self] in guard let self else { return } do { guard FileManager.default.fileExists(atPath: seed.localPath) else { throw CloudTransferError.localFileMissing } let data = try Data(contentsOf: URL(fileURLWithPath: seed.localPath)) try Task.checkCancellation() let uploadedURL = try await uploader.uploadCloudDriveFile( data: data, fileName: seed.fileName, fileType: seed.fileType, scenicId: scenicIdProvider() ) { [weak self] progress in Task { @MainActor in self?.updateUploadProgress(id: id, progress: progress) } } try Task.checkCancellation() try await api.uploadCloudFile( parentFolderId: seed.parentFolderId, fileUrl: uploadedURL, fileName: seed.fileName ) try? FileManager.default.removeItem(atPath: seed.localPath) runningTasks[id] = nil uploadTasks.removeAll { $0.id == id } persistAndNotify() onUploadCompleted?(seed.parentFolderId) } catch is CancellationError { runningTasks[id] = nil markUploadPaused(id: id) } catch { runningTasks[id] = nil markUploadFailed(id: id, message: error.localizedDescription) } } } private func startDownload(_ id: String) { guard runningTasks[id] == nil, let index = downloadTasks.firstIndex(where: { $0.id == id }) else { return } downloadTasks[index].status = .downloading downloadTasks[index].errorMessage = "" downloadTasks[index].progress = max(downloadTasks[index].progress, 1) downloadTasks[index].updatedAt = Date().timeIntervalSince1970 let seed = downloadTasks[index] persistAndNotify() runningTasks[id] = Task { [weak self] in guard let self else { return } do { let localURL = try await downloader.downloadFile( remoteURL: seed.remoteURL, fileName: seed.fileName ) { [weak self] progress in Task { @MainActor in self?.updateDownloadProgress(id: id, progress: progress) } } try Task.checkCancellation() try await photoSaver.saveToPhotoLibrary(fileURL: localURL, fileType: seed.fileType) try? FileManager.default.removeItem(at: localURL) runningTasks[id] = nil updateDownloadTask(id: id) { task in task.status = .completed task.progress = 100 task.updatedAt = Date().timeIntervalSince1970 } } catch is CancellationError { runningTasks[id] = nil markDownloadPaused(id: id) } catch { runningTasks[id] = nil markDownloadFailed(id: id, message: error.localizedDescription) } } } private func updateUploadProgress(id: String, progress: Int) { updateUploadTask(id: id) { task in task.progress = max(task.progress, progress.coerceIn(0 ... 100)) task.updatedAt = Date().timeIntervalSince1970 } } private func updateDownloadProgress(id: String, progress: Int) { updateDownloadTask(id: id) { task in task.progress = max(task.progress, progress.coerceIn(0 ... 100)) task.updatedAt = Date().timeIntervalSince1970 } } private func markUploadPaused(id: String) { updateUploadTask(id: id) { task in task.status = .paused task.updatedAt = Date().timeIntervalSince1970 } } private func markDownloadPaused(id: String) { updateDownloadTask(id: id) { task in task.status = .paused task.updatedAt = Date().timeIntervalSince1970 } } private func markUploadFailed(id: String, message: String) { updateUploadTask(id: id) { task in task.status = .failed task.errorMessage = message task.updatedAt = Date().timeIntervalSince1970 } } private func markDownloadFailed(id: String, message: String) { updateDownloadTask(id: id) { task in task.status = .failed task.errorMessage = message task.updatedAt = Date().timeIntervalSince1970 } } private func updateUploadTask(id: String, mutation: (inout CloudTransferTask) -> Void) { guard let index = uploadTasks.firstIndex(where: { $0.id == id }) else { return } mutation(&uploadTasks[index]) persistAndNotify() } private func updateDownloadTask(id: String, mutation: (inout CloudTransferTask) -> Void) { guard let index = downloadTasks.firstIndex(where: { $0.id == id }) else { return } mutation(&downloadTasks[index]) persistAndNotify() } private func persistAndNotify() { store.saveTasks(uploadTasks + downloadTasks) onTasksChange?() } } private extension Comparable { func coerceIn(_ range: ClosedRange) -> Self { min(max(self, range.lowerBound), range.upperBound) } }