同步相册云盘界面并完善相关交互

This commit is contained in:
2026-07-10 17:15:15 +08:00
parent ab5220e460
commit ceca780ab3
13 changed files with 2269 additions and 532 deletions

View File

@ -77,6 +77,104 @@ final class CloudTransferManagerTests: XCTestCase {
XCTAssertTrue(manager.downloadTasks.isEmpty)
}
func testUploadPauseThenResumeCompletes() async throws {
let uploader = PausingThenSucceedCloudUploader(url: "https://cdn/resumed.jpg")
let api = TransferMockCloudDriveAPI()
let manager = CloudTransferManager(
store: MemoryTransferStore(),
uploader: uploader,
api: api,
downloader: FakeDownloader(),
photoSaver: FakePhotoSaver(),
scenicIdProvider: { 10 }
)
let fileURL = try makeTempFile(name: "resume.jpg")
manager.addUploadTask(localFileURL: fileURL, fileName: "resume.jpg", fileType: 2, fileSize: 3, parentFolderId: 7)
let id = try XCTUnwrap(manager.uploadTasks.first?.id)
try await waitUntil { uploader.attempts == 1 }
manager.pauseUpload(id: id)
try await waitUntil { manager.uploadTasks.first?.status == .paused }
manager.resumeUpload(id: id)
try await waitUntil { manager.uploadTasks.isEmpty }
XCTAssertEqual(uploader.attempts, 2)
XCTAssertEqual(api.uploadCalls.first?.parentFolderId, 7)
XCTAssertEqual(api.uploadCalls.first?.fileUrl, "https://cdn/resumed.jpg")
}
func testFailedUploadCanRetry() async throws {
let uploader = FailOnceCloudUploader(url: "https://cdn/retried.jpg")
let api = TransferMockCloudDriveAPI()
let manager = CloudTransferManager(
store: MemoryTransferStore(),
uploader: uploader,
api: api,
downloader: FakeDownloader(),
photoSaver: FakePhotoSaver(),
scenicIdProvider: { 10 }
)
let fileURL = try makeTempFile(name: "retry.jpg")
manager.addUploadTask(localFileURL: fileURL, fileName: "retry.jpg", fileType: 2, fileSize: 3, parentFolderId: 0)
try await waitUntil { manager.uploadTasks.first?.status == .failed }
let id = try XCTUnwrap(manager.uploadTasks.first?.id)
manager.resumeUpload(id: id)
try await waitUntil { manager.uploadTasks.isEmpty }
XCTAssertEqual(uploader.attempts, 2)
XCTAssertEqual(api.uploadCalls.first?.fileUrl, "https://cdn/retried.jpg")
}
func testUploadCancelRemovesTaskAndStagedFile() async throws {
let uploader = PausingThenSucceedCloudUploader(url: "unused")
let manager = CloudTransferManager(
store: MemoryTransferStore(),
uploader: uploader,
api: TransferMockCloudDriveAPI(),
downloader: FakeDownloader(),
photoSaver: FakePhotoSaver(),
scenicIdProvider: { 10 }
)
let fileURL = try makeTempFile(name: "cancel.jpg")
manager.addUploadTask(localFileURL: fileURL, fileName: "cancel.jpg", fileType: 2, fileSize: 3, parentFolderId: 0)
let id = try XCTUnwrap(manager.uploadTasks.first?.id)
manager.cancelUpload(id: id)
XCTAssertTrue(manager.uploadTasks.isEmpty)
XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
}
func testDownloadPauseThenResumeCompletes() async throws {
let downloader = PausingThenSucceedDownloader()
let saver = FakePhotoSaver()
let manager = CloudTransferManager(
store: MemoryTransferStore(),
uploader: FakeCloudUploader(url: "unused"),
api: TransferMockCloudDriveAPI(),
downloader: downloader,
photoSaver: saver,
scenicIdProvider: { 10 }
)
manager.addDownloadTask(fileURL: "https://cdn/resume.jpg", fileName: "resume.jpg", fileType: 2)
let id = try XCTUnwrap(manager.downloadTasks.first?.id)
try await waitUntil { await downloader.attemptCount() == 1 }
manager.pauseDownload(id: id)
try await waitUntil { manager.downloadTasks.first?.status == .paused }
manager.resumeDownload(id: id)
try await waitUntil { manager.downloadTasks.first?.status == .completed }
let attemptCount = await downloader.attemptCount()
XCTAssertEqual(attemptCount, 2)
XCTAssertEqual(saver.saved.count, 1)
XCTAssertEqual(manager.downloadTasks.first?.progress, 100)
}
private func makeTempFile(name: String) throws -> URL {
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + name)
try Data([1, 2, 3]).write(to: url)
@ -96,6 +194,20 @@ final class CloudTransferManagerTests: XCTestCase {
try await Task.sleep(nanoseconds: 20_000_000)
}
}
private func waitUntil(
timeout: TimeInterval = 2,
condition: @escaping () async -> Bool
) async throws {
let deadline = Date().addingTimeInterval(timeout)
while !(await condition()) {
if Date() > deadline {
XCTFail("Timed out waiting for condition")
return
}
try await Task.sleep(nanoseconds: 20_000_000)
}
}
}
private final class MemoryTransferStore: CloudTransferTaskStoring {
@ -130,6 +242,44 @@ private final class FakeCloudUploader: CloudDriveUploading {
}
}
@MainActor
private final class PausingThenSucceedCloudUploader: CloudDriveUploading {
let url: String
private(set) var attempts = 0
init(url: String) {
self.url = url
}
func uploadCloudDriveFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
attempts += 1
if attempts == 1 {
try await Task.sleep(nanoseconds: 5_000_000_000)
}
onProgress(100)
return url
}
}
@MainActor
private final class FailOnceCloudUploader: CloudDriveUploading {
let url: String
private(set) var attempts = 0
init(url: String) {
self.url = url
}
func uploadCloudDriveFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
attempts += 1
if attempts == 1 {
throw TestError.failed
}
onProgress(100)
return url
}
}
private final class FakeDownloader: CloudFileDownloading {
var downloadCalls: [(remoteURL: String, fileName: String)] = []
@ -143,6 +293,38 @@ private final class FakeDownloader: CloudFileDownloading {
}
}
private final class PausingThenSucceedDownloader: CloudFileDownloading {
private let counter = DownloadAttemptCounter()
func downloadFile(remoteURL: String, fileName: String, onProgress: @escaping @Sendable (Int) -> Void) async throws -> URL {
let attempt = await counter.next()
if attempt == 1 {
try await Task.sleep(nanoseconds: 5_000_000_000)
}
onProgress(100)
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + fileName)
try Data([1]).write(to: url)
return url
}
func attemptCount() async -> Int {
await counter.current()
}
}
private actor DownloadAttemptCounter {
private var value = 0
func next() -> Int {
value += 1
return value
}
func current() -> Int {
value
}
}
private final class FakePhotoSaver: CloudPhotoSaving {
var saved: [(fileURL: URL, fileType: Int)] = []