warpgate/warpgate-prd-v3.md
grabbit 7fd1934be5 Clarify metadata.db must be on local filesystem, not FUSE mount
SQLite WAL depends on POSIX file locks and shared memory (-shm),
which FUSE/network filesystems cannot support correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 21:38:08 +08:00

71 KiB
Raw Blame History

Warpgate — Make your NAS feel local — 产品方案与需求描述v3


一、产品定位

一句话描述:摄影师的随身存储中枢——外拍插卡自动归档,路上缓存加速访问,全程高速组网,云端容灾兜底。

核心价值

  1. 远程访问加速:用户在外通过 Tailscale 等组网工具访问家中 NAS 时受限于公网带宽SMB 协议体验极差(卡顿、超时、缩略图加载慢)。本产品在客户端侧部署一层 SSD 缓存对上层应用Lightroom、Finder、Explorer 等)完全透明,首次访问按需拉取并缓存,后续访问直接命中本地 SSD写入先落本地再异步回写远程。
  2. 外拍现场备份归档:摄影师外拍结束插入 SD 卡,一键备份到本地 SSD后台自动异步归档回家中 NAS。把「现场备份」和「远程归档」打通成一条自动流水线市面上没有产品做到这一点。
  3. 数据安全兜底:支持 macOS Time Machine 备份目标 + 可选的云端异地容灾,为用户的数据提供多层保护。

产品形态

  • 软件方案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 整体架构

graph LR
    subgraph clients ["客户端设备"]
        LR["Lightroom<br/>macOS"]
        Linux["Linux<br/>客户端"]
        iPad["iPad<br/>移动端"]
    end

    subgraph proxy ["Linux Proxy局域网·高速"]
        Samba["Samba Server"]
        NFS_S["NFS Server"]
        WebDAV_S["WebDAV Server"]
        VFS["rclone VFS mount<br/>+ Write-back Controller"]
        SSD["SSD 缓存 + 元数据 DB<br/>(btrfs/ZFS)"]
        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

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 挂载点。缓存层只有一份,不会因为多协议而重复缓存。

graph LR
    SMB["SMB Server"] --> FUSE["/mnt/nas-photos<br/>(rclone FUSE mount)"]
    NFS["NFS Server"] --> FUSE
    WebDAV --> FUSE
    FUSE --> SSD["SSD 缓存<br/>唯一缓存层"]

注意事项SMB 和 NFS 的文件锁机制不同,同一文件不建议多协议同时写入。产品层面通过文档告知用户"多协议是为不同设备类型服务,非同时并发写同一文件"。


四、核心功能

P0MVP 必须)

4.1 透明多协议代理

  • 对外暴露标准 SMB 共享,客户端连接方式与直连 NAS 完全一致
  • 支持 SMB2/SMB3 协议
  • 同时支持 NFS 导出Linux 客户端)和 WebDAV 服务(移动端)
  • 支持 macOSFinder/Lightroom、WindowsExplorer、Linux、移动端客户端
  • 文件读写、目录浏览、文件属性(时间戳/权限)均正常工作
  • 所有协议共享同一个缓存层,不重复存储

4.2 读缓存Read-through Cache

  • 文件首次被访问时,从远程 NAS 拉取并存入本地 SSD 缓存
  • 后续访问同一文件直接从本地 SSD 返回
  • 支持分块读取chunked read大文件不需要整个下载完才能开始读取
  • 支持预读read-ahead顺序读取场景下提前拉取后续数据
  • 目录列表缓存:目录结构缓存一段时间,避免频繁远程查询

4.3 写回缓存Write-back Cache

  • 文件写入先落本地 SSD
  • 文件关闭后延迟一段时间再异步回写远程 NAS
  • 频繁写入(如 Lightroom catalog 自动保存)自动合并,避免重复传输
  • 回写前必须检查远程状态,不盲写(详见第五章一致性模型)
  • 回写失败自动重试(指数退避)

4.4 数据一致性保证

  • 基于三时间戳模型保证最终一致性(详见第五章)
  • 元数据持久化到 SSD重启后可恢复所有状态继续回写
  • 回写前检查远程 mtime防止覆盖更新数据
  • 远程删除感知,防止已删文件被回写复活
  • 写冲突检测与冲突副本保留

4.5 远程变更检测

  • 基于 SFTP 的分层轮询机制,自动发现远程数据变化(详见第六章)
  • 不依赖任何 NAS 品牌特有 API纯 SFTP 协议实现
  • 每日全量校对兜底

4.6 缓存空间管理

  • 设置缓存总大小上限
  • 超出上限时按 LRU最久未访问策略自动淘汰
  • 可设置缓存盘最低保留空间,防止磁盘写满
  • 可设置缓存最大保留时间
  • 脏文件(未回写)永不被淘汰

4.7 一键部署

  • 提供完整的配置文件 + 部署脚本
  • 自动安装依赖rclone、Samba、NFS、fuse
  • 自动配置 systemd 服务,开机自启
  • 自动配置日志轮转
  • 缓存盘文件系统建议 btrfs/ZFSCoW + journal 保护一致性)

P1重要但非 MVP

4.8 SD 卡导入 + 自动归档Ingest

摄影师外拍结束,把 SD 卡插入盒子,一键备份到本地 SSD后台利用已有的 write-back 引擎异步上传回家中 NAS。

  • 检测到 SD/CFexpress 卡插入后,支持物理按钮一键触发导入或自动导入模式
  • 导入前空间检查:估算 SD 卡总数据量,检查 SSD 可用空间(扣除已有 dirty 文件 + CACHE_MIN_FREE空间不足时拒绝导入并通知用户
  • 复制文件通过 rclone FUSE 挂载点写入(而非直接写入 VFS 缓存目录),确保 rclone 能正确跟踪这些文件(详见 5.8.1 架构讨论)
  • 导入时计算文件 checksumSHA-256确保数据完整
  • 导入的文件在 metadata.db 中标记为 state=dirty, origin_mtime=NULL(表示从未在远程存在)
  • Write-back Controller 自动接管,拦截 rclone 的原生回写按自定义逻辑INGEST_TARGET_PATH 模板)决定 NAS 上的目标路径并上传,无网络时排队等待
  • 支持重复文件检测(基于文件名+大小+checksum同时查询当前 cache_files 和 import_history 持久表(详见 5.7.6),避免重复导入已回写到 NAS 但被 LRU 淘汰的文件
  • 导入完成通过 LED 指示灯 / 蜂鸣器提示
  • 支持按日期模板自动组织目标路径(如 /{year}/{month}/{date}/),日期来源为 EXIF 拍摄日期,非 EXIF 文件回退到文件 mtime详见 INGEST_DATE_SOURCE 配置)
  • 导入中断保护:导入过程维护状态机(detecting → copying → checksumming → registered → complete),中断的文件(未完成 checksum 校验)将被清理而非作为 dirty 文件进入缓存(详见 5.9

4.9 双卡备份 + 校验

摄影师同时插入两张 SD 卡(或一张 SD 卡 + 一个 USB 移动硬盘),盒子自动做双向 checksum 比对,确保两份备份完全一致。

  • 并行读取两个存储设备的文件列表
  • 逐文件比对:文件名 + 大小一致后计算 SHA-256 比对
  • 不一致的文件标记为异常并通知用户
  • 仅一侧存在的文件单独提示
  • 校验完成输出报告LED 状态 + CLI 可查详情)
  • 校验通过的文件自动进入 4.8 导入归档流程

4.10 Time Machine 备份目标

利用 Samba 原生 Time Machine 支持,让 macOS 用户的 Mac 出差时也有本地备份兜底。

  • Samba 配置开启 fruit:time machine = yes
  • Time Machine 使用独立目录/mnt/ssd/warpgate/timemachine/),不通过 rclone VFS 管理,避免 sparsebundle 的大量小文件干扰 rclone 缓存逻辑
  • Time Machine 备份写入 SSD 的 timemachine/ 目录
  • 独立的 TM 回写引擎异步归档到 NAS 的 TIMEMACHINE_PATH 目录(与通用 Write-back Controller 分离,详见下文)
  • TIMEMACHINE_MAX_SIZE 作为硬配额强制执行(通过 smb.conf 的 fruit:time machine max size 参数),防止占满缓存盘
  • 几乎零 Samba 配置成本,但 TM 回写引擎需要少量开发

Time Machine 回写策略sparsebundle 特殊处理)

Time Machine 使用 sparsebundle 格式(一个目录包含数千个 8MB band 文件)。通用回写引擎的 60s 延迟+写入合并机制不适用于此场景:

  • TM 备份持续 10-30 分钟,持续写入不同 band 文件,通用计时器会不断重置
  • 每次 TM 备份只修改部分 band 文件,不需要传输整个 sparsebundle
flowchart LR
    TM["macOS Time Machine<br/>写入 sparsebundle"] --> Monitor["① 会话结束检测<br/>连续 5min 无新写入"]
    Monitor --> Sync["② band 文件级增量同步<br/>rsync/rclone 只传 mtime 变化的 band"]
    Sync --> NAS["③ SFTP 直传到<br/>TIMEMACHINE_PATH<br/>(NAS 独立目录)"]

    style NAS fill:#e8f5e9

:④ TM 回写不走通用 Write-back Controller 的三时间戳冲突检测TM 数据只有一个写入源)。

TIMEMACHINE_PATH 与 NAS_REMOTE_PATH 的关系:两者是 NAS 上的不同目录互不关联。NAS_REMOTE_PATH 是用户的照片/文件目录(如 /volume1/photosTIMEMACHINE_PATH 是 TM 备份专用目录(如 /volume1/timemachine。TM 回写通过独立的 SFTP 连接直接传输到 TIMEMACHINE_PATH不经过 rclone VFS。

4.11 配网模式 + 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.12 缓存预热Warm-up

  • 命令行手动预热指定目录
  • 按时间范围预热(如"最近 7 天新增的文件"
  • 定时预热任务(如每天凌晨自动拉取最新数据)
  • 预热进度显示

4.13 管理工具CLI

  • warpgate status — 查看服务状态、缓存命中率、回写队列、冲突文件数
  • warpgate cache-list — 列出缓存中的文件
  • warpgate cache-clean — 清理缓存
  • warpgate warmup — 手动预热
  • warpgate bwlimit — 动态调整带宽限制
  • warpgate conflicts — 查看和处理冲突文件
  • warpgate ingest — 手动触发 SD 卡导入
  • warpgate verify — 双卡校验
  • warpgate log — 查看实时日志
  • warpgate speed-test — 链路速度测试
  • warpgate setup-wifi — 手动进入配网模式
  • warpgate clone-mac <MAC> — 克隆指定设备的 MAC 地址

4.14 带宽管理

  • 支持上传/下载分别限速
  • 运行时动态调整限速(不重启服务)
  • 回写带宽不影响读取体验

4.15 连接容错

  • Tailscale 断连时自动重试
  • 已缓存的文件在离线时仍可正常读取
  • 写回队列在恢复连接后自动续传
  • 连接超时参数可配置

4.16 写冲突通知

  • 冲突发生时通知用户CLI 提示 / 日志 / 可选 Webhook
  • 冲突文件清单管理
  • 手动解决冲突工具

P2后续迭代

4.17 WiFi AP 现场共享

盒子内置 WiFi 模块开启持久热点,现场团队设备连上即可通过 SMB/WebDAV 访问缓存目录。与 4.11 配网模式的区别:配网 AP 是临时的(完成配网后关闭),本功能是持久的团队共享热点。

  • 支持 AP 模式,复用已有的 SMB/WebDAV 多协议服务
  • AP 网络与 Tailscale/WAN 网络隔离(安全考虑)
  • AP 模式下仍可同时通过有线/4G 连接 Tailscale 做后台回写
  • 硬件要求需要两个独立网络接口——WiFi 模块用于 AP 热点,有线/USB 4G 网卡用于 WAN/Tailscale 连接。一体机硬件设计需预留双网卡
  • 典型场景:婚礼现场摄影师导入 SD 卡后,助理 iPad 连上 WiFi 即可浏览选片

4.18 Web 管理界面

  • 缓存状态仪表盘(大小、命中率、回写队列、冲突文件、带宽趋势图)
  • 缓存文件浏览器(查看/手动清除/手动预热)
  • 配置修改界面(参数调整无需编辑配置文件)
  • 冲突文件可视化处理
  • 实时日志查看器

4.19 NAS 侧 Agent 推送(可选增强)

  • 在 NAS 上运行轻量 AgentDocker 容器),监听文件变化主动推送给 Proxy
  • 实现秒级远程变更感知(替代分钟级轮询)
  • 不依赖品牌 API基于 inotify 通用方案

4.20 多 NAS / 多目录支持

  • 同时连接多个远程 NAS如家里 + 工作室)
  • 每个 NAS 独立共享名,独立缓存策略
  • 每个共享可配置不同的缓存大小和保留时间

4.21 智能缓存策略

  • 根据文件类型自动调整策略:
    • .lrcat / .xmpLightroom catalog/sidecar→ 高优先级回写,短写回延迟
    • .CR3 / .ARW / .NEFRAW 照片)→ 大块预读,长缓存保留
    • .mp4 / .mov(视频)→ 顺序预读优化
    • .psd / .ai(设计文件)→ 完整缓存,避免分块导致的兼容问题
  • 基于访问频率自动调整缓存优先级(热数据不被淘汰)

4.22 Docker 镜像

  • 一行命令启动:docker run -v /mnt/ssd:/cache warpgate
  • docker-compose 配置
  • 支持环境变量或挂载配置文件

4.23 通知机制

  • 回写失败告警Webhook / Telegram / 邮件)
  • 缓存空间不足告警
  • NAS 离线告警
  • 写冲突告警
  • 回写完成通知(可选)

五、数据一致性模型

5.1 设计目标

采用**最终一致性Eventual Consistency**模型。具体承诺:

  1. 所有成功写入本地缓存的数据最终会同步到远程 NAS断电恢复靠硬件 UPS 保证)
  2. 远程 NAS 上的变更会在可控时间内被 Proxy 感知并更新本地缓存
  3. 写冲突可检测、可追溯,不会静默丢数据

5.2 设计讨论与决策过程

问题 A写回中断断电/crash是否会丢数据

讨论rclone 的 write-back 机制下,文件先写入本地 SSD 缓存,延迟回写到远程。如果在回写之前 Proxy 断电,未回写的脏数据面临丢失风险。

考虑过的方案

  • 方案 1软件层 Write-Ahead LogWAL写入前先写日志再写数据恢复时重放
  • 方案 2选用支持 Power Loss Protection 的企业级 SSD
  • 方案 3缓存盘用 btrfs/ZFS利用 CoW + journal 保证文件系统一致性
  • 方案 4硬件内置电池类似 UPS保证断电后有时间 flush

最终决策:硬件层面通过内置电池/UPS 保证断电安全,软件层面不做额外的 WAL。原因是硬件方案最简单可靠且后续做硬件产品时电池是自然的组成部分。软件层面只需保证重启后能正确识别脏文件并继续回写即可。

rclone 的 VFS cache 目录本身维护了元数据,重启后会自动发现未同步的脏文件并继续回写,满足此需求。

问题 B写冲突如何处理谁来比较时间

讨论:核心场景是 Cache 在外改了文件但还没回写NAS 侧也被别人改了同一个文件。如果 Cache 关机几天后重启,盲目回写会覆盖 NAS 上的新版本。

反过来,如果 NAS 上删了文件Cache 重启后发现本地有脏文件,盲目回写会把已删文件复活。

考虑过的方案

  • 方案 1Last-write-wins 盲写(简单但会丢数据)
  • 方案 2基于 mtime 比较的 last-write-wins有保护的覆盖
  • 方案 3全版本保留两边都存用户手动选
  • 方案 4基于 vector clock 的分布式一致性(过于复杂)

最终决策:采用方案 2——基于 mtime 的 last-write-wins配合冲突副本保留。由 Proxy 侧的 Write-back Controller 负责在回写前查询远程 mtime 并做比较决策。

5.3 三时间戳模型

每个缓存文件维护三个关键时间戳:

origin_mtime  ── 文件从远程拉下来时(或上次成功回写后)远程的 mtime
                 含义:上次同步时远程的状态,是判断"远程是否变了"的基准线

cache_mtime   ── 本地缓存文件当前的 mtime
                 含义:本地版本的时间,如果 != origin_mtime 则说明本地改过

remote_mtime  ── 回写/读取时实时查询远程的当前 mtime
                 含义:远程此刻的真实状态

状态推导:

本地是否修改过:
  origin_mtime = NULL          →  从未在远程存在SD 卡导入的新文件),始终视为 dirty
  cache_mtime == origin_mtime  →  干净clean
  cache_mtime != origin_mtime  →  脏dirty有未回写的本地修改

远程是否变化过:
  origin_mtime = NULL          →  不适用(文件从未同步过远程,回写逻辑见 5.4b
  remote_mtime == origin_mtime →  远程没变(自上次同步后)
  remote_mtime != origin_mtime →  远程被别人改了
  远程文件不存在               →  远程被删了

注意:虽然 cache_files 表中有显式的 state 字段存储状态clean/dirty/conflict运行时依赖 state 字段即可。上述推导规则用于状态字段的正确性校验和初始化逻辑。origin_mtime = NULL 的记录 state 必须为 dirty(或 conflict),不可能为 clean

5.4 回写决策矩阵

当 Write-back Controller 准备回写一个脏文件时,先通过 SFTP stat 查询远程 mtime然后按以下矩阵决策

flowchart TD
    Start["Write-back Controller<br/>准备回写脏文件"] --> Check["SFTP stat 查远程 mtime"]
    Check --> NoChange{"remote == origin?<br/>远程没变"}
    Check --> Changed{"remote != origin?<br/>远程也改了"}
    Check --> Deleted{"远程文件不存在?<br/>远程被删了"}

    NoChange -->|"✅ 安全回写"| WB["回写到远程<br/>更新 origin_mtime"]

    Changed --> CmpMtime{"比较 mtime"}
    CmpMtime -->|"cache > remote<br/>本地更新"| WB2["本地胜,回写"]
    CmpMtime -->|"remote > cache<br/>远程更新"| Conflict1["远程胜<br/>拉远程新版本<br/>本地存冲突副本"]
    CmpMtime -->|"mtime 相等<br/>内容可能不同"| LocalWins["本地胜<br/>(写入者优先)"]

    Deleted -->|"⚠️ 不回写"| Respect["尊重远程删除<br/>脏文件移到 conflict/<br/>通知用户"]

5.4b SD 卡导入文件的回写决策(补充)

从 SD 卡导入的文件,本质上等同于「本地新创建的脏文件」。在 metadata.db 的 cache_files 表中,origin_mtime = NULL 表示此文件从未存在于远程 NAS。

flowchart TD
    Start["origin_mtime = NULL<br/>SD 卡导入的新文件"] --> Stat["SFTP stat 查远程"]
    Stat -->|"文件不存在"| Upload["直接上传(新建)<br/>更新 origin_mtime<br/>state → clean"]
    Stat -->|"文件已存在<br/>(之前导入已上传 / 远程碰巧同名)"| Compare["比较 cache_mtime vs remote_mtime<br/>按 5.4 正常冲突逻辑处理"]
    Stat -->|"上传失败"| Retry["重试(指数退避)<br/>保持 dirty 状态"]

5.5 读取时的缓存验证

当应用读取一个已缓存的文件时:

远程没变 (remote == origin) 远程也改了 (remote != origin) 远程被删了 (文件不存在)
干净缓存 (clean) 直接用缓存 🔄 拉新版本,更新 origin_mtime 🗑️ 删除本地缓存,返回文件不存在
本地改过 (dirty) 用本地版本,等待回写 ⚠️ 标记冲突,两边版本都保留,通知用户 ⚠️ 用本地版本,但标记为冲突,通知用户

注意:读取时不是每次都查远程 mtime那样太慢。远程 mtime 信息由后台轮询线程定期更新(见第六章)。热路径上读取命中缓存直接返回,只有当轮询发现 mtime 变化时才触发重新验证。

5.6 关键场景走查

场景 1Cache 关机几天NAS 上文件被修改

flowchart TD
    D1["Day 1: Cache 缓存 photo.cr3<br/>(origin_mtime=Day1, clean)"] --> Off["Day 1: Cache 关机"]
    Off --> D3["Day 3: NAS 上 photo.cr3 被修改<br/>(NAS mtime → Day3)"]
    D3 --> D5["Day 5: Cache 开机<br/>rclone 启动,轮询线程开始"]
    D5 --> Detect["轮询发现 remote_mtime(Day3)<br/>!= origin_mtime(Day1)"]
    Detect --> Clean["文件是 clean本地没改过"]
    Clean --> Invalidate["标记缓存失效<br/>下次访问时拉新版本"]
    Invalidate --> Result["✅ 用户读到 NAS 最新版本"]

场景 2Cache 关机几天本地有脏数据NAS 也被更新

flowchart TD
    D1["Day 1: 用户修改 photo.cr3<br/>(origin=Day0, cache=Day1, dirty)"]
    D1 --> Off["Day 1: 回写未执行Cache 关机<br/>(电池保护数据落盘)"]
    Off --> D3["Day 3: 家人在 NAS 修改 photo.cr3<br/>(NAS mtime → Day3)"]
    D3 --> D5["Day 5: Cache 开机<br/>发现本地有脏文件"]
    D5 --> WB["Write-back Controller 准备回写"]
    WB --> Stat["SFTP stat 查远程:<br/>remote_mtime = Day3"]
    Stat --> Conflict["remote(Day3) != origin(Day0)<br/>→ 远程被改过!"]
    Conflict --> Compare["cache(Day1) vs remote(Day3)<br/>Day3 > Day1 → 远程胜"]
    Compare --> Actions["本地脏版本 → conflict/<br/>拉远程新版本覆盖缓存<br/>更新 origin=Day3, clean<br/>通知用户"]
    Actions --> Result["✅ 不丢数据,两版本都保留"]

场景 3NAS 删了文件Cache 上有脏数据

flowchart TD
    D1["Day 1: 用户修改 photo.cr3 (dirty)"] --> Off["Day 1: Cache 关机"]
    Off --> D3["Day 3: NAS 上 photo.cr3 被删除"]
    D3 --> D5["Day 5: Cache 开机,准备回写"]
    D5 --> Stat["Write-back Controller 查远程:<br/>photo.cr3 不存在"]
    Stat --> Conflict["远程已删 + 本地有脏数据 → 冲突"]
    Conflict --> Actions["❌ 不回写(尊重远程删除)<br/>脏版本 → conflict/<br/>通知用户"]
    Actions --> Result["✅ 不会复活已删文件<br/>用户可从 conflict/ 恢复"]

场景 4NAS 删了文件Cache 上是 clean 的

flowchart TD
    D1["Day 1: Cache 缓存 photo.cr3 (clean)"]
    D1 --> D3["Day 3: NAS 上删除 photo.cr3"]
    D3 --> Poll["轮询检测到远程目录变化"]
    Poll --> Check["photo.cr3 远程不存在<br/>+ 本地是 clean → 无争议"]
    Check --> Del["直接删除本地缓存"]
    Del --> Result["✅ 本地缓存与远程保持一致"]

场景 5Cache 在外编辑文件网络正常NAS 无变化(最常见 happy path

flowchart TD
    Open["用户在酒店打开 Lightroom 修图"]
    Open --> Miss["首次打开 photo.cr3<br/>缓存未命中 → 远程下载 → 缓存到 SSD<br/>(origin=T0, cache=T0, clean)"]
    Miss --> Edit["用户编辑并保存<br/>(cache=T1, dirty)"]
    Edit --> Timer["60s 后 Write-back Controller 触发"]
    Timer --> Check["查远程: remote == origin(T0)<br/>→ 远程没变"]
    Check --> WB["正常回写<br/>更新 origin=T1, clean"]
    WB --> Result["✅ 编辑秒存本地<br/>后台静默同步,用户无感"]

5.7 元数据持久化metadata DB

三时间戳和文件状态必须持久化到 SSD确保重启不丢失。使用 SQLite 存储。

5.7.1 设计讨论metadata DB 应该包含什么?

问题metadata DB 需要存 NAS 侧的所有文件元数据吗?

分析NAS 上可能有几十万甚至上百万文件,但用户一次出差实际访问的可能只有几百到几千个。如果全量存储 NAS 文件元数据,会带来几个问题:

  • 初始化成本高——首次使用需要递归扫描整个 NAS 目录树
  • 存储浪费——大量从未访问的文件元数据没有价值
  • 同步负担重——需要持续维护全量数据的一致性

结论metadata DB 只管"跟缓存有交集的文件",不存全量 NAS 元数据。NAS 上有 10 万张照片,用户只打开过 500 张,那核心表就只有 500 行。

但有一个衍生问题:怎么检测远程文件被删了?

轮询发现某目录 mtime 变了,做 sftp ls 拿到当前远程文件列表,但要知道"哪个文件消失了",需要跟之前的列表比较。这引出了"是否需要额外存目录文件列表"的设计选择。

方案推导

假设目录 /2026/02/ 下有 200 张照片,用户只缓存了 3 张。

场景 ANAS 上 IMG_0050.cr3 被删了(从未缓存过) → 跟缓存无关,不需要感知,不处理

场景 BNAS 上 IMG_0001.cr3 被删了(缓存过) → sftp ls 结果里找不到 IMG_0001 → 但 cache_files 表里有它 → 检测到删除

场景 CNAS 上新增了 IMG_0201.cr3同时修改了 IMG_0050.cr3 → IMG_0201 从没缓存过,不关心 → IMG_0050 从没缓存过,不关心 → 如果 IMG_0050 缓存过cache_files 里有 origin_mtime 可以直接比

关键洞察:删除检测只需要用 sftp ls 结果去反查 cache_files 表——"cache_files 里有记录,但 sftp ls 结果里没有这个文件"即为远程删除。不需要额外维护一张完整的目录文件列表。

最终决策(分阶段)

阶段 策略 表结构 理由
MVP (v1.0) 精简模式 cache_files + dir_snapshots两张表 删除检测通过反查 cache_files 实现,逻辑简单
v1.5+ 完整模式 加 dir_file_list三张表 记录关心目录下全部远程文件,支持精确变更类型识别和智能预热(如"自动缓存新增文件"

5.7.2 缓存目录结构

/mnt/ssd/warpgate/
├── vfs/                          # rclone VFS 缓存目录rclone 内部管理,外部进程不直接写入)
│   └── photos/
│       ├── 2026/
│       │   └── 02/
│       │       ├── IMG_0001.cr3   # 远程拉取缓存
│       │       └── IMG_0002.cr3   # 或通过 FUSE 挂载点写入的 SD 卡导入文件
│       └── ...
├── metadata.db                   # SQLite 元数据库WAL 模式,详见 5.7.7
├── conflict/                     # 冲突文件暂存目录
│   └── IMG_0001.cr3.local-20260216-143022
├── ingest_staging/               # SD 卡导入暂存目录(导入状态机使用,详见 5.9
│   └── <session-id>/            # 每次导入会话独立目录
└── timemachine/                  # Time Machine 备份目录(独立于 rclone VFS详见 4.10
    └── MacBook-Pro.sparsebundle

重要vfs/ 目录由 rclone VFS 内部管理,维护自己的元数据来跟踪缓存了哪些文件。任何外部进程(包括 SD 卡导入)不得直接写入此目录,否则会导致 rclone 内部元数据与 metadata.db 不同步。SD 卡导入必须通过 rclone FUSE 挂载点(/mnt/nas-photos)写入,详见 5.8.1。

5.7.3 表结构定义

表 1cache_files — 缓存文件状态(核心表)

只有进过缓存的文件才会有记录。

CREATE TABLE cache_files (
    path            TEXT PRIMARY KEY,    -- 相对路径,如 /2026/02/IMG_0001.cr3
    dir_path        TEXT NOT NULL,       -- 所属目录,如 /2026/02/(加速目录级查询)
    origin_mtime    INTEGER,            -- 拉下来时(或上次成功回写后)远程的 mtime
                                        -- NULL 表示 SD 卡导入的新文件,从未在远程存在
    cache_mtime     INTEGER NOT NULL,    -- 本地缓存文件当前 mtime
    file_size       INTEGER NOT NULL,    -- 文件大小(字节,统计 + 淘汰决策用)
    state           TEXT NOT NULL,       -- clean / dirty / conflict
    source          TEXT DEFAULT 'remote', -- 文件来源remote远程拉取/ ingestSD 卡导入)
    last_accessed   INTEGER NOT NULL,    -- 最后访问时间LRU 淘汰用)
    writeback_retry INTEGER DEFAULT 0,   -- 回写失败累计重试次数
    checksum        TEXT,               -- SHA-256 校验和SD 卡导入时计算)
    created_at      INTEGER NOT NULL     -- 首次缓存时间
);

CREATE INDEX idx_state ON cache_files(state);
CREATE INDEX idx_dir_path ON cache_files(dir_path);
CREATE INDEX idx_last_accessed ON cache_files(last_accessed);
CREATE INDEX idx_dirty ON cache_files(state) WHERE state = 'dirty';

生命周期:

文件首次被访问并下载   → INSERT (state=clean, source=remote, origin_mtime=远程mtime, cache_mtime=远程mtime)
SD 卡导入新文件       → INSERT (state=dirty, source=ingest, origin_mtime=NULL, cache_mtime=当前时间, checksum=SHA256)
本地修改(应用保存)   → UPDATE (state=dirty, cache_mtime=当前时间)
回写成功              → UPDATE (state=clean, origin_mtime=更新后的mtime, writeback_retry=0)
回写失败              → UPDATE (writeback_retry += 1)
缓存被 LRU 淘汰      → DELETE仅 state=clean 可被淘汰)
检测到远程删除clean→ DELETE
检测到冲突            → UPDATE (state=conflict)
冲突处理完成          → DELETE 或 UPDATE (state=clean)

表 2dir_snapshots — 目录级轮询快照

用于分层轮询的"目录 mtime 快检"。只记录被缓存文件所在的目录,不是 NAS 全量目录。

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,    -- 目录最后被访问时间(决定热/温/冷分级)
    cached_count    INTEGER DEFAULT 0    -- 该目录下 cache_files 条目数(辅助清理判断)
);

生命周期:

某目录下的文件首次被缓存    → INSERT或 UPDATE cached_count
SD 卡导入创建新目录          → INSERTremote_mtime = NULL表示远程目录尚不存在
                              回写成功后更新 remote_mtime 为实际值
轮询时目录 mtime 没变       → UPDATE last_polled
轮询时目录 mtime 变了       → UPDATE remote_mtime触发文件级检查
目录下已无缓存文件          → DELETE可选
remote_mtime = NULL 的目录  → 跳过轮询(远程还不存在,等回写创建后再轮询)

表 3dir_file_list — 目录文件列表快照v1.5+

MVP 阶段不需要。v1.5 加入后,记录被关心目录下的全部远程文件,支持精确变更类型识别和智能预热。

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.7.6 导入历史表(重复检测用)

问题:重复文件检测如果只查 cache_files 表,那么已回写到 NAS 并被 LRU 淘汰的文件cache_files 记录已被 DELETE再次导入同一张卡时无法检测到重复。

解决方案:增加 import_history 持久表,记录所有历史导入记录,不随 LRU 淘汰删除。

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,    -- 导入时间
    writeback_at    INTEGER,            -- 回写到 NAS 的时间NULL=未回写)
    state           TEXT NOT NULL        -- imported / writeback_done / 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 = ?
  → 命中 → 跳过该文件,标记为"已导入过"
  → 未命中 → 继续查 cache_files WHERE checksum = ? AND file_size = ?
  → 均未命中 → 执行导入

5.7.4 四张表的关系与数据规模

erDiagram
    dir_snapshots ||--o{ dir_file_list : "1:N (v1.5+)"
    dir_file_list ||--o| cache_files : "仅缓存过的文件"
    dir_snapshots ||--o{ cache_files : "1:N"

    dir_snapshots {
        TEXT dir_path PK "如 /2026/02/"
        INTEGER remote_mtime
        INTEGER last_polled
        INTEGER last_accessed
        INTEGER cached_count
    }

    dir_file_list {
        TEXT dir_path FK "v1.5+ 可选"
        TEXT file_name "该目录下全部远程文件"
        INTEGER remote_mtime
        INTEGER remote_size
    }

    cache_files {
        TEXT path PK "只有缓存过的文件"
        TEXT dir_path FK
        INTEGER origin_mtime "NULL=SD导入"
        INTEGER cache_mtime
        TEXT state "clean/dirty/conflict"
        TEXT source "remote/ingest"
    }

    import_history {
        INTEGER id PK "独立于缓存生命周期"
        TEXT original_path
        TEXT target_path
        TEXT checksum "持续累积,永久保留"
        TEXT state "imported/writeback_done/failed"
    }

数据规模参考dir_snapshots 每个关心的目录 1 行dir_file_list 可能 200 行/目录cache_files 仅缓存过的文件(可能 3 行/目录import_history 持续累积。

数据规模估算(以 NAS 上 10 万个文件、用户缓存了 1000 个为例):

记录范围 预估行数 存储开销
cache_files 本地缓存过的文件 ~1,000 ~100 KB
dir_snapshots 有缓存文件的目录 ~50 ~5 KB
dir_file_list (v1.5+) 关心目录下的全部远程文件 ~10,000 ~1 MB
import_history 所有历史导入记录(持续累积) ~5,000 ~500 KB
总计 < 2 MB

5.7.5 删除检测流程MVP 精简模式)

MVP 阶段不使用 dir_file_list通过反查 cache_files 实现删除检测:

flowchart TD
    Trigger["轮询发现 /2026/02/ 目录 mtime 变了"]
    Trigger --> LS["sftp ls /2026/02/<br/>→ 拿到 remote_set"]
    LS --> Query["SELECT path FROM cache_files<br/>WHERE dir_path = '/2026/02/'<br/>→ 拿到 cached_set"]
    Query --> Compare{"对比两个集合"}
    Compare -->|"在 cached 不在 remote"| Deleted["远程被删了<br/>→ 按决策矩阵处理"]
    Compare -->|"在 cached 且在 remote"| MtimeCheck["比较 mtime<br/>变了则标记失效"]
    Compare -->|"在 remote 不在 cached"| Ignore["远程新增/未缓存<br/>→ 不处理(等访问时拉)"]
    Deleted --> Update["更新 dir_snapshots<br/>remote_mtime + last_polled"]
    MtimeCheck --> Update
    Ignore --> Update

这样只用两张表就完成了所有检测,逻辑清晰,开销极小。

5.7.7 SQLite 并发访问策略

metadata.db 会被多个进程/线程并发访问Write-back Controller、轮询线程、SD 卡导入进程、CLI 管理工具。

要求metadata.db 必须存放在本地文件系统SSD 的 ext4/btrfs/ZFS严禁放在 rclone FUSE 挂载目录中。SQLite WAL 依赖 POSIX 文件锁和共享内存(-shm 文件FUSE/网络文件系统无法正确支持这些语义,会导致数据库损坏。

metadata.db 必须以 WALWrite-Ahead Logging模式运行:

PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;  -- 锁等待超时 5 秒

WAL 模式的优势:

  • 读操作不阻塞写操作,写操作不阻塞读操作
  • 多个进程可以同时读取,只有写入互斥
  • 适合"多读少写"的缓存元数据场景

所有访问 metadata.db 的进程必须使用同一个 WAL 模式配置。部署脚本在初始化数据库时自动设置。

5.8 Write-back Controller 架构

rclone 原生的 --vfs-cache-mode full 不做回写前的 mtime 比较(盲写),因此需要在 rclone 之上包一层 Write-back Controller

原来rclone 默认)rclone VFS → 脏文件 → 直接 SFTP 上传 → 可能覆盖新数据

改为(双管道架构)

flowchart TD
    subgraph pipeA ["管道 A — 远程拉取后的本地编辑"]
        A1["rclone VFS 脏文件<br/>(source=remote)"] --> WBC_A["Write-back Controller"]
        WBC_A --> A2["SFTP stat 查远程 mtime"]
        A2 --> A3["三时间戳比较"]
        A3 --> A4["决策: 回写 / 冲突保留 / 跳过"]
        A4 --> A5["回写到原始远程路径<br/>+ 更新 metadata.db"]
    end

    subgraph pipeB ["管道 B — SD 卡导入的新文件"]
        B1["SD 卡导入<br/>(通过 FUSE 挂载点写入)"] --> B2["rclone 自动标记为脏文件<br/>(source=ingest)"]
        B2 --> WBC_B["Write-back Controller"]
        WBC_B --> B3["检测 origin_mtime=NULL"]
        B3 --> B4["按 INGEST_TARGET_PATH<br/>计算 NAS 目标路径"]
        B4 --> B5["创建目标目录(如不存在)"]
        B5 --> B6["SFTP stat → 按 5.4b 决策"]
        B6 --> B7["上传到目标路径<br/>+ 更新 metadata.db<br/>+ 记录 import_history"]
    end

Write-back Controller 作为独立进程运行,监控 rclone 缓存目录中的脏文件,替代 rclone 的原生回写逻辑。Controller 通过 metadata.db 中的 source 字段区分两条管道,对 source=ingest 的文件执行路径重映射和目录创建逻辑。

5.8.1 SD 卡导入与 rclone VFS 的集成方案(架构讨论)

问题rclone VFS 维护自己的内部元数据来跟踪缓存了哪些文件。如果外部进程SD 卡导入)直接往 VFS 缓存目录写文件rclone 完全不知道这些文件的存在,会导致:

  1. rclone 的内部元数据与 metadata.db 不同步
  2. 应用通过 FUSE 挂载点访问这些文件时rclone 可能尝试从远程拉取(因为它不知道本地已有)
  3. Write-back Controller 需要处理的脏文件从 rclone 角度看根本不是脏文件

考虑过的方案

方案 原理 优点 缺点
A. 通过 FUSE 挂载点写入 SD 卡导入写入 /mnt/nas-photos/ingest/...rclone 自然跟踪 rclone 自动管理缓存元数据;脏文件对 rclone 可见 需要拦截 rclone 的原生回写以实现自定义路径映射FUSE 写入性能可能低于直写 SSD
B. 自定义缓存层 不依赖 rclone 作为唯一缓存管理器,构建自定义缓存层 完全控制缓存行为 开发量大,需要自己实现远程拉取、缓存淘汰等 rclone 已有功能

最终决策:采用方案 A——通过 rclone FUSE 挂载点写入。

实现要点:

  1. SD 卡导入进程先将文件复制到 ingest_staging/ 暂存目录并计算 checksum
  2. 校验通过后,通过 FUSE 挂载点(/mnt/nas-photos)写入到 INGEST_TARGET_PATH 指定的路径
  3. rclone 自动将其纳入 VFS 缓存管理并标记为脏文件
  4. 导入进程同步在 metadata.db 中创建 source=ingest, origin_mtime=NULL 记录
  5. Write-back Controller 拦截 rclone 的原生回写(通过配置 --vfs-write-back 为极大值或禁用),执行自定义回写逻辑

⚠️ 实现前需要做架构 spike 验证:确认 rclone 在 --vfs-write-back 被禁用时的行为,以及通过 FUSE 挂载点写入大文件的性能是否满足导入场景需求目标SD 卡导入速度 ≥ 80MB/s

5.9 SD 卡导入状态机(中断保护)

问题SD 卡导入过程中可能发生中断(卡被拔出、电池耗尽、进程崩溃)。部分复制的文件如果被标记为 dirty 并进入回写队列,会把不完整的文件上传到 NAS。

解决方案:导入过程维护严格的状态机,只有完整校验通过的文件才进入缓存。

stateDiagram-v2
    [*] --> detecting: SD 卡插入
    detecting --> copying: 列出文件清单
    copying --> checksumming: 复制到 ingest_staging/
    checksumming --> registered: SHA-256 校验通过
    registered --> complete: 移到 FUSE 挂载点 +<br/>写入 metadata.db + import_history
    complete --> [*]: Write-back Controller 接管

    note right of detecting: 中断 → 清理 staging 临时文件
    note right of copying: 中断 → 清理 staging 临时文件
    note right of checksumming: 中断 → 清理不完整文件
    note right of registered: 中断 → 文件已在 FUSE + DB<br/>重启后 Controller 正常接管

中断保护:每个导入会话有唯一 session_iddetecting/copying/checksumming 阶段中断时,ingest_staging/ 中的临时文件在下次启动时自动清理。registered 阶段中断时文件已安全进入缓存系统Controller 重启后正常接管。

实现要点

  • 暂存目录 ingest_staging/<session-id>/ 按导入会话隔离
  • 进程启动时扫描 ingest_staging/,清理所有未完成的会话(非 registered 状态的文件)
  • 导入进度持久化到 ingest_sessions 表或简单的 JSON 文件,支持断点续传

六、远程变更检测机制

6.1 设计约束

不依赖任何 NAS 品牌特有 API。产品需要支持群晖、QNAP、威联通、TrueNAS 等任意品牌 NAS因此只能基于标准 SFTP 协议实现。

6.2 SFTP 协议的能力边界(设计讨论)

问题:远程 NAS 上数据更新了Cache 怎么知道?能否实时感知?

讨论SFTP 协议本身没有任何通知/推送/订阅机制。它是无状态的文件传输协议,不支持 inotify、webhook、filesystem watch 等概念。每次想知道远程状态,必须主动发请求查询。

考虑过的方案

方案 原理 实时性 品牌依赖 取舍
SFTP 全量轮询 递归 ls 对比 mtime 分钟级 文件多时开销大
SFTP 分层轮询 先查目录 mtime变了再查文件 分钟级 高效,推荐
群晖 FileStation API 调 DSM Web API 秒级 仅群晖 不通用
NAS 侧 Agent 推送 inotifywait → HTTP 通知 秒级 需要 NAS 装软件

最终决策

  • P0MVPSFTP 分层轮询,零额外依赖
  • P2后续:可选的 NAS 侧 Agent 推送,作为增强项给需要秒级同步的用户

6.3 分层轮询策略

核心优化思路:SFTP 目录本身也有 mtime。当目录下有文件新增/修改/删除时,目录的 mtime 会更新。因此可以先查目录 mtime一个 stat 请求),没变就跳过整个目录下所有文件的检查,大幅减少远程请求量。

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/>捕捉遗漏 + 清理过期条目"]

轮询伪代码(与 5.7.5 删除检测对齐):

# 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  # 目录没变,跳过全部文件 ✅

    # 第二层:目录变了,查里面的文件
    remote_files = sftp_ls(dir.dir_path)                    # { name → mtime }
    cached_files = SELECT * FROM cache_files                 # 该目录下的缓存文件
                   WHERE dir_path = dir.dir_path

    # 检查已缓存文件的变化
    for file in cached_files:
        remote = remote_files.get(file.path)
        if remote is None:
            # 远程文件被删了 → 按决策矩阵处理
            handle_remote_deletion(file)
        elif remote.mtime != file.origin_mtime:
            # 远程文件被修改了
            if file.state == "clean":
                invalidate_cache(file.path)      # 标记失效,下次访问拉新版
            else:
                mark_conflict(file.path)          # 本地有脏数据,标记冲突

    # 远程新增的文件(在 remote_files 但不在 cached_files不处理
    # 等用户实际访问时再按需拉取

    # 更新目录快照
    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 监听变化并推送:

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 读取流程

flowchart TD
    App["应用请求读取文件"] --> Check{"① 查本地缓存"}
    Check -->|"未命中"| Remote["③ 向远程请求"]
    Check -->|"命中"| Valid{"② 轮询是否标记失效?"}

    Valid -->|"未失效"| Return["直接返回缓存<br/>SSD 速度)"]
    Valid -->|"已失效"| State{"④ 检查文件状态"}

    State -->|"clean"| Pull["拉远程新版本<br/>更新缓存"]
    State -->|"dirty"| Conflict["标记冲突<br/>两边保留"]

    Remote --> Chunk["④ 按 chunk 分块下载"]
    Chunk --> Write["⑤ 写入本地 SSD 缓存<br/>⑥ 记录 origin_mtime"]
    Write --> ReturnData["⑦ 返回数据给应用"]

设计要点

  • 读取热路径上不查远程(不产生网络请求),直接返回缓存,保证响应速度
  • 远程变更检测由后台轮询线程异步完成,发现变化时标记缓存失效
  • 失效的 clean 文件下次访问时自动拉新版本
  • 失效的 dirty 文件标记为冲突,需要用户介入

7.2 写入流程

flowchart TD
    App["应用写入文件"] --> SSD["① 写入本地 SSD 缓存<br/>(立即返回成功给应用)"]
    SSD --> Meta["② 更新 metadata.db<br/>cache_mtime=now, state=dirty"]
    Meta --> Timer["③ 启动写回计时器60s"]
    Timer -->|"计时期间又有写入"| Timer
    Timer -->|"计时器到期"| WBC["④ Write-back Controller 接管"]
    WBC --> Stat["⑤ SFTP stat 查远程 mtime"]
    Stat --> Decision{"⑥ 三时间戳比较"}

    Decision -->|"正常回写"| Upload["上传 + 更新 origin_mtime<br/>state → clean"]
    Decision -->|"冲突"| ConflictKeep["保留双版本 + 通知用户"]
    Decision -->|"远程已删"| NoWrite["不回写 + 移到 conflict/"]

    Upload -->|"上传失败"| Retry["重试(指数退避)<br/>10s, 20s, 40s... 最多 10 次"]
    Retry -->|"最终失败"| Keep["保留本地state=dirty<br/>记录日志"]

7.3 缓存淘汰策略

flowchart TD
    Trigger{"淘汰触发<br/>(总大小 > MAX_SIZE<br/>或可用 < MIN_FREE)"} --> D1{"① dirty 文件?"}
    D1 -->|"永不淘汰"| D2{"② conflict 文件?"}
    D2 -->|"永不淘汰"| D3["③ 淘汰超过 MAX_AGE<br/>的 clean 文件"]
    D3 --> D4["④ 剩余 clean 按 LRU 淘汰<br/>(last_accessed 最早优先)"]
    D4 --> D5{"⑤ 空间满足?"}
    D5 -->|"是"| Done["淘汰完成"]
    D5 -->|"否clean 已耗尽)"| Alert["⑥ 进入「缓存空间告警」<br/>详见 7.3.1"]

7.3.1 缓存空间保护机制

问题dirty 文件永不被淘汰,但 SD 卡导入和 Time Machine 写入都会创建大量 dirty 文件。极端场景下(如离线导入 350GB 照片dirty 文件总量可能超过 SSD 可用空间。

保护措施

  1. 导入前空间预检SD 卡导入):可用空间 = CACHE_MAX_SIZE - dirty总量 - conflict总量 - CACHE_MIN_FREE。SD 卡总数据量 > 可用空间时拒绝导入通知用户LED 红灯 + CLI 提示),建议先连网回写或清理缓存
  2. Time Machine 硬配额TIMEMACHINE_MAX_SIZE 由 smb.conf 的 fruit:time machine max size 强制执行,超过配额时 macOS 自动清理旧备份
  3. 配置验证(部署时):TIMEMACHINE_MAX_SIZE + INGEST_MAX_IMPORT_SIZE + CACHE_MIN_FREE < CACHE_MAX_SIZE,不满足时部署脚本报警告
  4. 缓存空间告警dirty + conflict 总量 > CACHE_MAX_SIZE × 80% 时触发告警CLI / 日志 / Webhook提示用户尽快连网回写

7.4 离线行为

场景 行为
远程不可达,读取已缓存文件 正常返回,无影响
远程不可达,读取未缓存文件 超时报错(可配置超时时间)
远程不可达,写入文件 正常写入本地缓存,回写排队等待恢复
远程不可达,后台轮询 静默跳过,不报错,下次重试
恢复连接后 自动续传回写队列 + 立即触发一轮轮询

八、配置参数清单

连接配置

参数 说明 默认值 建议值
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 缓存最大保留时间 720h30天 按使用习惯
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 -

写回配置

参数 说明 默认值 场景建议
WRITE_BACK 写回延迟 60s Lightroom: 60-120s,文档: 10-30s
TRANSFERS 并发回写线程 4 带宽小就设 2
BW_LIMIT_UP 上传限速 0(不限) 酒店 WiFi 建议 10-20M
BW_LIMIT_DOWN 下载限速 0(不限) 一般不限

目录缓存与轮询

参数 说明 默认值 场景建议
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 凌晨低峰期

冲突处理

参数 说明 默认值 建议值
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 时间自动扫描 CONFLICT_DIR,删除超过 CONFLICT_RETAIN_DAYS 天的冲突副本。清理前记录日志。

多协议配置

参数 说明 默认值 建议值
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拍摄日期回退到mtimemtime=文件修改时间,import=导入时间
INGEST_DUPLICATE_CHECK 重复文件检测(基于文件名+大小+checksum yes yes
INGEST_DELETE_AFTER 导入+校验完成后是否删除卡上数据 no no(安全起见)
INGEST_PRIORITY 导入文件的回写优先级 high 高于普通编辑文件
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 -

Time Machine 配置

参数 说明 默认值 建议值
TIMEMACHINE_ENABLED 启用 Time Machine 支持 no Mac 用户开启
TIMEMACHINE_MAX_SIZE Time Machine 空间上限 200G 按 SSD 容量调整
TIMEMACHINE_PATH NAS 上的归档目标路径 /timemachine/ -

九、场景预设(模板)

为降低用户配置门槛,提供开箱即用的预设模板。

摄影师模式

重点优化大文件读取性能、Lightroom catalog 回写保护
- CACHE_MAX_SIZE=500G
- READ_CHUNK_SIZE=256M
- READ_AHEAD=512M
- WRITE_BACK=120s        ← Lightroom 频繁自动保存,合并写入
- DIR_CACHE_TIME=2h      ← 目录结构不常变
- POLL_HOT_INTERVAL=30s
- TRANSFERS=4
- ENABLE_SMB=yes
- ENABLE_NFS=no
- ENABLE_WEBDAV=no

视频剪辑模式

重点优化:顺序读取性能、大文件预读
- CACHE_MAX_SIZE=1T
- READ_CHUNK_SIZE=512M
- READ_AHEAD=1G          ← 大预读保证播放流畅
- WRITE_BACK=60s
- DIR_CACHE_TIME=1h
- POLL_HOT_INTERVAL=1m
- TRANSFERS=2            ← 减少回写并发,保带宽给播放
- ENABLE_SMB=yes
- ENABLE_NFS=no
- ENABLE_WEBDAV=no

文档办公模式

重点优化:小文件快速响应、写入快速同步
- CACHE_MAX_SIZE=50G
- READ_CHUNK_SIZE=64M
- READ_AHEAD=128M
- WRITE_BACK=10s         ← 文档改完快同步
- DIR_CACHE_TIME=30m     ← 协作场景需要较快看到新文件
- POLL_HOT_INTERVAL=15s  ← 更频繁感知远程变更
- 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 机制,即使意外断电也能保证文件系统级别的一致性。

# 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+(关键参数:--vfs-cache-mode full --vfs-write-back 999h --vfs-cache-max-size {CACHE_MAX_SIZE}
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 等)

十一、风险与局限

风险 等级 说明 缓解措施
断电丢数据 write-back 窗口期内断电 硬件 UPS/电池保证落盘btrfs/ZFS 保证文件系统一致性
写冲突 多端同时改同一文件 回写前 mtime 比较 + 冲突副本保留 + 通知用户
远程删除后复活 Cache 脏文件回写已删文件 回写前检查远程存在性,远程已删则不回写
首次访问慢 固有 未缓存文件必须走远程 预热功能;分块下载优化
缓存一致性延迟 远程变更在轮询间隔内不可见 分层轮询(热目录 30s后续可选 Agent 推送
Tailscale 断连 远程不可达时新文件无法获取 已缓存文件仍可用;回写自动排队;恢复后自动续传
多协议锁冲突 SMB/NFS 锁机制不同 文档约束"同一文件不建议多协议同时写"
轮询开销 大量文件目录轮询消耗带宽 目录 mtime 快检跳过未变目录;热度分级降低冷目录频率
SD 卡导入数据损坏 卡本身坏块导致导入不完整 导入时计算 SHA-256 校验和;双卡校验比对
SD 卡导入中断 卡被拔出 / 电池耗尽 / 进程崩溃 导入状态机保护5.9);未完成文件清理而非标为 dirty
缓存空间耗尽 dirty 文件(导入+TM撑满 SSD 导入前空间预检TM 硬配额缓存空间告警7.3.1
中转服务带宽成本 DERP 中继带宽随用户增长上升 大部分连接走 P2P 直连;按流量分级限速/计费;初期节点少按需扩容
云备份存储成本 用户数据增长导致存储费用上升 接低价对象存储B2/R2按量计费传导成本增量备份减少传输量
酒店 Captive Portal Headless 设备无法完成网页认证,旅途场景不可用 配网 AP + Portal 代理4.11fallbackUSB tethering / 手机热点 / MAC 克隆

十二、后续演进方向

阶段 内容 重点
v1.0 — MVP 配置文件 + 部署脚本 + CLI 管理 + 基础一致性 + Time Machine 支持 SMB + 读写缓存 + 三时间戳 + 精简 metadata两表+ 分层轮询 + TM 备份目标(几乎零 Samba 配置成本)
v1.5 — 硬件原型 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 + 运维能力,门槛高。

方案

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. 无需了解 Headscale、DERP、WireGuard 等概念

迁移路径(避免 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 在家里是单点故障——硬盘坏、被盗、火灾、水灾都可能导致数据永久丢失。

方案

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

与现有架构的关系

  • 复用 write-back 引擎的思路:本地脏文件 → 异步上传
  • 不同点:备份目标是云端对象存储而非 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.22 的"将本产品打包为 Docker 镜像部署"

十五、术语表

术语 说明
Warpgate / 盒子 本产品——部署在用户身边的 SSD 缓存代理设备
NAS 用户家中的网络存储设备Network Attached Storage
VFS rclone 的虚拟文件系统层Virtual File System将远程存储挂载为本地目录
FUSE 用户空间文件系统Filesystem in UserspaceLinux 内核机制,允许 rclone 在不修改内核的情况下提供文件系统挂载
FUSE 挂载点 rclone 通过 FUSE 暴露的本地目录(如 /mnt/nas-photos),应用通过它访问远程文件
dirty / 脏文件 本地已修改但尚未回写到远程 NAS 的文件
clean / 干净文件 与远程 NAS 保持一致的缓存文件
origin_mtime 文件上次与远程同步时远程的修改时间,用于检测远程变更
Write-back Controller 自定义的回写控制器,替代 rclone 原生的盲写逻辑,加入冲突检测
sparsebundle macOS 的稀疏包磁盘映像格式,由大量 8MB band 文件组成Time Machine 使用此格式
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的工作模式