Redesign conflict UX: in-place copies like Dropbox/iCloud (4.16)

Instead of moving conflict files to a separate conflict/ directory,
keep them in the original directory with naming convention:
  {name} (Warpgate Conflict {YYYY-MM-DD HH-mm}).{ext}

Benefits:
- Lightroom/Finder see both versions side by side
- Preserved extension ensures app compatibility
- Matches Dropbox/iCloud behavior users already know
- Conflict copies auto-sync to NAS via rclone (backed up)

Remote-deleted + local-dirty: file stays in place (no rename),
marked as orphan-conflict, user decides whether to re-upload.

Updated: decision matrix diagrams, scenario walkthroughs,
cache_files lifecycle, CLI commands, config section, directory
structure description.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
grabbit 2026-02-16 21:44:51 +08:00
parent 823d20606a
commit d40997312b

View File

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