Files
suixinkan_uikit/suixinkan/Features/CloudDrive/Services/CloudTransferManager.swift
汉秋 005349f8e6 模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:01:08 +08:00

334 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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.session.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<Void, Never>] = [:]
private var operationGenerations: [String: Int] = [:]
///
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
invalidateOperation(id: id)
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
operationGenerations[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
invalidateOperation(id: id)
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
operationGenerations[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]
let generation = nextOperationGeneration(id: id)
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
guard let self, self.isCurrentOperation(id: id, generation: generation) else { return }
self.updateUploadProgress(id: id, progress: progress)
}
}
try Task.checkCancellation()
guard isCurrentOperation(id: id, generation: generation) else { return }
try await api.uploadCloudFile(
parentFolderId: seed.parentFolderId,
fileUrl: uploadedURL,
fileName: seed.fileName
)
guard isCurrentOperation(id: id, generation: generation) else { return }
try? FileManager.default.removeItem(atPath: seed.localPath)
runningTasks[id] = nil
operationGenerations[id] = nil
uploadTasks.removeAll { $0.id == id }
persistAndNotify()
onUploadCompleted?(seed.parentFolderId)
} catch is CancellationError {
guard isCurrentOperation(id: id, generation: generation) else { return }
runningTasks[id] = nil
markUploadPaused(id: id)
} catch {
guard isCurrentOperation(id: id, generation: generation) else { return }
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]
let generation = nextOperationGeneration(id: id)
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
guard let self, self.isCurrentOperation(id: id, generation: generation) else { return }
self.updateDownloadProgress(id: id, progress: progress)
}
}
try Task.checkCancellation()
guard isCurrentOperation(id: id, generation: generation) else { return }
try await photoSaver.saveToPhotoLibrary(fileURL: localURL, fileType: seed.fileType)
guard isCurrentOperation(id: id, generation: generation) else { return }
try? FileManager.default.removeItem(at: localURL)
runningTasks[id] = nil
operationGenerations[id] = nil
updateDownloadTask(id: id) { task in
task.status = .completed
task.progress = 100
task.updatedAt = Date().timeIntervalSince1970
}
} catch is CancellationError {
guard isCurrentOperation(id: id, generation: generation) else { return }
runningTasks[id] = nil
markDownloadPaused(id: id)
} catch {
guard isCurrentOperation(id: id, generation: generation) else { return }
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 func nextOperationGeneration(id: String) -> Int {
let generation = (operationGenerations[id] ?? 0) + 1
operationGenerations[id] = generation
return generation
}
private func invalidateOperation(id: String) {
operationGenerations[id] = (operationGenerations[id] ?? 0) + 1
}
private func isCurrentOperation(id: String, generation: Int) -> Bool {
operationGenerations[id] == generation
}
}
private extension Comparable {
func coerceIn(_ range: ClosedRange<Self>) -> Self {
min(max(self, range.lowerBound), range.upperBound)
}
}