diff --git a/warpgate-prd-v3.md b/warpgate-prd-v3.md index 1b49ea6..53bb307 100644 --- a/warpgate-prd-v3.md +++ b/warpgate-prd-v3.md @@ -258,7 +258,8 @@ flowchart LR - `warpgate cache-clean` — 清理缓存 - `warpgate warmup` — 手动预热 - `warpgate bwlimit` — 动态调整带宽限制 -- `warpgate conflicts` — 查看和处理冲突文件 +- `warpgate conflicts` — 列出所有冲突文件(跨目录聚合),显示原文件、冲突副本、时间 +- `warpgate conflicts resolve --keep=local|remote|both` — 解决冲突 - `warpgate ingest` — 手动触发 SD 卡导入 - `warpgate verify` — 双卡校验 - `warpgate log` — 查看实时日志 @@ -284,10 +285,33 @@ flowchart LR - 写回队列在恢复连接后自动续传 - 连接超时参数可配置 -#### 4.16 写冲突通知 -- 冲突发生时通知用户(CLI 提示 / 日志 / 可选 Webhook) -- 冲突文件清单管理 -- 手动解决冲突工具 +#### 4.16 写冲突处理 + +**冲突副本命名**:`{name} (Warpgate Conflict {YYYY-MM-DD HH-mm}).{ext}` + +冲突副本保留在**原目录**(而非移到独立 conflict/ 目录),让 Lightroom/Finder 等应用可以直接看到并打开。保留原始扩展名,确保应用兼容性。这与 Dropbox/iCloud 的冲突处理方式一致,用户已有认知习惯。 + +**各冲突场景的处理**: + +| 场景 | 主文件 | 冲突副本 | 说明 | +|------|--------|----------|------| +| 远程更新胜出(remote > local) | 拉取远程版本作为 `IMG_001.cr3` | 本地版本重命名为 `IMG_001 (Warpgate Conflict ...).cr3` | 两个版本都在原目录,用户可直接对比 | +| 本地更新胜出(local > remote) | 保持本地版本并回写 | 不产生冲突副本 | 正常回写 | +| 远程已删 + 本地有脏数据 | 本地文件**原地保留不改名** | 无 | metadata 中标记为 orphan-conflict;通知用户"NAS 上已删除,本地编辑是唯一副本,请决定是否重新上传" | + +**冲突副本的生命周期**: + +- 冲突副本在 FUSE 挂载点内,rclone 自动视为新脏文件 → Write-back Controller 将其同步到 NAS(冲突副本也有远程备份,与 Dropbox 行为一致) +- 用户在 Lightroom/Finder 中直接看到两个版本,自行决定保留哪个 +- 删除不需要的版本即可,rclone 自动同步删除到 NAS + +**管理工具**: + +- `warpgate conflicts` — 列出所有冲突文件(跨目录聚合),显示:原文件、冲突副本路径、冲突时间、哪个版本胜出 +- `warpgate conflicts resolve --keep=local|remote|both` — 批量解决冲突:保留本地/远程/两者 +- 超过 `CONFLICT_RETAIN_DAYS` 天的冲突副本,由定时清理任务自动删除(清理前记录日志) + +**通知**:冲突发生时通知用户(CLI 提示 / 日志 / 可选 Webhook),包含冲突文件路径和建议操作 ### P2(后续迭代) @@ -430,7 +454,7 @@ flowchart TD CmpMtime -->|"remote > cache
远程更新"| Conflict1["远程胜
拉远程新版本
本地存冲突副本"] CmpMtime -->|"mtime 相等
内容可能不同"| LocalWins["本地胜
(写入者优先)"] - Deleted -->|"⚠️ 不回写"| Respect["尊重远程删除
脏文件移到 conflict/
通知用户"] + Deleted -->|"⚠️ 不回写"| Respect["尊重远程删除
本地文件原地保留
标记 orphan-conflict
通知用户"] ``` ### 5.4b SD 卡导入文件的回写决策(补充) @@ -483,7 +507,7 @@ flowchart TD WB --> Stat["SFTP stat 查远程:
remote_mtime = Day3"] Stat --> Conflict["remote(Day3) != origin(Day0)
→ 远程被改过!"] Conflict --> Compare["cache(Day1) vs remote(Day3)
Day3 > Day1 → 远程胜"] - Compare --> Actions["本地脏版本 → conflict/
拉远程新版本覆盖缓存
更新 origin=Day3, clean
通知用户"] + Compare --> Actions["本地版本重命名为
(Warpgate Conflict ...)
拉远程新版本
更新 origin=Day3, clean
通知用户"] Actions --> Result["✅ 不丢数据,两版本都保留"] ``` @@ -496,8 +520,8 @@ flowchart TD D3 --> D5["Day 5: Cache 开机,准备回写"] D5 --> Stat["Write-back Controller 查远程:
photo.cr3 不存在"] Stat --> Conflict["远程已删 + 本地有脏数据 → 冲突"] - Conflict --> Actions["❌ 不回写(尊重远程删除)
脏版本 → conflict/
通知用户"] - Actions --> Result["✅ 不会复活已删文件
用户可从 conflict/ 恢复"] + Conflict --> Actions["❌ 不回写(尊重远程删除)
本地文件原地保留
标记 orphan-conflict
通知用户"] + Actions --> Result["✅ 不会复活已删文件
用户可决定是否重新上传"] ``` #### 场景 4:NAS 删了文件,Cache 上是 clean 的 @@ -581,8 +605,8 @@ flowchart TD │ │ └── IMG_0002.cr3 # 或通过 FUSE 挂载点写入的 SD 卡导入文件 │ └── ... ├── metadata.db # SQLite 元数据库(WAL 模式,详见 5.7.7) -├── conflict/ # 冲突文件暂存目录 -│ └── IMG_0001.cr3.local-20260216-143022 +├── conflict/ # 冲突副本自动清理暂存(CONFLICT_RETAIN_DAYS 到期后移入) +│ └── (仅用于到期自动清理前的归档,正常冲突副本在原目录) ├── ingest_staging/ # SD 卡导入暂存目录(导入状态机使用,详见 5.9) │ └── / # 每次导入会话独立目录 └── timemachine/ # Time Machine 备份目录(独立于 rclone VFS,详见 4.10) @@ -629,8 +653,11 @@ SD 卡导入新文件 → INSERT (state=dirty, source=ingest, origin_mtime 回写失败 → UPDATE (writeback_retry += 1) 缓存被 LRU 淘汰 → DELETE(仅 state=clean 可被淘汰) 检测到远程删除(clean)→ DELETE -检测到冲突 → UPDATE (state=conflict) -冲突处理完成 → DELETE 或 UPDATE (state=clean) +检测到冲突(远程胜) → 原文件 UPDATE (拉远程版本, state=clean) + 冲突副本 INSERT (重命名后的本地版本, state=dirty, source=conflict) + 冲突副本通过 rclone 自动回写到 NAS +检测到冲突(远程已删)→ UPDATE (state=conflict, 标记 orphan-conflict, 原地保留不改名) +冲突处理完成 → DELETE(用户删除不需要的版本)或 UPDATE (state=clean) ``` **表 2:dir_snapshots — 目录级轮询快照** @@ -1042,7 +1069,7 @@ flowchart TD Decision -->|"正常回写"| Upload["上传 + 更新 origin_mtime
state → clean"] Decision -->|"冲突"| ConflictKeep["保留双版本 + 通知用户"] - Decision -->|"远程已删"| NoWrite["不回写 + 移到 conflict/"] + Decision -->|"远程已删"| NoWrite["不回写
原地保留,标记 orphan-conflict"] Upload -->|"上传失败"| Retry["重试(指数退避)
10s, 20s, 40s... 最多 10 次"] Retry -->|"最终失败"| Keep["保留本地,state=dirty
记录日志"] @@ -1141,13 +1168,14 @@ flowchart TD | 参数 | 说明 | 默认值 | 建议值 | |------|------|--------|--------| -| `CONFLICT_DIR` | 冲突文件存放目录 | `{CACHE_DIR}/conflict` | - | | `CONFLICT_STRATEGY` | 冲突策略 | `mtime_wins` | `mtime_wins` | | `CONFLICT_NOTIFY` | 冲突通知方式 | `log` | `log` / `webhook` | | `CONFLICT_RETAIN_DAYS` | 冲突副本保留天数 | `30` | - | -| `CONFLICT_CLEANUP_SCHEDULE` | 冲突目录自动清理时间 | `04:00` | 与 FULL_SYNC_SCHEDULE 错开 | +| `CONFLICT_CLEANUP_SCHEDULE` | 冲突副本自动清理时间 | `04:00` | 与 FULL_SYNC_SCHEDULE 错开 | -**冲突目录清理进程**:每天在 `CONFLICT_CLEANUP_SCHEDULE` 时间自动扫描 `CONFLICT_DIR`,删除超过 `CONFLICT_RETAIN_DAYS` 天的冲突副本。清理前记录日志。 +**冲突副本命名**:`{name} (Warpgate Conflict {YYYY-MM-DD HH-mm}).{ext}`,保留在原目录中。 + +**清理进程**:每天在 `CONFLICT_CLEANUP_SCHEDULE` 扫描所有匹配 `(Warpgate Conflict ...)` 命名模式的文件,超过 `CONFLICT_RETAIN_DAYS` 天的自动删除(本地 + 已同步到 NAS 的副本一并删除)。清理前记录日志。 ### 多协议配置