复核 R-d4(升级前的 SQLite 备份只拷了应用库和断点库):填报的记录表 records/**/*.db 也用 SQLite 的备份接口拷进同一份备份,相对路径照旧,目录 0700、文件 0600;库在 PostgreSQL 时不做这份(记录表跟着整目录备份)。deploy/README 写清这份备份含什么、不含什么(上传原件和版本库、工件库、附件、知识库、沙箱工作区、.secret_key 要整目录备份),恢复步骤加上记录表

This commit is contained in:
ThearchyHelios
2026-10-19 16:12:07 +08:00
parent 676c546cbb
commit 65d35f6c97
3 changed files with 60 additions and 9 deletions
+23 -7
View File
@@ -10,8 +10,9 @@
用的仍是 SQLite,只是让下一次 migrate-to-postgres 认得出数据卷里那半份是自己上次留下的。
--container / --volume / --network 记下这个数据目录的内置库用的名字;第一次记时生成部署编号 id
(deploy.sh 把它打在库的容器、数据卷、网络的标签上,按它认哪些是这个部署的)。--get 键 打印一项
backup-sqlite 拷一份 agentlab.db、checkpoints.db 到 <数据目录>/backups/sqlite/<时间>/(SQLite 的备份接口,一致的一份),
只留最近 --keep 份
backup-sqlite 拷一份 agentlab.db、checkpoints.db 和填报的记录表 records/**/*.db 到 <数据目录>/backups/sqlite/<时间>/
(SQLite 的备份接口,各自一致的一份,记录表保持 records/ 下的相对路径),只留最近 --keep 份。上传原件、
上传表格的版本库、工件、附件、沙箱工作区不在里面:那些要整个数据目录一起备份
backup-dir 建(或收紧)备份目录:从 backups/ 起每一层 0700(pg_dump 写进去之前用)
prune 目录里按名字排序只留最近 --keep 个(pg_dump 的备份用)
@@ -172,18 +173,33 @@ def prune(folder: Path, keep: int) -> list[Path]:
return gone
def _sqlite_sources(data: Path) -> list[Path]:
"""要备份的 SQLite 文件(相对数据目录):应用库、断点库,和填报的记录表(records/ 下,原地追加写,开着服务直接拷可能拷到
写了一半的,所以也走备份接口)。"""
found = [Path(name) for name in ("agentlab.db", "checkpoints.db") if (data / name).is_file()]
records = data / "records"
if records.is_dir():
found += sorted(p.relative_to(data) for p in records.rglob("*.db") if p.is_file())
return found
def backup_sqlite(data: Path, *, keep: int) -> Path | None:
"""agentlab.db、checkpoints.db 拷到 backups/sqlite/<时间>/。两个都没有(库在 PostgreSQL)交回 None。"""
sources = [p for p in (data / "agentlab.db", data / "checkpoints.db") if p.is_file()]
if not sources:
"""agentlab.db、checkpoints.db、records/**/*.db 拷到 backups/sqlite/<时间>/(相对路径照旧)。应用库和断点库都没有(库在
PostgreSQL)交回 None——记录表那时由整目录备份管。"""
sources = _sqlite_sources(data)
if not any(p.parent == Path(".") for p in sources):
return None
with _private_umask():
root = backup_dir(data, "sqlite")
target = root / _stamp()
target.mkdir(exist_ok=True)
target.chmod(BACKUP_DIR_MODE)
for src in sources:
_copy_sqlite(src, target / src.name)
for rel in sources:
dst = target / rel
for folder in reversed(dst.relative_to(target).parents[:-1]):
(target / folder).mkdir(exist_ok=True)
(target / folder).chmod(BACKUP_DIR_MODE)
_copy_sqlite(data / rel, dst)
prune(root, keep)
return target
+32
View File
@@ -135,6 +135,38 @@ def test_backup_sqlite_copies_a_consistent_snapshot_including_the_wal(tmp_path,
assert len(list(target.iterdir())) == 2 and not (target / "20200101T000000Z").exists()
def test_backup_sqlite_also_copies_the_record_tables(tmp_path):
"""复核 R-d4:填报的记录表 records/**/*.db(原地追加写的 SQLite)也用备份接口拷进同一份备份,相对路径照旧。"""
sqlite3.connect(tmp_path / "agentlab.db").close()
(tmp_path / "records" / "nested").mkdir(parents=True)
rec = sqlite3.connect(tmp_path / "records" / "sales.v1.db")
rec.execute("PRAGMA journal_mode=WAL")
rec.execute("CREATE TABLE records (v TEXT)")
rec.execute("INSERT INTO records VALUES ('还在 WAL 里的一行')")
rec.commit()
sqlite3.connect(tmp_path / "records" / "nested" / "visits.v2.db").close()
(tmp_path / "records" / "notes.txt").write_text("不是库,不拷")
try:
target = ops.backup_sqlite(tmp_path, keep=5)
finally:
rec.close()
copied = sorted(str(p.relative_to(target)) for p in target.rglob("*") if p.is_file())
assert copied == ["agentlab.db", "records/nested/visits.v2.db", "records/sales.v1.db"]
db = sqlite3.connect(target / "records" / "sales.v1.db")
assert db.execute("SELECT v FROM records").fetchall() == [("还在 WAL 里的一行",)]
db.close()
for folder in (target / "records", target / "records" / "nested"):
assert _mode(folder) == 0o700
assert _mode(target / "records" / "sales.v1.db") == 0o600
def test_record_tables_alone_are_left_to_the_whole_directory_backup(tmp_path):
"""库在 PostgreSQL(没有 agentlab.db、checkpoints.db)时不做这份备份:记录表跟着整个数据目录备份。"""
(tmp_path / "records").mkdir()
sqlite3.connect(tmp_path / "records" / "sales.v1.db").close()
assert ops.backup_sqlite(tmp_path, keep=5) is None
def test_backup_dir_creates_and_tightens_every_level(tmp_path, capsys):
(tmp_path / "backups").mkdir(mode=0o755)
(tmp_path / "backups").chmod(0o755)
+5 -2
View File
@@ -66,10 +66,12 @@ deploy.sh 依次:检查 docker 和架构 → 导入镜像(`docker load -i im
另用 `pg_dump` 备份数据库。
`./deploy.sh upgrade` 换版本之前会自动备份一次库:SQLite 拷到数据目录的 `backups/sqlite/<时间>/`(SQLite 的备份接口,
一致的一份),内置 PostgreSQL 用 `pg_dump` 存到 `backups/postgres/`,各只留最近 5 份(`agentlab.env` 里 `BACKUP_KEEP=` 改)。
各自一致的一份:`agentlab.db`、`checkpoints.db`,和填报的记录表 `records/` 下的各个 `.db`,相对路径照旧),内置 PostgreSQL
用 `pg_dump` 存到 `backups/postgres/`(整个库,含断点),各只留最近 5 份(`agentlab.env` 里 `BACKUP_KEEP=` 改)。
备份里是整个库(含加密存着的密钥),所以 `backups/` 和下面每一层目录都是 0700、备份文件 0600,属主同数据目录(uid 10001):
宿主机上要看、要拷走都得用 sudo(`sudo ls -l data/backups/sqlite/`、`sudo cp -a data/backups /别处/`)。
这只是升级的保险,不代替下面的整目录备份:工件、上传文件和 `.secret_key` 不在里面。
这只是升级的保险,不代替下面的整目录备份:上传的原件和上传表格的各版本库、工件库、表单附件、知识库、沙箱工作区、加密密钥
`.secret_key` 都不在里面(用内置 PostgreSQL 时记录表也不在 `pg_dump` 里,它们是数据目录里的文件)。
用 SQLite 的部署要从 `backups/sqlite/` 恢复(以缺省的 `./data` 为例,`<时间>` 换成目录名):
@@ -77,6 +79,7 @@ deploy.sh 依次:检查 docker 和架构 → 导入镜像(`docker load -i im
./deploy.sh stop
sudo rm -f data/agentlab.db-wal data/agentlab.db-shm data/checkpoints.db-wal data/checkpoints.db-shm
sudo cp -p data/backups/sqlite/<时间>/agentlab.db data/backups/sqlite/<时间>/checkpoints.db data/ # -p:属主仍是 10001
sudo cp -rp data/backups/sqlite/<时间>/records/. data/records/ # 记录表(有的话),同样先删掉它们的 -wal、-shm
./deploy.sh start
```