fix(backup): rename store dir .backup_system to .backup + remove dead versions/ (FPLAN-0269 follow-up)
Backup root is now .backup/ via BACKUP_DIR (builder.py:19); tracker.py uses backup_root() not a hardcoded path; patterns.py BUILTIN_IGNORES + docstrings/README updated. Removed the orphaned per-timestamp versions/ scaffold (setup.py) and unused build_versioned_path() — both superseded by the Phase-3 versioned/ baseline+diff store. .backup/ coexists with flow's .backup/processed_plans/. Repo-root .backupignore now ignores both .backup/ and (until manual deletion) .backup_system/ (also carries Patrick's *logs rule). Verified by artifact (seedgo 100%, 220 tests) + live (throwaway writes to .backup/, no versions/, Drive reads .backup/versioned/). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a0016669b7
commit
c5a96cbdcf
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ``<project>/.backup_system/drive_tracker.json``.
|
||||
Tracker is stored at ``<project>/.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(
|
||||
|
||||
@@ -18,7 +18,7 @@ from ..json import json_handler
|
||||
from ..path import builder
|
||||
|
||||
BUILTIN_IGNORES = [
|
||||
".backup_system/",
|
||||
".backup/",
|
||||
".git/",
|
||||
".svn/",
|
||||
".hg/",
|
||||
|
||||
@@ -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: <project>/.backup_system/snapshots/"""
|
||||
"""Snapshot destination: <project>/.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: <project>/.backup_system/versions/<timestamp>/"""
|
||||
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: <project>/.backup_system/config.json"""
|
||||
"""Config file: <project>/.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: <project>/.backup_system/timestamps.json"""
|
||||
"""Timestamps file: <project>/.backup/timestamps.json"""
|
||||
return backup_root(project_root) / "timestamps.json"
|
||||
|
||||
|
||||
def build_changelog_path(project_root: str) -> Path:
|
||||
"""Changelog file: <project>/.backup_system/changelog.json"""
|
||||
"""Changelog file: <project>/.backup/changelog.json"""
|
||||
return backup_root(project_root) / "changelog.json"
|
||||
|
||||
|
||||
def build_log_dir(project_root: str) -> Path:
|
||||
"""Log directory: <project>/.backup_system/logs/"""
|
||||
"""Log directory: <project>/.backup/logs/"""
|
||||
return backup_root(project_root) / "logs"
|
||||
|
||||
|
||||
def build_versioned_store(project_root: str) -> Path:
|
||||
"""Persistent versioned store: <project>/.backup_system/versioned/"""
|
||||
"""Persistent versioned store: <project>/.backup/versioned/"""
|
||||
json_handler.log_operation("build_versioned_store", {"project_root": project_root})
|
||||
return backup_root(project_root) / "versioned"
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user