按账号与相册隔离相机下载目录,并同步更新传输管道与测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 15:55:54 +08:00
parent 660852ffad
commit 5f7ef24683
14 changed files with 382 additions and 163 deletions

View File

@ -11,16 +11,19 @@
| `CameraTransferPipeline` | 监听 `onNewAsset`、管理任务队列与上传 |
| `CameraTransferTask` | 单条传输任务及状态 |
| `CameraAssetUploadSink` | 上传协议,由业务模块实现 |
| `CameraDownloadStorage.Scope` | 账号 + 相册维度的本地文件隔离作用域 |
## 业务流程
1. 相机服务回调 `onNewAsset`
2. Pipeline 下载文件到本地并更新任务状态。
2. Pipeline 下载文件到当前 scope 的 `originals` 目录并更新任务状态。
3. 若照片是在开启自动上传时由 `onNewAsset` 新发现,加入自动上传资格队列并调用 `CameraAssetUploadSink.upload`
4. 上传成功后任务状态变为 `uploaded`
`syncExistingPhotos()` 仅同步相机历史照片到本地任务列表,不授予自动上传资格。拍后传输模式下已下载的 `downloaded` 任务,切换到边拍边传后仍保持待上传,只有业务层显式调用 `uploadAssets(withIDs:)``retryUpload(...)` 时才会上传。
本地文件路径按 `Documents/CameraDownloads/<accountKey>/<albumID>/originals/` 存放原图,缩略图由业务层生成到同 scope 的 `thumbnails/`。持久化只记录相对 Documents 的 scoped 路径,不保留未上线旧目录兼容。
## 与业务层关系
- 旅拍相册通过 `TravelAlbumMaterialUploader` 实现 SinkOSS 上传 + `upload-material` 登记。

View File

@ -16,6 +16,7 @@ final class CameraTransferPipeline {
private let cameraService: any CameraServiceProtocol
private var uploadSink: (any CameraAssetUploadSink)?
private var uploadEnabled = true
private var downloadScope = CameraDownloadStorage.Scope(accountKey: "unscoped", albumID: 0)
private var activeUploadAssetIDs: Set<String> = []
private var autoUploadEligibleAssetIDs: Set<String> = []
private var pendingDeferredNotifyTask: Task<Void, Never>?
@ -36,10 +37,17 @@ final class CameraTransferPipeline {
}
}
/// Sink
func configure(uploadSink: (any CameraAssetUploadSink)?, uploadEnabled: Bool) {
/// Sink
func configure(
uploadSink: (any CameraAssetUploadSink)?,
uploadEnabled: Bool,
downloadScope: CameraDownloadStorage.Scope? = nil
) {
self.uploadSink = uploadSink
self.uploadEnabled = uploadEnabled
if let downloadScope {
self.downloadScope = downloadScope
}
}
///
@ -243,10 +251,10 @@ final class CameraTransferPipeline {
let localURL = try await cameraService.downloadAsset(resolvedAsset)
let destination: URL
if CameraDownloadStorage.isInDownloadsDirectory(localURL) {
if CameraDownloadStorage.isInOriginalsDirectory(localURL, scope: downloadScope) {
destination = localURL
} else {
destination = CameraDownloadStorage.uniqueLocalURL(for: resolvedAsset.filename)
destination = CameraDownloadStorage.uniqueLocalURL(for: resolvedAsset.filename, scope: downloadScope)
if FileManager.default.fileExists(atPath: destination.path) {
try FileManager.default.removeItem(at: destination)
}

View File

@ -76,7 +76,7 @@ struct CameraTransferTask: Identifiable, Equatable, Codable {
return formatter
}()
/// URL
/// URL Documents scoped
var localURL: URL? {
guard let localPath, !localPath.isEmpty else { return nil }
return CameraDownloadStorage.resolveLocalURL(from: localPath)