feat: 添加云盘与消息中心功能

This commit is contained in:
2026-07-09 22:37:43 +08:00
parent 8e356973bd
commit f20ec7f06c
91 changed files with 5437 additions and 89 deletions

View File

@ -0,0 +1,300 @@
//
// 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<Void, Never>] = [:]
///
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>) -> Self {
min(max(self, range.lowerBound), range.upperBound)
}
}