文件
workbuddy-portal/backups
wangchuanli 1bf961f6b3 feat(权限): 收敛普通账号写权限至本人凭证
将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 Cookie 与 User-Agent。
新增 config.writable_by 作为唯一写权限入口,set_setting 强制全局键落到 user_id=0,
消除「管理员改了只有自己生效」的静默缺陷。新增 tools/check_docs.py 文档自检,
smoke 断言扩至 215 项、check_live 扩至 122 项并支持普通账号越权验收,
忽略 backups/、data/*.bak* 与 legacy-v1/,版本升至 v1.3.0。
2026-09-18 08:46:00 +08:00
..

backups/ — 数据库备份存放处

人工或脚本产生的数据库快照统一放这里。不要放进 data/,原因见下。

为什么备份不放在 data/

data/ 是运行时数据目录,在 Docker 部署下会被挂载成卷(workbuddy-portal_wb_data)。

  • 备份放进 data/ → 执行 docker compose down -v 或重建卷时,备份会跟着正本一起被删掉, 这正好是最需要备份的那一刻。
  • 备份放在仓库目录下的 backups/ → 卷重建不影响它,同时离源码够近、搬家不丢。

backups/ 下的一切都被 .gitignore 忽略(见仓库根 .gitignore 的「数据库备份」段), 绝不要提交:快照里含 settings 表的加密凭证密文与 users 表的密码哈希。

目录内容

文件 说明
usage.sqlite.bak-pre-v13 v1.2.0 → v1.3.0 迁移(DB_SCHEMA_VERSION 2 → 3)前的快照。保留用意:迁移同时做了「配置作用域收敛」(把调度与采集参数从个人级提升到实例级 user_id=0),万一收敛结果不符合预期,可回滚到这份 uv=2 的库重来。

校验记录(2026-09-16)

integrity_check : ok
user_version    : 2
usage_records   : 1665 条
SUM(credits)    : 8513.36

迁移后正本 data/usage.sqlite(uv=3)同样是 1665 条 / 8513.36 积分,零丢失。

快照本身是自包含的:全部数据都在主文件里(-wal 为 0 字节,没有未落盘的提交帧)。

但要注意一个会反复出现的现象:只要有人以 WAL 模式打开过这份快照, SQLite 就会就地重建 …-wal / …-shm 两个侧车文件 —— 连只读打开也会 (SQLite 需要 -shm 做锁表)。所以「移走一次」不是长久之计, 校验命令里加 immutable=1 才是根治(告诉 SQLite 这个文件不会变,不必建锁表)。

侧车只是运行时缓存,backups/ 已在 .gitignore 里被整体忽略, 不会误入库。归档或搬运快照前把两个侧车移走即可,前提是先确认 -wal 是 0 字节。

怎么用

校验一份备份是否可用(immutable=1 = 只读且不建锁表,不会改动也不会污染快照):

python -c "
import sqlite3
c = sqlite3.connect('file:backups/usage.sqlite.bak-pre-v13?immutable=1', uri=True)
print('integrity:', c.execute('PRAGMA integrity_check').fetchone()[0])
print('user_version:', c.execute('PRAGMA user_version').fetchone()[0])
print('records:', c.execute('SELECT COUNT(*) FROM usage_records').fetchone()[0])
"

备份文件的唯一权威判据是 integrity_check 与行数,别拿 sha256 当验签 —— 快照落盘后再被 SQLite 干净关闭过一次,主文件字节可能变化而内容完全一致。 校验只需 sqlite3 标准库,用你跑服务的同一个解释器即可。

回滚一份备份:停掉服务 → 备份当前正本 → 把快照覆盖回 data/usage.sqlite → 同时删除 data/usage.sqlite-wal 与 data/usage.sqlite-shm(残留的 WAL 会让 SQLite 读到旧状态) → 启动服务 → 跑 python manage.py status 与 python manage.py stats 确认账号与存档条数。

回滚前务必确认目标库的 user_version 与服务端 workbuddy_portal/db.py 的 DB_SCHEMA_VERSION 兼容:回滚到更老的版本号时,服务会在下次启动时重跑迁移。

自动备份(可选)

容器部署下推荐用宿主机的 cron 做,先落盘再压缩,避免 SQLite 在线拷贝产生撕裂快照:

# 每天 03:30,用 SQLite 自带的一致性备份命令(不是 cp!)
docker compose exec -T portal python -c "
import sqlite3
src = sqlite3.connect('/app/data/usage.sqlite')
dst = sqlite3.connect('/tmp/wb-backup.sqlite')
src.backup(dst); dst.close(); src.close()
"
docker compose cp portal:/tmp/wb-backup.sqlite \
  "./backups/usage-$(date +%Y%m%d).sqlite"
docker compose exec -T portal rm -f /tmp/wb-backup.sqlite

cp 一个正在被写入的 SQLite 文件可能拿到半截事务;sqlite3.Connection.backup() 走的是官方的在线备份 API,能保证快照一致。手工离线拷贝时才可以直接 cp。

清理策略

保留最近 7 份日备 + 每份迁移前快照。旧的直接删,别在这里堆积—— backups/ 是安全网,不是归档;数据正本永远只有 data/usage.sqlite 一个。