feat(app): P0 batch 2+2.5 — local upload dedup with batch dialog, album switch check, file size guard

P0-1 层 1 本地查重:
  - checkDuplicates:同账号+同相册+state=completed+filename+bytes 匹配
  - EnqueueResult 返回 enqueued+duplicates+skipped
  - capture_page + album_detail_page:批量单次弹窗(非逐张)三选一
    [跳过重复上传非重复] [全部上传] [取消]

P0-8 相册上传开关预检:album_detail_page 入队前检查 uploadEnabled,
  关闭时提示「该相册已关闭上传」+ 不入队

P1-8 文件大小预检:preparer 限单文件 ≤100MB(磁盘满由 on Object 兜底)

P0-7 更正:上传并发限制已有(kMaxParallelUploads=2),v2 分析误报

397/397 全绿、analyze 0

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-28 11:06:40 +08:00
parent 9edcb3f2b1
commit 638dd9b45d
9 changed files with 302 additions and 26 deletions
+20 -14
View File
@@ -55,8 +55,8 @@ class UploadPreparer {
Random? random,
this.maxBytes = kMaxUploadBytes,
this.onEnqueued,
}) : _baseDirProvider = baseDirProvider ?? getApplicationSupportDirectory,
_random = random ?? Random.secure();
}) : _baseDirProvider = baseDirProvider ?? getApplicationSupportDirectory,
_random = random ?? Random.secure();
final TaskStore _store;
final Future<Directory> Function() _baseDirProvider;
@@ -88,13 +88,15 @@ class UploadPreparer {
required String sourcePath,
String? filename,
}) {
return _serialize(() => _prepare(
accountId: accountId,
albumId: albumId,
categoryId: categoryId,
sourcePath: sourcePath,
filename: filename,
));
return _serialize(
() => _prepare(
accountId: accountId,
albumId: albumId,
categoryId: categoryId,
sourcePath: sourcePath,
filename: filename,
),
);
}
/// 重试准备失败的任务(preparationFailed → preparing → 复制 → queued)。
@@ -106,11 +108,10 @@ class UploadPreparer {
required String taskId,
required String sourcePath,
}) {
return _serialize(() => _retry(
accountId: accountId,
taskId: taskId,
sourcePath: sourcePath,
));
return _serialize(
() =>
_retry(accountId: accountId, taskId: taskId, sourcePath: sourcePath),
);
}
/// 串行化:所有准备操作首尾相接(错误吞掉以不断链,错误经返回值上抛)。
@@ -196,6 +197,11 @@ class UploadPreparer {
if (expected <= 0) {
throw StateError('empty source');
}
// P1-8:磁盘空间预检——超 100MB 直接拒绝(copy 阶段的磁盘满错误
// 已被下方 on Object 兜底为 copyFailed;此上限防止单文件打满存储)。
if (expected > 100 * 1024 * 1024) {
throw StateError('source too large');
}
await target.parent.create(recursive: true);
await source.copy(task.sourceFilePath);
final copied = await target.length();