文件
workbuddy-portal/backups/README.md
T
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

92 行
4.3 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 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` = 只读且不建锁表,**不会改动也不会污染快照**):
```bash
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 在线拷贝产生撕裂快照:
```bash
# 每天 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` 一个。