No description
Find a file
guochao c4a2fd1ac7 README:架构图纯 ASCII 化,goroutine 改 thread,移除 Windows 构建段;.envrc 转 ignore
- 架构图重写:只用 ASCII(原 Box-drawing/中文/全角箭头违例),术语修正
  goroutine -> thread(Rust 线程,非 Go 协程),并补上 on_request/webhook
  入口与 verify -> sync_log 流,与现状一致
- 删除 Windows 交叉构建独立段:命令已在 shell.nix 头注释,README 不重复
- .envrc 属个人 direnv 配置,转 ignore(shell.nix 保留为可复现工件;
  target 专属变量按三元组生效,不影响 native 构建)
2026-09-23 18:33:22 +08:00
scripts webhook.lua 推送驱动 provider:监听 HTTP,处理输入并同步 2026-09-23 15:35:20 +08:00
src webhook.lua 推送驱动 provider:监听 HTTP,处理输入并同步 2026-09-23 15:35:20 +08:00
.gitignore README:架构图纯 ASCII 化,goroutine 改 thread,移除 Windows 构建段;.envrc 转 ignore 2026-09-23 18:33:22 +08:00
Cargo.lock 单实例锁换 std File::try_lock:去 nix 依赖,跨平台兼容 Windows 2026-09-23 14:22:58 +08:00
Cargo.toml 单实例锁换 std File::try_lock:去 nix 依赖,跨平台兼容 Windows 2026-09-23 14:22:58 +08:00
config.example.toml webhook.lua 推送驱动 provider:监听 HTTP,处理输入并同步 2026-09-23 15:35:20 +08:00
README.md README:架构图纯 ASCII 化,goroutine 改 thread,移除 Windows 构建段;.envrc 转 ignore 2026-09-23 18:33:22 +08:00
shell.nix Windows 交叉编译:nix mingw 工具链 + 静态链出单文件 davsyncd.exe 2026-09-23 15:22:48 +08:00

davsyncd

单向 WebDAV 镜像工具。运行时是哑执行器,Lua 脚本是策略大脑:脚本感知源端变动、探测目标端、 编排动作列表,运行时只负责队列、比对、传输和凭据。

架构

signal source thread                         worker thread (only one)
+-----------------------------+              +---------------------------------+
| Lua VM (serialized)         |    emit      | coalescing queue (latest per    |
|  on_poll()   <- timer tick  |  --------->  | key), compare-then-sync         |
|  on_request() <- webhook    |   events     | mkcol / put / delete / move     |
|  change sensing + probing   |              | all ok -> manifest[key] = rev   |
|  action planning            |              | (verify -> sync_log)            |
+-----------------------------+              +---------------------------------+
shared: HTTP channel (Authorization injected per host) | manifest/state/db | config
  • 运行时零协议魔法:没有自动递归 MKCOL、没有自动临时名上传、不解释 ETag。 一个 provider 的全部知识(delta API、XML 解析、分页、限频、删除语义、目标端动作序列) 都在该 provider 的脚本里,由脚本作者显式写出。
  • 幂等底线:worker 在执行动作组前比对 event.rev == manifest[key],相同整组跳过; 动作任一失败即中止且不更新 manifest,事件重试最多 5 次后丢弃(下次 poll 重新感知)。
  • 凭据隔离:凭据只存在于配置;所有出站 HTTP 经过唯一通道 send(),按 host 精确匹配 注入 Authorization。脚本对已配置 host 自设该头会被拒绝。

脚本契约

function on_init(cfg, cold) end   -- cfg 为配置 [cfg] 表(自动附加 _dst = target_base)
                                  -- cold = manifest 缺失(自然触发全量)
function on_poll() end            -- 定时触发;有变动则 emit。可返回 { next = 秒 } 调节下次间隔(nil/缺省用 interval_secs;正值钳制 1s..1d,避免脚本异常值 busy loop 或长眠)
function on_verify(ev) end        -- 可选;动作组执行成功后回调 ev={key,rev}(在 worker 线程内执行)。结果按 Lua 真值判定:
                                  -- true/nil=通过;false/报错=不通过;可 return false, "原因" 附详情。结果只写入 davsyncd.db
                                  -- (verify_ok/verify_fail)供异步复核,不影响重试与 manifest;未定义则跳过校验
function on_request(req) end      -- webhook 模式(配置 http_listen 后启用);req={method,path,headers,body}
                                  -- 返回 {status, headers, body} 或 nil(204);emit 照常可用
emit{                             -- key 必须以 / 开头且唯一;rev 必填(不透明字符串)
  key  = "/dir/a.txt",
  rev  = "42",
  actions = {
    { op = "mkcol",  path = "/dir" },
    { op = "put",    path = "/dir/a.txt" },                        -- GET source_base+path → PUT target_base+path
    { op = "put",    path = "/x", as = "/.tmp/x" },                 -- 上传到临时名(与 move 组合做原子发布)
    { op = "put",    path = "/x", headers = { ["X-OC-Mtime"] = "1735000000" } },  -- 附加目标端请求头(mtime 保真等)
    { op = "move",   from = "/.tmp/x", to = "/x" },                -- 目标端 MOVE,带 Overwrite: T
    { op = "delete", path = "/old" },
  },
}

state.get(k) / state.set(k, v)    -- 脚本私有 kv(游标等),运行时持久化到 state.json
http.request{method=, url=, headers=, body=}   -- 原始 HTTP;返回 {status, headers, body}
http.decode(s)                    -- JSON 字符串 → Lua table(无语义翻译)
xml.decode(s)                     -- XML → 元素树 { tag = "名", attrs = {名=值}, [1..n] = 子元素/文本 };
                                  -- 前缀原样保留(d:href),实体/CDATA 已解码,叶子文本即 children[1]
sleep(sec)                        -- 线程级阻塞:on_poll 里限频用(不碍 worker);on_verify 里慎用(会阻塞 worker)
log(level, msg)
db.exec(sql, params?)              -- 写 SQLite(davsyncd.db,WAL);返回影响行数;params 为数组表按 ? 顺序绑定
db.query(sql, params?)             -- 读 SQLite;返回 { {列=值}, ... };可建自己的记录表供异步复核

队列语义:动作组按 emit 顺序执行;不同 key 保序,同 key 后到覆盖先到。因此 「同步进行中收到新变动」不需要任何特殊处理——新事件排队,旧事件靠幂等自然消化。

运行

cargo build --release
cp config.example.toml config.toml   # 填入 source/target/credentials
./target/release/davsyncd [config.toml]
  • manifest.json(key → rev,删除即冷启动全量)、state.json(脚本 cursor)与 davsyncd.db(SQLite/WAL 同步记录)写在进程工作目录。
  • davsyncd.db 的 sync_log 表由运行时逐事件写结局(ts/key/rev/status/op/err;status ∈ ok/exec_failed/dropped/verify_ok/verify_fail),外部复核进程可直接并发读它做异步处置;脚本亦可用 db.exec/query 建自己的记录表。
  • source_base / target_base 是逻辑路径根;脚本 emit 的 path 相对它们。
  • 配置 http_listen = "127.0.0.1:8787" 启用 webhook 监听;日志经 tracing,级别由 RUST_LOG 控制(默认 info)。
  • 遵循环境代理变量(http_proxy/no_proxy);对内网端点测试时用 env -u http_proxy -u https_proxy 剥离。

运行模式

./target/release/davsyncd [config.toml]              # daemon
./target/release/davsyncd --dry-run [config.toml]    # 动作只打印不执行、不落 manifest

推送模式(webhook):配置 http_listen + scripts/webhook.lua 示例——进程不轮询,POST /hook (JSON {"path":"/a.txt"},可选 "event":"delete")或 GET /hook?path=%2Fa.txt&event=delete 即探测 源端并同步;event 省略时按源端实况分流。配置 [cfg] token 后要求 x-token 头或 token 参数一致。

已知裁剪

  • 无优雅退出;依赖暴毙后重启自愈(幂等 + manifest + 原子写 + flock)
  • 全量扫描控频由脚本负责(示例脚本经 on_poll 返回 {next=6} 控制在坚果云 600/h 预算内)
  • 坚果云示例脚本未处理目录删除时子项顺序等边界(依赖 delta 事件顺序)
  • nextcloud.lua 已知局限:重命名只产生 file_changed(新路径),旧路径无事件,dest 会残留旧文件; 删除整目录时按目标端子树深度倒序清理,顺带清掉残留。事件源为 Activity API 轮询(filter=files + since 游标)