# retoor import os import tempfile from pathlib import Path from devplacepy.services.backup import store def _make_backup(target, *, offloaded, missing_local=False, schedule_uid=""): job_uid = f"job-{store.generate_uid()}" uid = store.create_backup( target=target, created_by="test", job_uid=job_uid, schedule_uid=schedule_uid ) path = Path(tempfile.gettempdir()) / f"backup-store-test-{uid}.tar.gz" if not missing_local: path.write_bytes(b"x" * 10) store.finalize_backup( uid, filename=path.name, local_path=str(path), stats={"bytes_out": 10, "bytes_in": 10, "file_count": 1, "dir_count": 0, "sha256": "abc"}, ) if offloaded: store.mark_remote_uploaded(uid, f"remote:{target}/{path.name}") return uid, path def test_prune_orphans_spares_offloaded_backups_with_purged_local_copies(local_db): offloaded_uid, _ = _make_backup("database", offloaded=True) store.mark_local_purged(offloaded_uid) orphan_uid, _ = _make_backup("database", offloaded=False, missing_local=True) try: removed = store.prune_orphans() remaining = {row["uid"] for row in store.list_backups(limit=1000)} assert offloaded_uid in remaining assert orphan_uid not in remaining assert removed >= 1 finally: store.delete_backup(offloaded_uid) def test_rotate_schedule_never_removes_a_backup_without_a_confirmed_remote_copy(local_db): schedule_uid = store.create_schedule( name="test-rotate", target="database", kind="interval", every_seconds=86400, cron="", keep_last=1, created_by="test", next_run_at="2026-01-01T00:00:00", ) older_uid, _ = _make_backup("database", offloaded=False, schedule_uid=schedule_uid) newer_uid, _ = _make_backup("database", offloaded=True, schedule_uid=schedule_uid) try: removed = store.rotate_schedule(schedule_uid, keep_last=1) remaining = {row["uid"] for row in store.list_backups(limit=1000)} assert removed == 0 assert older_uid in remaining assert newer_uid in remaining finally: store.delete_backup(older_uid) store.delete_backup(newer_uid) def _point_storage_at(monkeypatch, root: Path): uploads = root / "uploads" attachments = uploads / "attachments" project_files = uploads / "project_files" keys = root / "keys" zips = root / "zips" deepsearch = root / "deepsearch" workspaces = root / "container_workspaces" backups = root / "backups" for path in ( attachments, project_files, keys, zips, deepsearch, workspaces, backups, ): path.mkdir(parents=True, exist_ok=True) db_file = root / "devplace.db" db_file.write_bytes(b"db") tasks = root / "devii_tasks.db" tasks.write_bytes(b"t") lessons = root / "devii_lessons.db" lessons.write_bytes(b"l") (project_files / "a.bin").write_bytes(b"x" * 10) (attachments / "b.bin").write_bytes(b"y" * 5) monkeypatch.setattr(store.config, "DATA_DIR", root) monkeypatch.setattr(store.config, "UPLOADS_DIR", uploads) monkeypatch.setattr(store.config, "ATTACHMENTS_DIR", attachments) monkeypatch.setattr(store.config, "PROJECT_FILES_DIR", project_files) monkeypatch.setattr(store.config, "KEYS_DIR", keys) monkeypatch.setattr(store.config, "ZIPS_DIR", zips) monkeypatch.setattr(store.config, "DEEPSEARCH_DIR", deepsearch) monkeypatch.setattr(store.config, "CONTAINER_WORKSPACES_DIR", workspaces) monkeypatch.setattr(store.config, "BACKUPS_DIR", backups) monkeypatch.setattr(store.config, "DEVII_TASKS_DB", tasks) monkeypatch.setattr(store.config, "DEVII_LESSONS_DB", lessons) monkeypatch.setattr(store.config, "DATABASE_URL", f"sqlite:///{db_file}") store.clear_storage_stats_cache() return { "uploads": uploads, "attachments": attachments, "project_files": project_files, "db_file": db_file, } def test_disk_usage_does_not_walk(monkeypatch, tmp_path): monkeypatch.setattr(store.config, "DATA_DIR", tmp_path) store.clear_storage_stats_cache() def boom(*_args, **_kwargs): raise AssertionError("disk_usage must not walk the data tree") monkeypatch.setattr(store.os, "walk", boom) disk = store.disk_usage() assert disk["total_bytes"] > 0 assert "used_percent" in disk def test_compute_storage_stats_walks_data_dir_once(local_db, monkeypatch, tmp_path): root = tmp_path / "data" _point_storage_at(monkeypatch, root) walks = [] real_walk = os.walk def counting_walk(path, *args, **kwargs): walks.append(os.path.realpath(path)) return real_walk(path, *args, **kwargs) monkeypatch.setattr(store.os, "walk", counting_walk) stats = store.compute_storage_stats() assert walks == [os.path.realpath(root)] by_key = {row["key"]: row for row in stats["paths"]} assert by_key["project_files"]["file_count"] == 1 assert by_key["project_files"]["size_bytes"] == 10 assert by_key["attachments"]["file_count"] == 1 assert by_key["uploads"]["file_count"] >= 2 assert by_key["database"]["file_count"] == 1 assert stats["data_dir"]["file_count"] >= 3 walks.clear() again = store.compute_storage_stats() assert walks == [] assert again["data_dir"]["file_count"] == stats["data_dir"]["file_count"]