fix(backup): stage SQLite snapshots beside output zip in pre-update path too
The pre-update / pre-migration backup path (_write_full_zip_backup) had the same /tmp staging bug as run_backup: a small tmpfs at the default tempfile location silently drops large *.db files from the archive. Route its SQLite staging temp files to the output zip's directory as well, and add regression tests (mutation-verified) for both staging paths. Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
This commit is contained in:
co-authored by
liuhao1024
parent
dd40600e0a
commit
ed2b9e43c8
@@ -870,7 +870,13 @@ def _write_full_zip_backup(out_path: Path, hermes_root: Path) -> Optional[Path]:
|
|||||||
for abs_path, rel_path in files_to_add:
|
for abs_path, rel_path in files_to_add:
|
||||||
try:
|
try:
|
||||||
if abs_path.suffix == ".db":
|
if abs_path.suffix == ".db":
|
||||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
|
# Stage the snapshot alongside the output zip so that the
|
||||||
|
# temp file lives on the same filesystem. The system
|
||||||
|
# default (/tmp) may be a small tmpfs that cannot hold
|
||||||
|
# large databases, causing silent backup incompleteness.
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
suffix=".db", delete=False, dir=str(out_path.parent)
|
||||||
|
) as tmp:
|
||||||
tmp_db = Path(tmp.name)
|
tmp_db = Path(tmp.name)
|
||||||
try:
|
try:
|
||||||
if _safe_copy_db(abs_path, tmp_db):
|
if _safe_copy_db(abs_path, tmp_db):
|
||||||
|
|||||||
@@ -192,6 +192,66 @@ class TestBackup:
|
|||||||
# Skins
|
# Skins
|
||||||
assert "skins/cyber.yaml" in names
|
assert "skins/cyber.yaml" in names
|
||||||
|
|
||||||
|
def test_db_snapshots_staged_beside_output_zip(self, tmp_path, monkeypatch):
|
||||||
|
"""SQLite staging temp files must be created on the output zip's
|
||||||
|
filesystem (dir=out_path.parent), NOT the system /tmp default — a
|
||||||
|
small tmpfs there silently drops large DBs from the backup (#35376)."""
|
||||||
|
hermes_home = tmp_path / ".hermes"
|
||||||
|
hermes_home.mkdir()
|
||||||
|
_make_hermes_tree(hermes_home)
|
||||||
|
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||||
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
|
||||||
|
out_dir = tmp_path / "external-drive"
|
||||||
|
out_dir.mkdir()
|
||||||
|
out_zip = out_dir / "backup.zip"
|
||||||
|
args = Namespace(output=str(out_zip))
|
||||||
|
|
||||||
|
import hermes_cli.backup as backup_mod
|
||||||
|
staged_dirs = []
|
||||||
|
real_ntf = backup_mod.tempfile.NamedTemporaryFile
|
||||||
|
|
||||||
|
def _spy(*a, **kw):
|
||||||
|
staged_dirs.append(kw.get("dir"))
|
||||||
|
return real_ntf(*a, **kw)
|
||||||
|
|
||||||
|
monkeypatch.setattr(backup_mod.tempfile, "NamedTemporaryFile", _spy)
|
||||||
|
backup_mod.run_backup(args)
|
||||||
|
|
||||||
|
# At least one .db was staged, and every staging call targeted the
|
||||||
|
# output zip's directory rather than the system temp default.
|
||||||
|
assert staged_dirs, "no SQLite snapshot was staged"
|
||||||
|
assert all(d == str(out_dir) for d in staged_dirs), staged_dirs
|
||||||
|
|
||||||
|
def test_pre_update_db_snapshots_staged_beside_output_zip(self, tmp_path, monkeypatch):
|
||||||
|
"""The pre-update/pre-migration zip path (_write_full_zip_backup) must
|
||||||
|
also stage SQLite snapshots beside its output zip, not in /tmp."""
|
||||||
|
hermes_home = tmp_path / ".hermes"
|
||||||
|
hermes_home.mkdir()
|
||||||
|
_make_hermes_tree(hermes_home)
|
||||||
|
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||||
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||||
|
|
||||||
|
out_zip = hermes_home / "backups" / "pre-update-test.zip"
|
||||||
|
out_zip.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
import hermes_cli.backup as backup_mod
|
||||||
|
staged_dirs = []
|
||||||
|
real_ntf = backup_mod.tempfile.NamedTemporaryFile
|
||||||
|
|
||||||
|
def _spy(*a, **kw):
|
||||||
|
staged_dirs.append(kw.get("dir"))
|
||||||
|
return real_ntf(*a, **kw)
|
||||||
|
|
||||||
|
monkeypatch.setattr(backup_mod.tempfile, "NamedTemporaryFile", _spy)
|
||||||
|
result = backup_mod._write_full_zip_backup(out_zip, hermes_home)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert staged_dirs, "no SQLite snapshot was staged"
|
||||||
|
assert all(d == str(out_zip.parent) for d in staged_dirs), staged_dirs
|
||||||
|
|
||||||
def test_excludes_hermes_agent(self, tmp_path, monkeypatch):
|
def test_excludes_hermes_agent(self, tmp_path, monkeypatch):
|
||||||
"""Backup does NOT include hermes-agent/ directory."""
|
"""Backup does NOT include hermes-agent/ directory."""
|
||||||
hermes_home = tmp_path / ".hermes"
|
hermes_home = tmp_path / ".hermes"
|
||||||
|
|||||||
Reference in New Issue
Block a user