No description
- 架构图重写:只用 ASCII(原 Box-drawing/中文/全角箭头违例),术语修正 goroutine -> thread(Rust 线程,非 Go 协程),并补上 on_request/webhook 入口与 verify -> sync_log 流,与现状一致 - 删除 Windows 交叉构建独立段:命令已在 shell.nix 头注释,README 不重复 - .envrc 属个人 direnv 配置,转 ignore(shell.nix 保留为可复现工件; target 专属变量按三元组生效,不影响 native 构建) |
||
|---|---|---|
| scripts | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| config.example.toml | ||
| README.md | ||
| shell.nix | ||
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 游标)