warpgate/warpgate-prd-v3.md
grabbit 3caddc6370 Simplify architecture: read-only cache + one-way SD upload
Replace OverlayFS + sync daemon with two independent subsystems:
- Read-only cache: rclone --read-only + Samba read only = yes
- SD Uploader: staging → SFTP direct upload to NAS (temp file + rename)

Remove: OverlayFS, sync daemon, three-timestamp model, write-back,
conflict detection, dirty file tracking. Net -299 lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:35:43 +08:00

1208 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Warpgate — Make your NAS feel local — 产品方案与需求描述v3
---
## 一、产品定位
**一句话描述**:摄影师的随身存储中枢——外拍插卡自动归档,路上缓存加速访问,全程高速组网,云端容灾兜底。
**核心价值**
1. **远程访问加速**:用户在外通过 Tailscale 等组网工具访问家中 NAS 时受限于公网带宽SMB 协议体验极差(卡顿、超时、缩略图加载慢)。本产品在客户端侧部署一层 SSD 只读缓存对上层应用Lightroom、Finder、Explorer 等)完全透明,首次访问按需拉取并缓存到本地 SSD后续访问直接命中缓存。
2. **外拍现场备份归档**:摄影师外拍结束插入 SD 卡,一键备份到本地 SSD后台自动异步归档回家中 NAS。把「现场备份」和「远程归档」打通成一条自动流水线市面上没有产品做到这一点。
3. **数据安全兜底**:可选的云端异地容灾,为用户的数据提供多层保护。
**产品形态**
- **软件方案**MVP配置文件 + 一键部署脚本,部署在任意 Linux 主机上Docker 镜像在 v2.5 提供)
- **硬件一体机**(目标形态):定制盒子,内置 SSD + 电池 + SD 卡槽 + WiFi开箱即用
**市场机会**
- Gnarbox曾最受欢迎的摄影师外拍备份设备已停产市场空缺明显
- UnifyDrive UT2$599硬件形态相似但软件体验差、电池仅 1 小时
- ClouZen TAINER 功能单一,只能备份不能联网同步
- **没有产品把「现场备份」和「远程归档回 NAS」打通成自动流水线**
---
## 二、目标用户
| 用户画像 | 典型场景 | 痛点 |
|----------|----------|------|
| 摄影师 | 出差/外拍酒店回看、粗修当天照片Lightroom | RAW 文件 25-60MBSMB 远程逐张打开极慢,预览生成卡死 |
| 视频创作者 | 远程剪辑,浏览素材库、拖拽代理文件 | 视频文件更大,顺序播放需持续带宽 |
| 设计师 | 出差访问公司 NAS 上的 PSD/AI 源文件 | 大文件 + 多图层,打开一个文件几分钟 |
| 远程办公族 | 日常办公文档、项目资料存 NAS | 小文件多SMB 目录浏览延迟高,体验卡顿 |
| NAS 重度用户 | 旅行途中访问个人数据 | 没有公网 IP 或带宽不足,现有方案都不理想 |
**核心用户优先级**摄影师Lightroom + 外拍备份)> 远程办公 > 视频创作者
---
## 三、系统架构
### 3.1 整体架构
```mermaid
graph LR
subgraph clients ["客户端设备"]
LR["Lightroom<br/>macOS"]
Linux["Linux<br/>客户端"]
iPad["iPad<br/>移动端"]
end
subgraph proxy ["Linux Proxy局域网·高速"]
Samba["Samba Server<br/>(read only)"]
NFS_S["NFS Server<br/>(read only)"]
WebDAV_S["WebDAV Server<br/>(read only)"]
VFS["rclone VFS mount<br/>(只读缓存)"]
Uploader["SD Uploader<br/>(单向上传到 NAS)"]
SSD["SSD 缓存 + 元数据 DB"]
Samba --> VFS
NFS_S --> VFS
WebDAV_S --> VFS
VFS --> SSD
end
subgraph remote ["远程(公网 / Tailscale·低速"]
TS["Tailscale /<br/>WireGuard"]
NAS["任意品牌 NAS<br/>(SFTP)"]
TS --- NAS
end
LR -- "SMB" --> Samba
Linux -- "NFS" --> NFS_S
iPad -- "WebDAV" --> WebDAV_S
VFS -- "SFTP (读取)" --> TS
Uploader -- "SFTP (上传)" --> TS
```
### 3.2 协议选择说明
| 段 | 协议 | 原因 |
|----|------|------|
| 客户端 → Proxy | SMB | 对 Lightroom/macOS/Windows 原生兼容,应用无感 |
| 客户端 → Proxy | NFS | Linux 客户端性能更好,内核级支持 |
| 客户端 → Proxy | WebDAV | 移动端 App 支持广泛 |
| Proxy → NAS | SFTP | 高延迟链路下比 SMB 稳定得多,任意品牌 NAS 均支持,无需额外套件 |
### 3.3 多协议对外服务(设计讨论)
**问题**:客户端 → Warpgate 之间只支持 SMB 是否足够?
**讨论**不同客户端设备对协议的偏好不同。macOS + Lightroom 最适合 SMB但 Linux 客户端用 NFS 性能更好(内核级支持,且 Linux 侧还能再叠一层 FS-CacheiPad/移动端 App 则普遍支持 WebDAV。
**设计决策**:所有对外协议服务共享同一个 rclone FUSE 只读挂载点。缓存层只有一份,不会因为多协议而重复缓存。所有协议均为只读模式,客户端无法通过共享写入文件。
```mermaid
graph LR
SMB["SMB Server<br/>(read only)"] --> Mount["/mnt/nas-photos<br/>(rclone 只读 FUSE 挂载)"]
NFS["NFS Server<br/>(read only)"] --> Mount
WebDAV["WebDAV<br/>(read only)"] --> Mount
Mount --> SSD["SSD 缓存<br/>(rclone 内部管理)"]
```
**注意事项**:所有共享均为只读。文件写入通过 SD 卡导入功能单向上传到 NAS独立于缓存系统
---
## 四、核心功能
### P0MVP 必须)
#### 4.1 透明多协议只读代理
- 对外暴露标准 SMB 只读共享,客户端连接方式与直连 NAS 完全一致
- 支持 SMB2/SMB3 协议(`read only = yes`
- 同时支持 NFS 只读导出Linux 客户端)和 WebDAV 只读服务(移动端)
- 支持 macOSFinder/Lightroom、WindowsExplorer、Linux、移动端客户端
- 文件读取、目录浏览、文件属性(时间戳/权限)均正常工作
- 所有协议共享同一个 rclone 只读缓存层,不重复存储
- **只读设计**Lightroom 等应用读取 RAW 文件,编辑参数存在本地 catalog 中,不需要写入 NAS 文件所在目录
#### 4.2 读缓存Read-through Cache
- 文件首次被访问时,从远程 NAS 拉取并存入本地 SSD 缓存
- 后续访问同一文件直接从本地 SSD 返回
- 支持分块读取chunked read大文件不需要整个下载完才能开始读取
- 支持预读read-ahead顺序读取场景下提前拉取后续数据
- 目录列表缓存:目录结构缓存一段时间,避免频繁远程查询
#### 4.3 缓存一致性
- 缓存为**只读**,不存在本地修改,因此不存在写冲突问题
- 后台轮询检测远程变更,自动标记缓存失效(详见第六章)
- 远程文件被修改 → 下次访问时 rclone 自动拉取最新版本
- 远程文件被删除 → rclone 缓存刷新后文件自动消失
- 无脏文件、无回写、无冲突检测 —— 架构极简
#### 4.5 远程变更检测
- 基于 SFTP 的分层轮询机制,自动发现远程数据变化(详见第六章)
- 不依赖任何 NAS 品牌特有 API纯 SFTP 协议实现
- 每日全量校对兜底
#### 4.6 缓存空间管理
- 设置缓存总大小上限
- 超出上限时按 LRU最久未访问策略自动淘汰
- 可设置缓存盘最低保留空间,防止磁盘写满
- 可设置缓存最大保留时间
- 缓存全部为只读cleanrclone 可自由淘汰任何缓存文件
#### 4.7 一键部署
- 提供完整的配置文件 + 部署脚本
- 自动安装依赖rclone、Samba、NFS、fuse
- 自动配置 systemd 服务,开机自启
- 自动配置日志轮转
- 缓存盘文件系统建议 btrfs/ZFSCoW + journal 保护一致性)
### P1重要但非 MVP
#### 4.8 SD 卡导入 + 自动归档Ingest
摄影师外拍结束,把 SD 卡插入盒子,一键备份到本地 SSD 暂存,后台独立进程通过 SFTP 直接上传到家中 NAS。**与只读缓存完全独立**——导入是单向的「SD → NAS」管道不经过缓存系统。
- 检测到 SD/CFexpress 卡插入后,支持物理按钮一键触发导入或自动导入模式
- **导入前空间检查**:估算 SD 卡总数据量,检查 SSD 暂存区可用空间(扣除 CACHE_MIN_FREE空间不足时拒绝导入并通知用户
- 复制文件到 SSD 暂存目录(`ingest_staging/`),导入时计算文件 checksumSHA-256确保数据完整
- 导入记录写入 import_history 持久表(详见 5.6.4
- **SD Uploader 独立进程**通过 SFTP 将暂存文件直接上传到 NAS 目标路径,无网络时排队等待
- 上传成功后清理暂存文件 + 可选 `vfs/forget` 通知 rclone 刷新(使新文件在只读缓存中可见)
- 支持重复文件检测(基于文件名+大小+checksum查询 import_history 持久表(详见 5.6.4),避免重复导入
- 导入完成通过 LED 指示灯 / 蜂鸣器提示
- 支持按日期模板自动组织目标路径(如 `/{year}/{month}/{date}/`),日期来源为 EXIF 拍摄日期,非 EXIF 文件回退到文件 mtime详见 INGEST_DATE_SOURCE 配置)
- **导入中断保护**:导入过程维护状态机(`detecting → copying → checksumming → uploading → complete`),中断的文件(未完成 checksum 校验)将被清理(详见 5.8
#### 4.9 双卡备份 + 校验
摄影师同时插入两张 SD 卡(或一张 SD 卡 + 一个 USB 移动硬盘),盒子自动做双向 checksum 比对,确保两份备份完全一致。
- 并行读取两个存储设备的文件列表
- 逐文件比对:文件名 + 大小一致后计算 SHA-256 比对
- 不一致的文件标记为异常并通知用户
- 仅一侧存在的文件单独提示
- 校验完成输出报告LED 状态 + CLI 可查详情)
- 校验通过的文件自动进入 4.8 导入归档流程
#### 4.10 配网模式 + Captive Portal 代理Setup AP
盒子是 Headless 设备(无屏幕),而绝大多数酒店/机场 WiFi 需要网页认证Captive Portal。没有这个功能旅途场景直接不可用。
**核心流程**
```
① 盒子开机,检测到未配置 WiFi 或无法联网
→ 自动进入「配网模式」WiFi 模块启动临时 APSSID: Warpgate-Setup
② 用户手机连接 Warpgate-Setup 热点
→ 自动弹出配网页面(或手动访问 http://192.168.4.1
③ 配网页面显示周围可用 WiFi 列表,用户选择酒店 WiFi
④ 盒子连接酒店 WiFiWiFi 模块切换为 AP+STA 并发模式)
→ 检测到 Captive Portal 重定向
⑤ 盒子将 Captive Portal 认证页面代理到配网页面
→ 用户在手机上完成酒店 WiFi 的网页认证(输入房号/姓名等)
⑥ 认证通过盒子获得互联网访问Tailscale 自动连接
→ 配网模式关闭,临时 AP 关闭(或保持为管理入口)
```
**硬件要求**WiFi 模块必须支持 **AP+STA 并发模式**(同时作为热点和连接外部 WiFi这是配网模式的前提。大多数支持 AP 模式的 WiFi 芯片均支持此功能。
**Fallback 方案**(不需要额外开发,文档中列出即可):
- **USB 网络共享**:手机 USB 连接盒子共享手机网络tethering绕过酒店 WiFi
- **手机热点**:盒子直连手机 4G/5G 热点
- **有线以太网**:部分酒店有网口,直插通常无需认证
- **MAC 克隆**`warpgate clone-mac <MAC>` 克隆已认证设备的 MAC 地址(高级用户)
#### 4.11 缓存预热Warm-up
- 命令行手动预热指定目录
- 按时间范围预热(如"最近 7 天新增的文件"
- 定时预热任务(如每天凌晨自动拉取最新数据)
- 预热进度显示
#### 4.12 管理工具CLI
- `warpgate status` — 查看服务状态、缓存使用量、SD 上传队列、当前带宽限速
- `warpgate cache-list` — 列出缓存中的文件
- `warpgate cache-clean` — 清理缓存
- `warpgate warmup` — 手动预热
- `warpgate bwlimit` — 动态调整带宽限制
- `warpgate ingest` — 手动触发 SD 卡导入
- `warpgate verify` — 双卡校验
- `warpgate log` — 查看实时日志
- `warpgate speed-test` — 链路速度测试
- `warpgate setup-wifi` — 手动进入配网模式
- `warpgate clone-mac <MAC>` — 克隆指定设备的 MAC 地址
#### 4.13 带宽管理
- 支持上传SD 导入上传)/下载(缓存拉取)分别限速
- 运行时动态调整限速(不重启服务)
- SD 上传带宽不影响读取体验
- **自适应限速Adaptive Throttle**:基于吞吐量观测自动降速,避免 SD 上传占满链路
- 监控上传吞吐量的滑动窗口(如最近 30s 平均值),当吞吐持续下降超过阈值时判定链路拥塞
- 拥塞时自动降速,释放带宽给读取和其他流量
- 每隔一段时间小幅探测提速
- throttle 状态通过 `warpgate status` 实时可见
- 用户可通过 `BW_ADAPTIVE` 配置关闭自适应限速
- 自适应限速**仅控制 SD 上传**,不影响缓存读取拉取
#### 4.14 连接容错
- Tailscale 断连时自动重试
- 已缓存的文件在离线时仍可正常读取
- SD 上传队列在恢复连接后自动续传
- 连接超时参数可配置
### P2后续迭代
#### 4.16 WiFi AP 现场共享
盒子内置 WiFi 模块开启持久热点,现场团队设备连上即可通过 SMB/WebDAV 访问缓存目录。与 4.10 配网模式的区别:配网 AP 是临时的(完成配网后关闭),本功能是持久的团队共享热点。
- 支持 AP 模式,复用已有的 SMB/WebDAV 多协议服务
- AP 网络与 Tailscale/WAN 网络隔离(安全考虑)
- AP 模式下仍可同时通过有线/4G 连接 Tailscale 做后台 SD 上传
- **硬件要求**需要两个独立网络接口——WiFi 模块用于 AP 热点,有线/USB 4G 网卡用于 WAN/Tailscale 连接。一体机硬件设计需预留双网卡
- 典型场景:婚礼现场摄影师导入 SD 卡后,助理 iPad 连上 WiFi 即可浏览选片
#### 4.17 Web 管理界面
- 缓存状态仪表盘大小、命中率、SD 上传队列、带宽趋势图)
- 缓存文件浏览器(查看/手动清除/手动预热)
- 配置修改界面(参数调整无需编辑配置文件)
- SD 导入历史浏览
- 实时日志查看器
#### 4.18 NAS 侧 Agent 推送(可选增强)
- 在 NAS 上运行轻量 AgentDocker 容器),监听文件变化主动推送给 Proxy
- 实现秒级远程变更感知(替代分钟级轮询)
- 不依赖品牌 API基于 inotify 通用方案
#### 4.19 多 NAS / 多目录支持
- 同时连接多个远程 NAS如家里 + 工作室)
- 每个 NAS 独立共享名,独立缓存策略
- 每个共享可配置不同的缓存大小和保留时间
#### 4.20 智能缓存策略
- 根据文件类型自动调整策略:
- `.lrcat` / `.xmp`Lightroom catalog/sidecar→ 高缓存优先级
- `.CR3` / `.ARW` / `.NEF`RAW 照片)→ 大块预读,长缓存保留
- `.mp4` / `.mov`(视频)→ 顺序预读优化
- `.psd` / `.ai`(设计文件)→ 完整缓存,避免分块导致的兼容问题
- 基于访问频率自动调整缓存优先级(热数据不被淘汰)
#### 4.21 Docker 镜像
- 一行命令启动:`docker run -v /mnt/ssd:/cache warpgate`
- docker-compose 配置
- 支持环境变量或挂载配置文件
#### 4.22 通知机制
- SD 上传失败告警Webhook / Telegram / 邮件)
- 缓存空间不足告警
- NAS 离线告警
- SD 导入完成 / 上传完成通知(可选)
---
## 五、数据一致性模型
### 5.1 设计目标
采用**只读缓存 + 单向上传**模型,两个功能完全独立。具体承诺:
1. 远程 NAS 上的变更会在可控时间内被 Proxy 感知并更新本地缓存(后台轮询)
2. 已缓存的文件在离线时仍可正常访问
3. SD 卡导入的文件最终会上传到远程 NASSD Uploader 异步上传 + 断电恢复靠 SSD 持久化暂存文件)
4. **无写冲突**:缓存只读,不存在本地修改与远程变更冲突的可能
### 5.2 设计讨论与决策过程
#### 问题 A为什么选择只读缓存而非读写缓存
**讨论**:最初设计了 OverlayFS + sync daemon 的读写缓存方案(本地写入 → 异步回写 NAS但面临大量边界场景
- 写冲突检测(本地改了文件 + NAS 也被别人改了)
- mtime 精度不一致导致误判
- 断电后脏文件恢复
- TOCTOU 竞态条件
- 远程删除后脏文件复活
**最终决策**:只读缓存 + 单向上传。理由:
- 摄影师核心工作流是「浏览/粗选 RAW」只读+ 「SD 卡导入」(单向),不需要通过缓存写回 NAS
- Lightroom 编辑参数存在本地 catalog.lrcat不修改 RAW 文件本身
- 只读缓存消灭了所有写冲突、脏文件、回写等复杂性
- SD 导入是纯新文件的单向上传,不存在冲突
- 写回能力可以作为 v2.0 按需加入
> **MVP 部署建议****建议**缓存盘使用 btrfs 或 ZFS 文件系统CoW + checksum 保护断电一致性)。部署脚本应检测并警告非 CoW 文件系统。
#### 问题 BSD 导入上传中断怎么办?
**讨论**SD 卡导入后文件暂存在 SSD 上等待上传到 NAS。上传过程中可能断电、断网、进程崩溃。
**决策**
- 暂存文件持久化在 SSD 上,重启后 SD Uploader 扫描暂存目录自动继续上传
- 上传到 NAS 时使用临时文件名(`.warpgate-tmp-<hash>`),完成后 rename 为最终文件名,避免 NAS 上出现不完整文件
- 上传成功后清理暂存文件,可选 `vfs/forget` 刷新缓存使新文件可见
### 5.3 一致性模型(只读缓存 + 单向上传)
架构分为两个完全独立的子系统:
```
只读缓存路径:应用 → Samba/NFS只读 → rclone FUSE 挂载 → SSD 缓存 ← SFTP 从 NAS 按需拉取
SD 上传路径: SD 卡 → ingest_staging/SSD 暂存) → SD Uploader → SFTP 直传 NAS
```
**只读缓存**
- rclone 以 `--read-only --vfs-cache-mode full` 挂载Samba/NFS 以只读模式共享
- 所有缓存文件都是 clean 的与远程一致rclone 自由管理 LRU 淘汰
- 后台轮询检测远程变更 → `vfs/forget` 通知 rclone 刷新 → 下次访问拿到最新版本
- 无脏文件、无回写、无冲突 —— **零数据一致性风险**
**SD 单向上传**
- SD Uploader 是独立进程,与缓存系统无交互
-`ingest_staging/` 扫描待上传文件 → SFTP 上传到 NAS临时文件名 + rename
- 上传失败自动重试(指数退避),断电重启后扫描暂存目录继续上传
- 上传成功后清理暂存文件,可选 `vfs/forget` 刷新缓存使新文件可见
- **无冲突**SD 导入的都是新文件NAS 上不存在同名文件时直接创建已存在时跳过import_history 去重已在导入阶段处理)
**缓存淘汰**rclone 由 `--vfs-cache-max-size``--vfs-cache-max-age` 控制 LRU 淘汰,所有缓存文件均可安全淘汰。
### 5.4 读取时的缓存验证
缓存为只读,所有文件都是 clean 的,验证逻辑非常简单:
| 远程状态 | 缓存行为 |
|---|---|
| 远程没变 | ✅ 直接用缓存 |
| 远程被修改 | 🔄 轮询发现变化 → `vfs/forget` → rclone 下次访问时拉新版本 |
| 远程被删除 | 🗑️ 轮询发现变化 → `vfs/forget` → rclone 刷新后文件消失 |
**注意**:读取热路径上**不查远程**(不产生网络请求),直接返回缓存。远程变更由后台轮询线程定期发现(见第六章)。
### 5.5 关键场景走查
#### 场景 1Cache 关机几天NAS 上文件被修改
```mermaid
flowchart TD
D1["Day 1: Cache 缓存 photo.cr3 (clean)"] --> Off["Day 1: Cache 关机"]
Off --> D3["Day 3: NAS 上 photo.cr3 被修改"]
D3 --> D5["Day 5: Cache 开机<br/>rclone 启动,轮询线程开始"]
D5 --> Detect["轮询发现目录 mtime 变化"]
Detect --> Forget["vfs/forget → rclone 缓存失效"]
Forget --> Access["用户访问时 rclone 自动拉新版本"]
Access --> Result["✅ 用户读到 NAS 最新版本"]
```
#### 场景 2NAS 删了文件
```mermaid
flowchart TD
D1["Day 1: Cache 缓存 photo.cr3 (clean)"]
D1 --> D3["Day 3: NAS 上删除 photo.cr3"]
D3 --> Poll["轮询检测到远程目录变化"]
Poll --> Forget["vfs/forget → rclone 刷新"]
Forget --> Gone["rclone 缓存自动消失"]
Gone --> Result["✅ 本地缓存与远程保持一致"]
```
#### 场景 3SD 卡导入 + 上传到 NAS最常见 happy path
```mermaid
flowchart TD
Insert["摄影师插入 SD 卡"] --> Detect["检测到卡,按按钮触发导入"]
Detect --> Copy["复制到 ingest_staging/<br/>计算 SHA-256"]
Copy --> Dedup["查 import_history → 无重复"]
Dedup --> Record["记录 import_history"]
Record --> Upload["SD Uploader 通过 SFTP<br/>上传到 NAS临时文件 + rename"]
Upload --> Clean["清理暂存文件<br/>vfs/forget 刷新缓存"]
Clean --> Result["✅ 文件安全到达 NAS<br/>缓存中可见新文件"]
```
#### 场景 4SD 导入后离线,后续上传
```mermaid
flowchart TD
Insert["外拍现场,无网络"] --> Import["SD 卡导入到暂存目录<br/>文件安全存在 SSD 上"]
Import --> Queue["SD Uploader 发现无网络<br/>上传排队等待"]
Queue --> Hotel["回到酒店,网络恢复"]
Hotel --> Resume["SD Uploader 自动续传<br/>逐文件上传到 NAS"]
Resume --> Result["✅ 无需人工干预"]
```
### 5.6 元数据持久化metadata DB
metadata.db 用于 **SD 卡导入去重**import_history和**远程变更轮询**dir_snapshots。使用 SQLite 存储。只读缓存不需要额外状态数据库——rclone 内部管理所有缓存状态。
#### 5.6.1 设计讨论metadata DB 应该包含什么?
**问题**metadata DB 需要存 NAS 侧的所有文件元数据吗?
**分析**NAS 上可能有几十万甚至上百万文件,但用户一次出差实际访问的可能只有几百到几千个。如果全量存储 NAS 文件元数据,会带来几个问题:
- 初始化成本高——首次使用需要递归扫描整个 NAS 目录树
- 存储浪费——大量从未访问的文件元数据没有价值
- 同步负担重——需要持续维护全量数据的一致性
**结论**metadata DB **只管辅助功能**(轮询和导入去重),不存文件级缓存状态。缓存状态完全由 rclone 内部管理。
**怎么检测远程文件被删了?**
rclone 自动处理,不需要数据库:
假设目录 `/2026/02/` 下有 200 张照片,用户只缓存了 3 张。
场景 ANAS 上 IMG_0050.cr3 被删了(从未缓存过)
→ 跟缓存无关,不处理 ✅
场景 BNAS 上 IMG_0001.cr3 被删了(缓存过)
→ 轮询发现目录变化 → `vfs/forget` → rclone 刷新 → 缓存文件消失 ✅
场景 CNAS 上 IMG_0050.cr3 被修改了(缓存过)
→ 轮询发现目录变化 → `vfs/forget` → rclone 刷新 → 下次读取拿到新版本 ✅
**最终决策**
| 表 | 用途 | 理由 |
|---|---|---|
| dir_snapshots | 轮询目录 mtime 快检 | 记住上次轮询时目录的 mtime判断是否变化 |
| import_history | SD 导入去重 | 持久记录历史导入,防止重复导入 |
#### 5.6.2 缓存目录结构
```
/mnt/ssd/warpgate/
├── rclone-cache/ # rclone VFS 内部缓存目录rclone 自动管理)
│ └── vfs/
│ └── photos/
│ └── 2026/02/
│ └── IMG_0001.cr3 # 远程拉取缓存
├── metadata.db # SQLite 元数据库WAL 模式,详见 5.6.7
└── ingest_staging/ # SD 卡导入暂存目录(导入状态机使用,详见 5.8
└── <session-id>/ # 每次导入会话独立目录
```
**挂载关系**
```bash
# rclone 只读 FUSE 挂载(= Samba/NFS 共享的根目录)
rclone mount remote:photos /mnt/nas-photos \
--read-only \
--vfs-cache-mode full \
--vfs-cache-max-size ${CACHE_MAX_SIZE} \
--vfs-cache-max-age ${CACHE_MAX_AGE} \
--cache-dir /mnt/ssd/warpgate/rclone-cache \
--rc
# Samba 配置(只读)
# [nas-photos]
# path = /mnt/nas-photos
# read only = yes
```
**重要**
- `rclone-cache/` 由 rclone 内部管理,外部进程**不得直接操作**
- `ingest_staging/` 由 SD 导入进程管理SD Uploader 从此目录读取待上传文件
- Samba/NFS 直接服务于 rclone FUSE 挂载点 `/mnt/nas-photos`
#### 5.6.3 表结构定义
**表 1dir_snapshots — 目录级轮询快照**
用于分层轮询的"目录 mtime 快检"。只记录被缓存文件所在的目录,不是 NAS 全量目录。
```sql
CREATE TABLE dir_snapshots (
dir_path TEXT PRIMARY KEY, -- 目录相对路径,如 /2026/02/
remote_mtime INTEGER, -- 上次轮询时远程目录的 mtime
-- NULL 表示 SD 卡导入创建的目录,远程尚不存在
last_polled INTEGER NOT NULL, -- 上次轮询时间
last_accessed INTEGER NOT NULL -- 目录最后被访问时间(决定热/温/冷分级)
);
```
生命周期:
```
某目录下的文件首次被缓存 → INSERT
SD 卡导入上传到新目录 → INSERTSD Uploader 上传完成后remote_mtime 为上传后 stat 的值)
轮询时目录 mtime 没变 → UPDATE last_polled
轮询时目录 mtime 变了 → UPDATE remote_mtime触发 vfs/forget 刷新缓存
目录下已无缓存文件 → DELETE可选
```
**表 1bdir_file_list — 目录文件列表快照(后续增强)**
MVP 阶段不需要。后续加入后,记录被关心目录下的全部远程文件,支持精确变更类型识别和智能预热。
```sql
CREATE TABLE dir_file_list (
dir_path TEXT NOT NULL, -- 所属目录
file_name TEXT NOT NULL, -- 文件名
remote_mtime INTEGER NOT NULL, -- 上次已知的远程 mtime
remote_size INTEGER NOT NULL, -- 上次已知的远程文件大小
snapshot_time INTEGER NOT NULL, -- 快照时间
PRIMARY KEY (dir_path, file_name)
);
CREATE INDEX idx_dir ON dir_file_list(dir_path);
```
#### 5.6.4 导入历史表(重复检测用)
**问题**:重复文件检测不能只看当前暂存目录内容——已上传到 NAS 并从暂存区清理的文件,再次导入同一张 SD 卡时无法检测到重复。
**解决方案**import_history 持久表,记录所有历史导入记录,永不删除。
```sql
CREATE TABLE import_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
original_path TEXT NOT NULL, -- SD 卡上的原始路径
target_path TEXT NOT NULL, -- 上传到 NAS 的目标路径
file_size INTEGER NOT NULL, -- 文件大小
checksum TEXT NOT NULL, -- SHA-256 校验和
source_device TEXT, -- SD 卡设备标识(如序列号)
imported_at INTEGER NOT NULL, -- 导入时间
uploaded_at INTEGER, -- 上传到 NAS 的时间NULL=未上传)
state TEXT NOT NULL -- imported / uploaded / failed
);
CREATE INDEX idx_checksum ON import_history(checksum);
CREATE INDEX idx_original_path ON import_history(original_path, file_size);
```
重复检测流程:
```
导入文件前 → 计算 checksum
→ 查 import_history WHERE checksum = ? AND file_size = ?
→ 命中 → 跳过该文件,标记为"已导入过"
→ 未命中 → 执行导入
```
#### 5.6.5 MVP 表结构关系与数据规模
```mermaid
erDiagram
dir_snapshots {
TEXT dir_path PK "如 /2026/02/"
INTEGER remote_mtime
INTEGER last_polled
INTEGER last_accessed
}
import_history {
INTEGER id PK "独立于缓存生命周期"
TEXT original_path
TEXT target_path
TEXT checksum "持续累积,永久保留"
TEXT state "imported/uploaded/failed"
}
```
两张核心表。只读缓存不需要额外的文件状态数据库——rclone 内部管理所有缓存状态。后续可选增加 dir_file_list精确变更检测
数据规模估算:
| 表 | 记录范围 | 预估行数 | 存储开销 |
|---|---|---|---|
| dir_snapshots | 有缓存文件的目录 | ~50 | ~5 KB |
| import_history | 所有历史导入记录(持续累积) | ~5,000 | ~500 KB |
| **总计** | | | **< 1 MB** |
#### 5.6.6 远程变更检测
后台轮询线程通过 dir_snapshots 表实现分层轮询//冷目录区分访问频率)。发现目录 mtime 变化后
1. `vfs/forget` 通知 rclone 清除该目录的缓存元数据
2. rclone 下次访问该目录时自动从远程重新读取删除的文件消失修改的文件 mtime 更新新增的文件出现
**不需要逐文件对比数据库记录**——rclone `vfs/forget` + 自动重新拉取机制覆盖所有远程变更场景
#### 5.6.7 SQLite 并发访问策略
metadata.db 会被多个进程/线程并发访问轮询线程读写 dir_snapshots)、SD 卡导入进程读写 import_history)、SD Uploader更新 import_history 状态)、CLI 管理工具只读查询)。
**要求**metadata.db 必须存放在**本地文件系统**SSD ext4/btrfs/ZFS**严禁放在 rclone FUSE 挂载目录中**。SQLite WAL 依赖 POSIX 文件锁和共享内存`-shm` 文件FUSE/网络文件系统无法正确支持这些语义会导致数据库损坏
metadata.db 必须以 **WALWrite-Ahead Logging模式**运行
```sql
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000; -- 锁等待超时 5 秒
```
WAL 模式的优势
- 读操作不阻塞写操作写操作不阻塞读操作
- 多个进程可以同时读取只有写入互斥
- 适合"多读少写"的缓存元数据场景
所有访问 metadata.db 的进程必须使用同一个 WAL 模式配置部署脚本在初始化数据库时自动设置
### 5.7 SD 卡导入与上传
SD 卡导入和上传是**独立于缓存系统的单向管道**SD SSD 暂存 SFTP 上传 NAS
**实现要点**
1. SD 卡导入进程将文件复制到 `ingest_staging/<session-id>/` 暂存目录并计算 checksum
2. import_history 去重校验通过后记录导入历史
3. SD Uploader 独立进程扫描 `ingest_staging/` 中已校验完成的文件
4. 通过 SFTP 上传到 NAS 目标路径临时文件名 `.warpgate-tmp-<hash>` rename 为最终文件名
5. 上传成功后更新 import_history 状态 + 清理暂存文件 + 可选 `vfs/forget` 刷新缓存
**性能优势**导入直接写入 SSD 本地文件系统 FUSE 开销导入速度取决于 SD 卡读取速度和 SSD 写入速度通常可达 100MB/s+。上传带宽独立于缓存读取带宽
### 5.8 SD 卡导入状态机(中断保护)
**问题**SD 卡导入过程中可能发生中断卡被拔出电池耗尽进程崩溃)。部分复制的文件不应进入上传队列
**解决方案**导入过程维护严格的状态机只有完整校验通过的文件才进入上传队列
```mermaid
stateDiagram-v2
[*] --> detecting: SD 卡插入
detecting --> copying: 列出文件清单
copying --> checksumming: 复制到 ingest_staging/
checksumming --> registered: SHA-256 校验通过 +<br/>写入 import_history
registered --> uploading: SD Uploader 开始上传
uploading --> complete: SFTP 上传成功 +<br/>清理暂存文件
complete --> [*]
note right of detecting: 中断 → 清理 staging 临时文件
note right of copying: 中断 → 清理 staging 临时文件
note right of checksumming: 中断 → 清理不完整文件
note right of registered: 中断 → 文件在暂存区 + DB<br/>重启后 SD Uploader 继续上传
note right of uploading: 中断 → NAS 上临时文件<br/>重启后重新上传覆盖
```
**中断保护**每个导入会话有唯一 `session_id``detecting`/`copying`/`checksumming` 阶段中断时`ingest_staging/` 中的临时文件在下次启动时自动清理`registered`/`uploading` 阶段中断时文件安全在暂存目录中SD Uploader 重启后扫描暂存目录继续上传NAS 上的临时文件名`.warpgate-tmp-<hash>`确保不完整上传不会被当作正常文件
**实现要点**
- 暂存目录 `ingest_staging/<session-id>/` 按导入会话隔离
- SD Uploader 启动时扫描 `ingest_staging/`清理未完成校验的会话续传已校验的文件
- 导入进度持久化到 `ingest_sessions` 表或简单的 JSON 文件支持断点续传
---
## 六、远程变更检测机制
### 6.1 设计约束
**不依赖任何 NAS 品牌特有 API**产品需要支持群晖QNAP威联通TrueNAS 等任意品牌 NAS因此只能基于标准 SFTP 协议实现
### 6.2 SFTP 协议的能力边界(设计讨论)
**问题**远程 NAS 上数据更新了Cache 怎么知道能否实时感知
**讨论**SFTP 协议本身**没有任何通知/推送/订阅机制**。它是无状态的文件传输协议不支持 inotifywebhookfilesystem watch 等概念每次想知道远程状态必须主动发请求查询
**考虑过的方案**
| 方案 | 原理 | 实时性 | 品牌依赖 | 取舍 |
|------|------|--------|----------|------|
| SFTP 全量轮询 | 递归 ls 对比 mtime | 分钟级 | | 文件多时开销大 |
| SFTP 分层轮询 | 先查目录 mtime变了再查文件 | 分钟级 | | 高效推荐 |
| 群晖 FileStation API | DSM Web API | 秒级 | 仅群晖 | 不通用 |
| NAS Agent 推送 | inotifywait HTTP 通知 | 秒级 | | 需要 NAS 装软件 |
**最终决策**
- **P0MVP**SFTP 分层轮询零额外依赖
- **P2后续**可选的 NAS Agent 推送作为增强项给需要秒级同步的用户
### 6.3 分层轮询策略
核心优化思路**SFTP 目录本身也有 mtime**。当目录下有文件新增/修改/删除时目录的 mtime 会更新因此可以先查目录 mtime一个 stat 请求没变就跳过整个目录下所有文件的检查大幅减少远程请求量
```mermaid
flowchart TD
Start["轮询触发"] --> L3{"第三层:热度分级<br/>决定轮询间隔"}
L3 -->|"热目录7天内访问<br/>每 30s"| L1
L3 -->|"温目录7-30天<br/>每 5m"| L1
L3 -->|"冷目录30天+<br/>每 1h"| L1
L1["第一层:目录 mtime 快检<br/>SFTP stat 查目录 mtime<br/>(每目录 1 个请求)"]
L1 -->|"mtime 没变"| Skip["跳过该目录 ✅<br/>所有文件一定没变"]
L1 -->|"mtime 变了"| L2
L2["第二层:文件级 mtime 对比<br/>SFTP ls -l 该目录"]
L2 --> Changed["发现变化 → 标记缓存失效<br/>(等访问时再拉)"]
L2 --> Del["发现删除 → 清理本地缓存"]
L2 --> New["远程新增 → 不处理"]
L4["第四层:每日全量校对(兜底)<br/>凌晨全量递归对比<br/>捕捉遗漏 + 清理过期条目"]
```
轮询伪代码MVP
```
# watched_directories = SELECT dir_path FROM dir_snapshots
# 按热度分级决定轮询间隔(热 30s / 温 5m / 冷 1h
for dir in watched_directories:
if now - dir.last_polled < poll_interval_for(dir):
continue # 还没到该目录的轮询时间
# 第一层:目录 mtime 快检1 个 SFTP stat 请求)
dir_mtime = sftp_stat(dir.dir_path).mtime
if dir_mtime == dir.remote_mtime:
UPDATE dir_snapshots SET last_polled = now WHERE dir_path = dir.dir_path
continue # 目录没变,跳过 ✅
# 第二层:目录变了 → 通知 rclone 刷新缓存
rclone_rc("vfs/forget", dir=dir.dir_path) # 清除 rclone 目录缓存
# rclone 下次访问时自动从远程重新读取:
# - 远程删除的文件 → rclone 缓存消失 → 文件不再可见
# - 远程修改的文件 → rclone 下次访问时拉新版本
# - 远程新增的文件 → rclone 下次访问目录时可见
log_info(f"Remote directory changed: {dir.dir_path}, rclone cache invalidated")
# 更新目录快照
UPDATE dir_snapshots SET remote_mtime = dir_mtime, last_polled = now
WHERE dir_path = dir.dir_path
```
### 6.4 后续增强NAS 侧 Agent 推送P2
对于需要秒级同步的用户可选在 NAS 上部署轻量 AgentDocker 容器通过 inotify 监听变化并推送
```mermaid
flowchart LR
subgraph NAS ["群晖 NAS"]
Agent["inotifywait 监听文件变化<br/>(Docker 容器)"]
end
subgraph Proxy ["Linux Proxy"]
HTTP["HTTP 接收端<br/>触发缓存刷新"]
end
Agent -- "轻量 HTTP POST<br/>(via Tailscale)" --> HTTP
```
此方案作为分层轮询的增强不是替代即使 Agent 不可用轮询机制仍然工作
---
## 七、缓存行为详细描述
### 7.1 读取流程
```mermaid
flowchart TD
App["应用请求读取文件"] --> Samba["① Samba/NFS只读"]
Samba --> FUSE["② rclone FUSE 挂载"]
FUSE -->|"缓存命中"| Return["直接返回缓存<br/>SSD 速度)"]
FUSE -->|"缓存未命中"| Remote["③ rclone 从远程 NAS 拉取"]
FUSE -->|"缓存已过期"| Refresh["④ rclone 检查远程 mtime<br/>变了则重新拉取"]
Remote --> Chunk["⑤ 按 chunk 分块下载"]
Chunk --> Cache["⑥ 缓存到 SSD"]
Cache --> ReturnData["⑦ 返回数据给应用"]
Refresh --> ReturnData
```
**设计要点**
- 读取热路径上**不查远程**不产生网络请求直接返回缓存保证响应速度
- 目录缓存通过 rclone `--dir-cache-time` 控制过期刷新
- 后台轮询线程通过 `vfs/forget` 主动刷新已知变化的目录
### 7.2 SD 导入上传流程
```mermaid
flowchart TD
SD["① SD 卡插入 → 导入到暂存目录<br/>SHA-256 校验 + import_history 去重)"]
SD --> Staging["② 文件安全存在 ingest_staging/<br/>SSD 本地目录)"]
Staging --> Scan["③ SD Uploader 扫描暂存目录"]
Scan --> Upload["④ SFTP 上传到 NAS<br/>(临时文件名 → rename"]
Upload -->|"上传成功"| Clean["⑤ 更新 import_history<br/>清理暂存文件<br/>vfs/forget 刷新缓存"]
Upload -->|"上传失败"| Retry["⑤ 自动重试<br/>(指数退避)"]
Upload -->|"网络不可用"| Queue["⑤ 排队等待<br/>网络恢复后自动续传"]
Retry --> Upload
Queue --> Upload
```
### 7.3 缓存淘汰策略
缓存全部由 rclone 管理所有文件都是 clean 与远程一致可安全淘汰
- `--vfs-cache-max-size`缓存总大小上限超出时按 LRU 淘汰
- `--vfs-cache-max-age`缓存最大保留时间
- rclone 自行管理淘汰无需外部干预
- 无脏文件 不存在不能淘汰的场景 缓存空间管理极简
#### 7.3.1 SD 导入暂存空间保护
**问题**SD 卡导入的暂存文件占用 SSD 空间等待上传到 NAS离线时暂存文件会累积
**保护措施**
1. **导入前空间预检**估算 SD 卡总数据量检查 SSD 可用空间扣除缓存 + `CACHE_MIN_FREE`空间不足时拒绝导入并通知用户LED 红灯 + CLI 提示建议先连网上传或清理
2. **配置验证**部署时`INGEST_MAX_IMPORT_SIZE + CACHE_MIN_FREE < SSD 总容量 - CACHE_MAX_SIZE`不满足时部署脚本报警告
### 7.4 离线行为
| 场景 | 行为 |
|------|------|
| 远程不可达读取已缓存文件 | 正常返回无影响 |
| 远程不可达读取未缓存文件 | 超时报错可配置超时时间 |
| 远程不可达SD 卡导入 | 正常导入到暂存目录上传排队等待网络恢复 |
| 远程不可达后台轮询 | 静默跳过不报错下次重试 |
| 恢复连接后 | SD Uploader 自动续传 + 立即触发一轮轮询 |
---
## 八、配置参数清单
### 连接配置
| 参数 | 说明 | 默认值 | 建议值 |
|------|------|--------|--------|
| `NAS_HOST` | 远程 NAS Tailscale IP | - | `100.x.x.x` |
| `NAS_USER` | SFTP 用户名 | - | - |
| `NAS_PASS` / `NAS_KEY_FILE` | 认证信息 | - | 建议密钥 |
| `NAS_REMOTE_PATH` | NAS 上的目标路径 | - | `/volume1/photos` |
| `SFTP_PORT` | SFTP 端口 | `22` | `22` |
| `SFTP_CONNECTIONS` | SFTP 连接复用数 | `8` | `4-16` |
### 缓存配置
| 参数 | 说明 | 默认值 | 建议值 |
|------|------|--------|--------|
| `CACHE_DIR` | 缓存存储路径 | - | SSD 路径建议 btrfs/ZFS |
| `CACHE_MAX_SIZE` | 缓存大小上限 | `200G` | SSD 容量的 70-80% |
| `CACHE_MAX_AGE` | 缓存最大保留时间 | `720h`30天 | 按使用习惯 |
| `CACHE_MIN_FREE` | 缓存盘最低可用空间 | `10G` | `10-20G` |
### 读取优化
| 参数 | 说明 | 默认值 | 场景建议 |
|------|------|--------|----------|
| `READ_CHUNK_SIZE` | 分块读取大小 | `256M` | RAW 照片: `256M`视频: `512M`文档: `64M` |
| `READ_CHUNK_LIMIT` | chunk 自动增长上限 | `1G` | - |
| `READ_AHEAD` | 预读缓冲区 | `512M` | 视频场景可加到 `1G` |
| `BUFFER_SIZE` | 内存缓冲区 | `256M` | - |
### 带宽配置
| 参数 | 说明 | 默认值 | 场景建议 |
|------|------|--------|----------|
| `UPLOAD_TRANSFERS` | SD 上传并发线程 | `4` | 带宽小就设 `2` |
| `BW_LIMIT_UP` | SD 上传限速上限 | `0`不限 | 酒店 WiFi 建议 `10-20M` |
| `BW_LIMIT_DOWN` | 缓存拉取下载限速 | `0`不限 | 一般不限 |
| `BW_ADAPTIVE` | 自适应上传限速开关 | `yes` | `yes`=根据吞吐量自动降速`no`=纯手动 |
### 目录缓存与轮询
| 参数 | 说明 | 默认值 | 场景建议 |
|------|------|--------|----------|
| `DIR_CACHE_TIME` | 目录列表缓存时间 | `1h` | 个人: `1-2h`协作: `5-15m` |
| `POLL_HOT_INTERVAL` | 热目录轮询间隔7天内有访问 | `30s` | - |
| `POLL_WARM_INTERVAL` | 温目录轮询间隔7-30天内访问 | `5m` | - |
| `POLL_COLD_INTERVAL` | 冷目录轮询间隔30天+未访问 | `1h` | - |
| `FULL_SYNC_SCHEDULE` | 每日全量校对时间 | `03:00` | 凌晨低峰期 |
### 多协议配置
| 参数 | 说明 | 默认值 | 建议值 |
|------|------|--------|--------|
| `ENABLE_SMB` | 启用 SMB 共享 | `yes` | `yes` |
| `ENABLE_NFS` | 启用 NFS 导出 | `no` | Linux 客户端时开启 |
| `ENABLE_WEBDAV` | 启用 WebDAV 服务 | `no` | 有移动端需求时开启 |
| `NFS_ALLOWED_NETWORK` | NFS 允许访问的网段 | `192.168.0.0/24` | 按实际局域网设置 |
| `WEBDAV_PORT` | WebDAV 监听端口 | `8080` | - |
### SD 卡导入配置
| 参数 | 说明 | 默认值 | 建议值 |
|------|------|--------|--------|
| `INGEST_MAX_IMPORT_SIZE` | 单次导入预留空间上限 | `256G` | 按最大 SD 卡容量设置 |
| `INGEST_AUTO` | 插卡后自动导入 | `no` | 需按按钮确认 |
| `INGEST_TARGET_PATH` | 导入到 NAS 的目标路径模板 | `/{year}/{month}/{date}/` | 按个人习惯变量从 INGEST_DATE_SOURCE 确定 |
| `INGEST_DATE_SOURCE` | 路径模板中日期变量的来源 | `exif` | `exif`=EXIF拍摄日期回退到mtime`mtime`=文件修改时间`import`=导入时间 |
| `INGEST_DUPLICATE_CHECK` | 重复文件检测基于文件名+大小+checksum | `yes` | `yes` |
| `INGEST_DELETE_AFTER` | 导入+校验完成后是否删除卡上数据 | `no` | `no`安全起见 |
| `INGEST_IO_CLASS` | 导入时的 I/O 调度优先级 | `best-effort:4` | 使用 ionice 设置避免导入阻塞缓存读取 |
### 配网模式配置
| 参数 | 说明 | 默认值 | 建议值 |
|------|------|--------|--------|
| `SETUP_AP_SSID` | 配网热点名称 | `Warpgate-Setup` | - |
| `SETUP_AP_PASSWORD` | 配网热点密码=开放) | | 首次配网建议开放降低门槛 |
| `SETUP_AP_AUTO` | 无网络时自动进入配网模式 | `yes` | `yes` |
| `SETUP_AP_TIMEOUT` | 配网完成后临时 AP 保持时间 | `5m` | 认证成功后自动关闭 |
| `SETUP_PORTAL_LISTEN` | 配网 Web 服务监听地址 | `192.168.4.1:80` | - |
### WiFi AP 配置
| 参数 | 说明 | 默认值 | 建议值 |
|------|------|--------|--------|
| `AP_ENABLED` | 启用 WiFi 热点 | `no` | 现场共享时开启 |
| `AP_SSID` | 热点名称 | `Warpgate` | - |
| `AP_PASSWORD` | 热点密码 | 随机生成 | 首次配置时设定 |
| `AP_ISOLATION` | AP 网络与 WAN 隔离 | `yes` | `yes` |
| `AP_MAX_CLIENTS` | 最大连接数 | `8` | - |
---
## 九、场景预设(模板)
为降低用户配置门槛提供开箱即用的预设模板
### 摄影师模式
```
重点优化大文件读取性能、RAW 浏览流畅
- CACHE_MAX_SIZE=500G
- READ_CHUNK_SIZE=256M
- READ_AHEAD=512M
- DIR_CACHE_TIME=2h ← 目录结构不常变
- POLL_HOT_INTERVAL=30s
- UPLOAD_TRANSFERS=4 ← SD 上传并发
- ENABLE_SMB=yes
- ENABLE_NFS=no
- ENABLE_WEBDAV=no
```
### 视频剪辑模式
```
重点优化:顺序读取性能、大文件预读
- CACHE_MAX_SIZE=1T
- READ_CHUNK_SIZE=512M
- READ_AHEAD=1G ← 大预读保证播放流畅
- DIR_CACHE_TIME=1h
- POLL_HOT_INTERVAL=1m
- UPLOAD_TRANSFERS=2 ← 减少 SD 上传并发,保带宽给播放
- ENABLE_SMB=yes
- ENABLE_NFS=no
- ENABLE_WEBDAV=no
```
### 文档办公模式
```
重点优化:小文件快速响应、频繁感知远程变更
- CACHE_MAX_SIZE=50G
- READ_CHUNK_SIZE=64M
- READ_AHEAD=128M
- DIR_CACHE_TIME=30m ← 协作场景需要较快看到新文件
- POLL_HOT_INTERVAL=15s ← 更频繁感知远程变更
- UPLOAD_TRANSFERS=4
- ENABLE_SMB=yes
- ENABLE_NFS=no
- ENABLE_WEBDAV=yes ← 移动端也能访问
```
---
## 十、部署要求
### 硬件要求(通用 Linux 主机部署)
| 组件 | 最低配置 | 推荐配置 |
|------|----------|----------|
| CPU | ARMv8 / x86_64 任意 | N100 或同级 |
| 内存 | 1 GB | 2-4 GB |
| 缓存盘 | 任意 SSD | NVMe SSD |
| 缓存容量 | 32 GB | 常用数据量的 30%+ |
| 网口 | 100M | 千兆2.5G 更好 |
| 断电保护 | - | 内置电池或外接 UPS |
### 硬件要求(一体机目标形态)
通用要求之外一体机额外需要
| 组件 | 说明 |
|------|------|
| SD 卡槽 | SD / microSD覆盖大多数相机 |
| CFexpress 可选 | CFexpress Type-B高端相机用户 |
| USB-A/C | 至少 2 用于外接读卡器XQD 或移动硬盘 |
| WiFi 模块 | 支持 **AP+STA 并发模式**配网必须建议 WiFi 6 |
| 物理按钮 | 触发 SD 卡导入 / 确认操作 |
| LED 状态指示 | 导入进度 / 完成 / 错误 / 上传状态 |
| 内置电池 | 支持断电保护 + 便携使用 |
**缓存盘文件系统建议**btrfs ZFS利用 CoWCopy-on-Write journal 机制即使意外断电也能保证文件系统级别的一致性
```bash
# btrfs 格式化示例
mkfs.btrfs /dev/ssd_partition
mount -o compress=zstd /dev/ssd_partition /mnt/ssd/warpgate
```
### 软件要求
| 组件 | 版本 |
|------|------|
| OS | Ubuntu 22.04+ / Debian 12+ / 任意 Linux |
| rclone | 1.65+关键参数`--read-only --vfs-cache-mode full --vfs-cache-max-size {CACHE_MAX_SIZE} --cache-dir {CACHE_DIR}/rclone-cache --rc`)。`--read-only` 确保只读挂载`--rc` 启用 RC API供轮询线程调用 `vfs/forget` 刷新目录缓存 |
| Samba | 4.x |
| NFS server | nfs-kernel-server如启用 NFS |
| FUSE | 3.x |
| SQLite | 3.x元数据存储 |
| Tailscale / ZeroTier | 已配置并可连通 NAS |
### NAS 侧要求
| 项目 | 要求 |
|------|------|
| SFTP 服务 | 开启群晖控制面板 文件服务 FTP 勾选 SFTP |
| 用户权限 | SFTP 用户对目标目录有读写权限 |
| Tailscale | 已安装并登录同一网络 |
| 品牌 | **无限制**任何支持 SFTP NAS 均可群晖/QNAP/威联通/TrueNAS/DIY |
---
## 十一、风险与局限
| 风险 | 等级 | 说明 | 缓解措施 |
|------|------|------|----------|
| 首次访问慢 | 固有 | 未缓存文件必须走远程 | 预热功能分块下载优化 |
| 缓存一致性延迟 | | 远程变更在轮询间隔内不可见 | 分层轮询热目录 30s后续可选 Agent 推送 |
| Tailscale 断连 | | 远程不可达时新文件无法获取 | 已缓存文件仍可用SD 上传自动排队恢复后自动续传 |
| 轮询开销 | | 大量文件目录轮询消耗带宽 | 目录 mtime 快检跳过未变目录热度分级降低冷目录频率 |
| SD 卡导入数据损坏 | | 卡本身坏块导致导入不完整 | 导入时计算 SHA-256 校验和双卡校验比对 |
| SD 卡导入中断 | | 卡被拔出 / 电池耗尽 / 进程崩溃 | 导入状态机保护5.8未完成文件清理 |
| SD 上传中断 | | 上传过程中断网/断电 | 临时文件名保护 NAS 数据完整重启后自动续传 |
| 暂存空间耗尽 | | 离线时 SD 导入暂存累积 | 导入前空间预检7.3.1|
| 中转服务带宽成本 | | DERP 中继带宽随用户增长上升 | 大部分连接走 P2P 直连按流量分级限速/计费初期节点少按需扩容 |
| 云备份存储成本 | | 用户数据增长导致存储费用上升 | 接低价对象存储B2/R2按量计费传导成本增量备份减少传输量 |
| 酒店 Captive Portal | | Headless 设备无法完成网页认证旅途场景不可用 | 配网 AP + Portal 代理4.10fallbackUSB tethering / 手机热点 / MAC 克隆 |
---
## 十二、后续演进方向
| 阶段 | 内容 | 重点 |
|------|------|------|
| **v1.0 — MVP** | 配置文件 + 部署脚本 + CLI 管理 | SMB 只读共享 + rclone 只读缓存 + 分层轮询 + SD 导入 + SD Uploader 单向上传 NAS |
| **v1.5 — 硬件原型 + P1 功能** | SD 卡导入 + 双卡校验 + 配网模式 + Captive Portal 代理 + LED/按钮交互 + 缓存预热 + 带宽管理 + 连接容错 | 硬件原型开发P1 功能完善 |
| **v2.0 — 组网服务** | 内置 Headscale + 高速 DERP 节点 + WiFi AP 共享 | 开箱即连 + 现场团队协作 |
| **v2.5 — 容灾 + 附加** | 云端异地备份 + Docker 镜像 + 多协议NFS/WebDAV+ NAS Agent 推送 | 数据安全闭环 + 降低部署门槛 |
| **v3.0 — 硬件产品** | 定制硬件SSD + 电池 + SD + WiFi工业设计开箱即用 | 产品化面向非技术用户 |
---
## 十三、付费服务
### 13.1 Headscale + 高速 DERP 中转
**问题**Tailscale 官方 DERP 是共享资源跨运营商/跨国时带宽受限用户自建 DERP 需要有 VPS + 运维能力门槛高
**方案**
```mermaid
flowchart BT
subgraph infra ["运营基础设施"]
HS["Headscale<br/>控制面板<br/>(用户管理)"]
DERP1["DERP 节点<br/>国内 BGP<br/>(低延迟)"]
DERP2["DERP 节点<br/>香港/日本<br/>(跨境加速)"]
end
Box["盒子<br/>开箱即连<br/>(内置配置)"] --> HS
Box --> DERP1
NAS_C["NAS 端<br/>自动连接<br/>(安装脚本)"] --> DERP1
NAS_C --> DERP2
Travel["出差设备<br/>自动连接<br/>(通过盒子中继)"] --> DERP2
```
**用户体验**
1. 买盒子 开机 扫码绑定账号
2. NAS 端运行一行安装脚本加入用户的 Tailnet
3. 完成盒子带到任何地方自动通过最优 DERP 节点连回家
4. 无需了解 HeadscaleDERPWireGuard 等概念
**迁移路径**避免 vendor lock-in
- 盒子底层使用标准 WireGuard 协议用户可随时切换到自建 Headscale 或官方 Tailscale
- 提供配置导出工具一键导出 WireGuard 配置节点列表DERP 自定义映射
- 如用户不再使用我们的 Headscale 服务盒子仍可正常工作手动配置 Tailscale/WireGuard
**定价思路**
| 套餐 | 内容 | 参考价 |
|------|------|--------|
| 免费 | Headscale 控制面板 + 1 个基础 DERP 节点限速 | ¥0 |
| 基础版 | + 多节点智能选路 + 不限速 | ¥15-30/ |
| 专业版 | + 优先带宽 + 跨境加速节点 + SLA 保证 | ¥50-100/ |
**成本控制**
- DERP 中继只在无法打洞直连时使用大部分 Tailscale 连接是 P2P 直连中继流量占比通常不高
- 可按实际中继流量动态限速/计费避免被少数大流量用户拖垮
- 初期节点少1-2 按用户增长逐步扩容
### 13.2 异地容灾备份
**问题**NAS 在家里是单点故障——硬盘坏被盗火灾水灾都可能导致数据永久丢失
**方案**
```mermaid
flowchart TD
SSD["盒子 SSD 缓存<br/>(热数据子集)"]
SSD -->|"空闲时段<br/>加密增量备份"| Cloud
subgraph Cloud ["云存储服务"]
UX["用户视角: 一键开通,按月付费"]
Backend["后端: B2 / R2 / MinIO<br/>(~$5/TB/月)"]
Encrypt["数据加密: 用户本地生成密钥<br/>运营方看不到明文"]
KeyBackup["密钥备份: 首次设置强制引导<br/>(密钥文件 / 助记词 / 密码管理器)"]
Sync["增量同步: 只传变化部分"]
Restore["恢复: 新盒子 → 输入密钥 → 自动拉取"]
end
```
**与现有架构的关系**
- 复用 SD Uploader 的思路本地文件 异步上传到云端
- 不同点备份目标是云端对象存储而非 NAS且可以备份 NAS 全量数据不限于缓存过的文件
- 可以做成两级
- **热备份**盒子 SSD 上缓存过的文件自动备份几乎零额外成本
- **全量备份**通过以下任一路径从 NAS 全量增量备份到云端
- **方案 1推荐**盒子在家局域网时自动执行高速零公网带宽消耗
- **方案 2**盒子在外通过 Tailscale 远程执行速度受限于公网带宽但保证便携场景也能跑备份
- **方案 3远期** NAS 侧部署独立的备份 agentDocker 容器NAS 直接备份到云端不依赖盒子在线
- 便携性说明盒子的核心场景是带出去用全量备份不要求盒子必须在家——方案 2 保证在外时也能慢速备份方案 3 完全解耦盒子和全量备份
**定价思路**
| 套餐 | 内容 | 参考价 |
|------|------|--------|
| 免费 | 热数据备份仅缓存过的文件上限 50GB | ¥0 |
| 基础版 | 全量备份500GB | ¥15/ |
| 专业版 | 全量备份5TB | ¥50/ |
| 按量 | 超出部分 | ¥10/TB/ |
---
## 十四、明确不做的方向
| 方向 | 原因 |
|------|------|
| 缩略图/预览生成Web 相册 | 破坏透明代理核心定位产品本质是协议透传不是数据加工 |
| AI 选片 | 非核心远期可选 |
| 程序员场景Git 缓存Docker 镜像等 | 痛点不够强已有成熟方案Git 天然分布式Codespaces |
| 公网文件分享链接 | 法律风险 + 需求不明确 |
| 多设备 SaaS 管理面板 | 需求不明确过早 |
| Docker 开放运行环境 | 产品定位发散这里指的是允许用户在盒子上运行任意 Docker 容器而非 4.21 "将本产品打包为 Docker 镜像部署" |
---
## 十五、术语表
| 术语 | 说明 |
|------|------|
| **Warpgate / 盒子** | 本产品——部署在用户身边的 SSD 缓存代理设备 |
| **NAS** | 用户家中的网络存储设备Network Attached Storage |
| **VFS** | rclone 的虚拟文件系统层Virtual File System将远程存储挂载为本地目录 |
| **FUSE** | 用户空间文件系统Filesystem in UserspaceLinux 内核机制允许 rclone 在不修改内核的情况下提供文件系统挂载 |
| **FUSE 挂载点** | rclone 通过 FUSE 暴露的只读本地目录 `/mnt/nas-photos`Samba/NFS 直接服务于此目录 |
| **SD Uploader** | 独立上传进程 SD 卡导入的暂存文件通过 SFTP 单向上传到 NAS |
| **ingest_staging** | SD 卡导入暂存目录文件在此完成校验后等待上传到 NAS |
| **LRU** | 最近最少使用Least Recently Used缓存淘汰算法 |
| **SFTP** | SSH 文件传输协议本产品与 NAS 通信的主要协议 |
| **Tailscale** | 基于 WireGuard 的组网工具用于建立盒子与 NAS 之间的安全隧道 |
| **Headscale** | Tailscale 控制面板的开源自建实现 |
| **DERP** | Tailscale 的中继服务器Designated Encrypted Relay for Packets在无法直连时中转流量 |
| **Ingest / 导入** | SD 卡文件导入到缓存并自动归档到 NAS 的过程 |
| **WAL** | SQLite 的写前日志模式Write-Ahead Logging允许并发读写 |
| **Captive Portal** | 强制门户认证酒店/机场等 WiFi 连接后重定向到网页要求登录的机制 |
| **AP+STA 并发** | WiFi 模块同时作为热点AP和连接外部网络STA的工作模式 |