diff --git a/.backupignore b/.backupignore index 81c5e902..f606661b 100644 --- a/.backupignore +++ b/.backupignore @@ -3,6 +3,7 @@ # Edit this file to customize. Source defaults: handlers/ignore/patterns.py .backup_system/ +.backup/ .git/ .svn/ .hg/ @@ -26,3 +27,4 @@ dist/ *.log .ruff_cache/ .coverage +*logs \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a92378f..59e26ec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,14 @@ PyPI version — not the changelog header. the default. `pathspec` (pure-Python, cross-OS) declared. Verified by artifact (seedgo 100%, 220 tests incl. 26 new gitignore-parity tests) + live (a dotfile flows into the store, `!` negation re-includes end-to-end). +- **Backup store dir renamed `.backup_system/` → `.backup/`, dead `versions/` + removed (FPLAN-0269 follow-up).** The backup root is now `.backup/` (shorter, + coexists with `@flow`'s `.backup/processed_plans/`); the orphaned per-timestamp + `versions/` scaffold and the unused `build_versioned_path()` — both superseded + by the Phase-3 `versioned/` baseline+diff store — are gone. Drive sync confirmed + reading `.backup/versioned/` + `.backup/drive_tracker.json` via the shared + `backup_root()`. Verified by artifact (seedgo 100%, 220 tests) + live (a + throwaway project writes to `.backup/`, no `versions/` dir). ### Fixed diff --git a/src/aipass/backup/README.md b/src/aipass/backup/README.md index eb347d12..c81c37b9 100644 --- a/src/aipass/backup/README.md +++ b/src/aipass/backup/README.md @@ -13,7 +13,7 @@ ### What I Do - Back up any project directory on the system (not just AIPass projects) -- Each project owns its backup config (`.backup_system/`) and ignore patterns (`.backupignore`) +- Each project owns its backup config (`.backup/`) and ignore patterns (`.backupignore`) - Snapshot mode: full mirror copy - Versioned mode: incremental timestamped backups with automatic pruning - Project registry for name-based lookups (`backup snapshot @AIPass`) @@ -49,7 +49,7 @@ apps/ ├── ignore/ # .backupignore patterns + whitelist ├── json/ # JSON persistence, atomic writes, ops log ├── path/ # Backup path building - ├── project/ # Config, registry, setup (.backup_system/) + ├── project/ # Config, registry, setup (.backup/) ├── report/ # Result formatting ├── scan/ # Directory walking + filtering ├── state/ # Changelog, metadata, timestamps @@ -78,4 +78,4 @@ backup --version # Show version - @cli — Rich console output ### Provides To -- Any project on the PC — backups are project-owned (.backup_system/ in target root) +- Any project on the PC — backups are project-owned (.backup/ in target root) diff --git a/src/aipass/backup/apps/backup.py b/src/aipass/backup/apps/backup.py index a32b30d5..b80ed2c4 100644 --- a/src/aipass/backup/apps/backup.py +++ b/src/aipass/backup/apps/backup.py @@ -67,7 +67,7 @@ def print_help() -> None: console.print(" [green]snapshot[/green] Full mirror backup of a project") console.print(" [green]versioned[/green] Incremental timestamped backup") console.print(" [green]all[/green] Run snapshot then versioned in sequence") - console.print(" [green]register[/green] Register a project + scaffold its .backup_system/") + console.print(" [green]register[/green] Register a project + scaffold its .backup/") console.print(" [green]status[/green] Show backup info and recent history") console.print(" [green]settings[/green] View/edit backup settings") console.print(" [green]drive_sync[/green] Sync backups to the remote drive") diff --git a/src/aipass/backup/apps/handlers/drive/tracker.py b/src/aipass/backup/apps/handlers/drive/tracker.py index 2e3eb639..439b9a9a 100644 --- a/src/aipass/backup/apps/handlers/drive/tracker.py +++ b/src/aipass/backup/apps/handlers/drive/tracker.py @@ -10,7 +10,7 @@ Maintains a persistent mapping of local file paths to Drive metadata (file ID, mtime, size) so repeat syncs can skip unchanged files. -Tracker is stored at ``/.backup_system/drive_tracker.json``. +Tracker is stored at ``/.backup/drive_tracker.json``. """ from __future__ import annotations @@ -27,11 +27,13 @@ TRACKER_FILENAME = "drive_tracker.json" def _tracker_path(project_root: str) -> Path: """Return the tracker file path for a project.""" - return Path(project_root) / ".backup_system" / TRACKER_FILENAME + from ..path.builder import backup_root + + return backup_root(project_root) / TRACKER_FILENAME def load_tracker(project_root: str) -> dict: - """Load tracker from .backup_system/drive_tracker.json. + """Load tracker from .backup/drive_tracker.json. Returns: Dict keyed by relative file path with metadata values. @@ -46,7 +48,7 @@ def load_tracker(project_root: str) -> dict: def save_tracker(project_root: str, tracker: dict) -> None: - """Save tracker to .backup_system/drive_tracker.json.""" + """Save tracker to .backup/drive_tracker.json.""" path = _tracker_path(project_root) json_handler.save_json(str(path), tracker) json_handler.log_operation( diff --git a/src/aipass/backup/apps/handlers/ignore/patterns.py b/src/aipass/backup/apps/handlers/ignore/patterns.py index 9fbee8c8..f77ad88a 100644 --- a/src/aipass/backup/apps/handlers/ignore/patterns.py +++ b/src/aipass/backup/apps/handlers/ignore/patterns.py @@ -18,7 +18,7 @@ from ..json import json_handler from ..path import builder BUILTIN_IGNORES = [ - ".backup_system/", + ".backup/", ".git/", ".svn/", ".hg/", diff --git a/src/aipass/backup/apps/handlers/path/builder.py b/src/aipass/backup/apps/handlers/path/builder.py index 1914e50a..f2b4c465 100644 --- a/src/aipass/backup/apps/handlers/path/builder.py +++ b/src/aipass/backup/apps/handlers/path/builder.py @@ -9,38 +9,29 @@ """Path builder handler. Computes destination paths for backup modes. All paths are relative to the -target project's .backup_system/ directory. +target project's .backup/ directory. """ from pathlib import Path from ..json import json_handler -BACKUP_DIR = ".backup_system" +BACKUP_DIR = ".backup" def backup_root(project_root: str) -> Path: - """Return the .backup_system/ path for a project.""" + """Return the .backup/ path for a project.""" return Path(project_root) / BACKUP_DIR def build_snapshot_path(project_root: str) -> Path: - """Snapshot destination: /.backup_system/snapshots/""" + """Snapshot destination: /.backup/snapshots/""" json_handler.log_operation("build_snapshot_path", {"project_root": project_root}) return backup_root(project_root) / "snapshots" -def build_versioned_path(project_root: str, timestamp: str) -> Path: - """Versioned destination: /.backup_system/versions//""" - json_handler.log_operation( - "build_versioned_path", - {"project_root": project_root, "timestamp": timestamp}, - ) - return backup_root(project_root) / "versions" / timestamp - - def build_config_path(project_root: str) -> Path: - """Config file: /.backup_system/config.json""" + """Config file: /.backup/config.json""" return backup_root(project_root) / "config.json" @@ -50,22 +41,22 @@ def build_ignore_path(project_root: str) -> Path: def build_timestamps_path(project_root: str) -> Path: - """Timestamps file: /.backup_system/timestamps.json""" + """Timestamps file: /.backup/timestamps.json""" return backup_root(project_root) / "timestamps.json" def build_changelog_path(project_root: str) -> Path: - """Changelog file: /.backup_system/changelog.json""" + """Changelog file: /.backup/changelog.json""" return backup_root(project_root) / "changelog.json" def build_log_dir(project_root: str) -> Path: - """Log directory: /.backup_system/logs/""" + """Log directory: /.backup/logs/""" return backup_root(project_root) / "logs" def build_versioned_store(project_root: str) -> Path: - """Persistent versioned store: /.backup_system/versioned/""" + """Persistent versioned store: /.backup/versioned/""" json_handler.log_operation("build_versioned_store", {"project_root": project_root}) return backup_root(project_root) / "versioned" diff --git a/src/aipass/backup/apps/handlers/project/config.py b/src/aipass/backup/apps/handlers/project/config.py index 21672f6e..b72e4baf 100644 --- a/src/aipass/backup/apps/handlers/project/config.py +++ b/src/aipass/backup/apps/handlers/project/config.py @@ -8,7 +8,7 @@ """Project configuration handler. -Reads and writes the per-project ``.backup_system/config.json`` that stores mode +Reads and writes the per-project ``.backup/config.json`` that stores mode preferences, size limits, and drive-sync settings. """ diff --git a/src/aipass/backup/apps/handlers/project/setup.py b/src/aipass/backup/apps/handlers/project/setup.py index 7e43e5ec..e0aa3b24 100644 --- a/src/aipass/backup/apps/handlers/project/setup.py +++ b/src/aipass/backup/apps/handlers/project/setup.py @@ -1,6 +1,6 @@ # =================== AIPass ==================== # Name: setup.py -# Description: Project setup handler — scaffold .backup_system/ directory in target +# Description: Project setup handler — scaffold .backup/ directory in target # Version: 1.0.0 # Created: 2026-04-16 # Modified: 2026-04-23 @@ -8,7 +8,7 @@ """Project setup handler. -Creates the ``.backup_system/`` scaffold (config, snapshots/, versions/, logs/) +Creates the ``.backup/`` scaffold (config, snapshots/, logs/) inside a target project path, and a ``.backupignore`` at the project root. """ @@ -45,13 +45,13 @@ DEFAULT_CONFIG = { def create_backup_dir(project_path: str) -> Path | None: - """Create the ``.backup_system/`` scaffold inside a project path. + """Create the ``.backup/`` scaffold inside a project path. Args: project_path: Absolute filesystem path to the target project. Returns: - Path to the created ``.backup_system/`` directory, or None on failure. + Path to the created ``.backup/`` directory, or None on failure. """ root = Path(project_path) if not root.is_dir(): @@ -61,7 +61,6 @@ def create_backup_dir(project_path: str) -> Path | None: backup_dir = builder.backup_root(project_path) subdirs = [ backup_dir / "snapshots", - backup_dir / "versions", backup_dir / "logs", ] diff --git a/src/aipass/backup/apps/handlers/state/changelog.py b/src/aipass/backup/apps/handlers/state/changelog.py index 74e962a5..63a42352 100644 --- a/src/aipass/backup/apps/handlers/state/changelog.py +++ b/src/aipass/backup/apps/handlers/state/changelog.py @@ -9,7 +9,7 @@ """Changelog state handler. Appends and reads structured changelog entries describing each backup run -for a project. Stored at .backup_system/changelog.json. +for a project. Stored at .backup/changelog.json. """ from ..json import json_handler diff --git a/src/aipass/backup/apps/modules/register.py b/src/aipass/backup/apps/modules/register.py index 863c02a0..d7d28480 100644 --- a/src/aipass/backup/apps/modules/register.py +++ b/src/aipass/backup/apps/modules/register.py @@ -1,12 +1,12 @@ # =================== AIPass ==================== # Name: register.py -# Description: Register module — adds a project to backup and creates .backup_system/ +# Description: Register module — adds a project to backup and creates .backup/ # Version: 1.0.0 # Created: 2026-04-17 # Modified: 2026-04-23 # ============================================= -"""Register Module — register a project for backup and scaffold its .backup_system/.""" +"""Register Module — register a project for backup and scaffold its .backup/.""" import sys from pathlib import Path @@ -88,7 +88,7 @@ def handle_command(command: str, args: list) -> bool: backup_dir = create_backup_dir(project_path) if backup_dir is None: - console.print(f"[red]Error:[/red] Failed to create .backup_system/ in {project_path}") + console.print(f"[red]Error:[/red] Failed to create .backup/ in {project_path}") return True register_project(name, project_path) diff --git a/src/aipass/backup/tests/test_drive_pipeline.py b/src/aipass/backup/tests/test_drive_pipeline.py index 9c152b87..efa9c704 100644 --- a/src/aipass/backup/tests/test_drive_pipeline.py +++ b/src/aipass/backup/tests/test_drive_pipeline.py @@ -173,9 +173,7 @@ class TestDriveClient: mock_service = MagicMock() client._drive_service = mock_service - mod.api_call_with_retry = MagicMock( - return_value={"files": [{"id": "folder_123", "name": "AIPass Backups"}]} - ) + mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "folder_123", "name": "AIPass Backups"}]}) result = client.get_or_create_backup_folder() assert result == "folder_123" @@ -220,9 +218,7 @@ class TestDriveClient: client._drive_service = mock_service client.backup_folder_id = "root_folder" - mod.api_call_with_retry = MagicMock( - return_value={"files": [{"id": "proj_folder_789", "name": "myproject"}]} - ) + mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "proj_folder_789", "name": "myproject"}]}) result = client.get_or_create_project_folder("myproject") assert result == "proj_folder_789" @@ -472,7 +468,7 @@ class TestDriveTracker: mod = _fresh_import("aipass.backup.apps.handlers.drive.tracker") project = tmp_path / "project" project.mkdir() - backup_dir = project / ".backup_system" + backup_dir = project / ".backup" backup_dir.mkdir() result = mod.clear_all(str(project)) @@ -694,7 +690,7 @@ class TestDriveSync: """Empty versioned store -- skip upload.""" project = tmp_path / "project" project.mkdir() - bs = project / ".backup_system" / "versioned" + bs = project / ".backup" / "versioned" bs.mkdir(parents=True) mod = _fresh_import("aipass.backup.apps.modules.drive_sync") @@ -776,7 +772,7 @@ class TestDriveSync: """Files present -- upload called.""" project = tmp_path / "project" project.mkdir() - bs = project / ".backup_system" / "versioned" + bs = project / ".backup" / "versioned" bs.mkdir(parents=True) for i in range(3): @@ -1018,9 +1014,7 @@ class TestThreadSafety: client._drive_service = MagicMock() client.file_tracker = {"existing.txt": {"drive_id": "abc"}} - mod.api_call_with_retry = MagicMock( - return_value={"files": [{"id": "found_folder", "name": "AIPass Backups"}]} - ) + mod.api_call_with_retry = MagicMock(return_value={"files": [{"id": "found_folder", "name": "AIPass Backups"}]}) result = client.get_or_create_backup_folder() assert result == "found_folder" diff --git a/src/aipass/backup/tests/test_handlers_filesystem.py b/src/aipass/backup/tests/test_handlers_filesystem.py index bb10a85f..b4cf5834 100644 --- a/src/aipass/backup/tests/test_handlers_filesystem.py +++ b/src/aipass/backup/tests/test_handlers_filesystem.py @@ -117,12 +117,12 @@ class TestProjectSetup: """Test project setup -- creates_files, .exists(), mkdir, makedirs tokens.""" def test_create_backup_dir(self, tmp_path: Path) -> None: - """create_backup_dir creates .backup_system/ -- mkdir, .exists().""" + """create_backup_dir creates .backup/ -- mkdir, .exists().""" with patch("aipass.backup.apps.handlers.json.json_handler.log_operation"): from aipass.backup.apps.handlers.project.setup import create_backup_dir create_backup_dir(str(tmp_path)) - backup_dir = tmp_path / ".backup_system" + backup_dir = tmp_path / ".backup" assert backup_dir.exists() def test_create_backup_dir_idempotent(self, tmp_path: Path) -> None: @@ -132,7 +132,7 @@ class TestProjectSetup: create_backup_dir(str(tmp_path)) create_backup_dir(str(tmp_path)) - assert (tmp_path / ".backup_system").exists() + assert (tmp_path / ".backup").exists() class TestProjectConfig: @@ -161,16 +161,16 @@ class TestPathBuilder: """Test path builder handler -- module coverage for 'path' package.""" def test_backup_root(self, tmp_path: Path) -> None: - """backup_root returns .backup_system path.""" + """backup_root returns .backup path.""" with patch("aipass.backup.apps.handlers.json.json_handler.log_operation"): from aipass.backup.apps.handlers.path.builder import backup_root result = backup_root(str(tmp_path)) assert isinstance(result, Path) - assert result.name == ".backup_system" + assert result.name == ".backup" def test_build_snapshot_path(self, tmp_path: Path) -> None: - """build_snapshot_path returns snapshots/ under .backup_system.""" + """build_snapshot_path returns snapshots/ under .backup.""" with patch("aipass.backup.apps.handlers.json.json_handler.log_operation"): from aipass.backup.apps.handlers.path.builder import build_snapshot_path