操作日志即数据
每个账户一份 journal.log,一行一条 put / del。把日志从头重放一遍即为该账户的全部笔记与待办,不存在别的状态来源。
自建同步服务端 · 单个 Python 文件 · 仅标准库
Note, Nothing.
多账户、操作日志、可复用 ID、令牌鉴权与管理后台,并托管网页版客户端。 数据以「操作(put / del)」为单位追加到日志,把日志从头重放一遍即为全部数据。
主体为 sync.py,除 Python 3 标准库外没有依赖,不连接任何外部服务;
未配置凭据时同步接口一律返回 401。
服务端不保存以服务端为准的笔记副本 · 客户端只记录「已应用到第几号」
[INFO] [Server] 启动 EsprinSync v1 [INFO] [Server] listen=127.0.0.1:8686 hostSource=default portSource=default [INFO] [Sync] endpoint=/sync/* origin=http://127.0.0.1:8686 [INFO] [Admin] endpoint=/admin dir=/srv/esprin/manager api=/admin/api [INFO] [Auth] tokenSource=accounts accountCount=1 tokenCount=0
sync.py)adminjournal.log,一行一条01特性
服务端只有一份操作日志:一行一条 JSON,追加写入。删除写成明确的删除标记, 条目被彻底删除后连正文带历史一并抹掉,只留那一行。
每个账户一份 journal.log,一行一条 put / del。把日志从头重放一遍即为该账户的全部笔记与待办,不存在别的状态来源。
删除是一条带全局序号的删除标记。重放只会删除,不会把别的设备上的老副本推回来,也不会把已删除的条目重新生成。
条目彻底删除后,其 ID 进回收池供后续新建使用;被某台设备领走的 ID 会被占住 15 分钟,条目落盘即释放,两台设备同时新建不会撞号。
每条操作带 opId。客户端断线重试、重复提交同一个 opId 不会重复写入日志,服务端返回它原来的序号。
每个账户一套访问令牌,服务端只保存 HMAC-SHA256 摘要,明文仅在创建或重置时显示一次,并可与设备 id 绑定。
只接受数据目录内的相对路径,绝对路径与含 .. 的路径一律拒绝;每个凭据只能读写它所属账户的那一份日志。
服务端默认只有内置账户 admin,其余账户在面板中新建(名称 1~32 个字符、密码至少 8 位);各账户的笔记与待办互不可见。
/admin 下的一套静态页:账户、令牌、日志与密码。页面按请求读取后原样返回,改动页面无需改动或重启服务端。
启动时把 Web 仓库克隆到 web/ 并在根路径托管网页版;根路径留给客户端,同步接口在 /sync。
02同步模型
同步以「操作」为单位追加到日志,客户端只记录「已应用到第几号」, 同步过程即「拉取序号之后的操作并重放 + 推送本地改动」。
| 同步模型 | 客户端可见信息 | 删除的后果 |
|---|---|---|
| 文件快照 + 目录对比 | 远端存在 / 不存在某文件 | 「删除」与「从未见过」不可区分,其他设备会把本地缺失视为待上传,从而重新生成该文件 |
| 操作日志(本项目) | 逐条 put / del 操作及其全局序号 | 删除是一条明确的删除标记,重放只会删除,不会反向产生写回 |
逐条校验操作类型与路径,分配全局递增序号,以一行 JSON 追加写入并 fsync;同一 opId 重复提交直接返回原序号。
GET /sync/ops?since=&limit= 按序号分页返回,响应带上 latestSeq 与 hasMore;客户端据此记录进度。
条目被彻底删除后抹掉其正文与历史,只留一行删除标记;其余操作的序号不动,各设备记的进度照旧有效。管理后台也可手动整理。
03数据与日志
账户表、各账户的日志与令牌都在 --data 指向的目录里。备份该目录即为一次完整备份。
data/ ├── users.json # 账户表:名称、口令摘要、启用与管理员标记 ├── config.json # 可选:host 与 port └── users/ ├── admin/ │ ├── journal.log # 该账户的全部数据,一行一条 put / del │ ├── journal.id # 日志身份,客户端据此判断是否仍是同一份日志 │ └── tokens.json # 该账户的访问令牌,只存摘要 └── alice/ └── journal.log
{"seq": 128, "opId": "5f0c…", "device": "台式机",
"time": 1789840300633, "op": "put",
"path": "notes/aB3dE5fG7h.md",
"data": "<!--EsprinData … -->\n正文…",
"encoding": "utf8", "hash": "sha256:…"}
每个账户的日志目录下有一枚 journal.id,客户端据此判断「还是不是同一份日志」,避免把两份无关的数据接在一起。
账户口令以 PBKDF2-SHA256(20 万次迭代)保存,令牌只存 HMAC-SHA256 摘要,两者都不落明文。
旧版单账户布局(数据目录根下的 admin.json 与日志、令牌)在首次启动时自动并入内置账户,原文件归档为 admin.json.migrated。
04接口
根路径 / 留给网页版客户端,管理后台在 /admin(接口在 /admin/api),
同步接口在 /sync。
| 接口 | 凭据 | 说明 |
|---|---|---|
| GET /sync/health | 不需要 | 探活,用于区分「地址错」与「凭据错」;带凭据时额外报出该账户的日志身份与账户名 |
| GET /sync/ops?since=&limit= | 需要 | 取该序号之后的操作,默认 500 条、最多 2000 条;响应带 latestSeq 与 hasMore |
| POST /sync/ops | 需要 | 提交一批操作,逐条返回受理结果与序号;重复的 opId 返回原序号 |
| GET /sync/state | 需要 | 报出 latestSeq、操作总数、已删除条目数、回收池大小与日志身份 |
| GET /sync/file?path= | 需要 | 取单个条目的当前内容:正文、编码、哈希与它所在的序号 |
| GET /sync/ids?limit= | 需要 | 列出回收池中可复用的 ID,最近删除的排在前面 |
| POST /sync/ids/claim | 需要 | 领用可复用 ID(device / kind / count / since);序号没跟上的用 pending 报回 |
Authorization: Bearer)或账户登录会话;/sync/* 未携带凭据一律返回 401。
05管理后台
启动后在浏览器中打开「服务器地址 + /admin」。管理页是仓库 manager/ 下的静态文件,
由服务端按请求读取后原样返回。
界面示意,按管理页的实际分节列出
admin 不可删除、不可停用、不可取消管理员,也不能改名(它的名字同时是数据目录名)。
journal.log 的快照,这个账户的全部笔记与待办就在其中。
06部署
在存放数据的机器上运行即可。默认只监听本机;要给局域网内的设备使用,
把监听地址写成 0.0.0.0 并在防火墙上放行端口。
# 默认:127.0.0.1:8686,数据目录 ./data python sync.py # 局域网共享,并指定数据目录 python sync.py --host 0.0.0.0 --port 8686 --data ./esprin-data # 离线自测:覆盖接口、管理后台、账户、回收 ID、日志整理与旧版迁移,不监听端口 python sync.py --selftest
| 参数 | 说明 | 默认 |
|---|---|---|
| --host | 监听地址 | 127.0.0.1,或 config.json 中的 host |
| --port | 监听端口 | 8686,或 config.json 中的 port |
| --data | 数据目录:账户表与各账户的日志、令牌都在这里 | ./data |
| --token | 访问令牌,等同内置账户的一份令牌;不传则凭据只来自管理后台 | 环境变量 ESPRIN_TOKEN |
| --web-repo | 网页版客户端仓库,web/ 里没有页面时从这里克隆 |
https://github.com/EsprinProject/Web.git |
| --web-ref | 克隆该仓库的哪个分支或标签 | main |
| --web-update | 启动时对已有的克隆做一次 git pull --ff-only |
关闭 |
| --no-web-clone | 不克隆网页版客户端,只用现有的 web/ 目录 |
关闭 |
| --selftest | 跑一遍内置自测后退出 | 关闭 |
想让设置常驻而不改命令行,把 host 与 port 写进 config.json;查找顺序为数据目录、sync.py 所在目录、其下的 data/。命令行参数优先。
仓库带有 ecosystem.config.js,用 pm2 起名为 EsprinSync、以 python3 解释执行并自动重启;也可交给系统的服务管理器。
--selftest 顺次跑过配置、接口、网页版、管理后台、账户、强制鉴权、重载日志、回收 ID、整理日志、重载后台与旧版迁移各节,全部离线完成。
07接入
桌面端与网页端共用同一套文件格式与同一份操作日志,同一批数据可在两端之间互相接管。