diff --git a/src/password_manager/backup.py b/src/password_manager/backup.py index 817847b..10da249 100644 --- a/src/password_manager/backup.py +++ b/src/password_manager/backup.py @@ -54,6 +54,7 @@ class BackupManager: self.backup_dir = self.fingerprint_dir / "backups" self.backup_dir.mkdir(parents=True, exist_ok=True) self.index_file = self.fingerprint_dir / "seedpass_entries_db.json.enc" + self._last_backup_time = 0.0 logger.debug( f"BackupManager initialized with backup directory at {self.backup_dir}" ) @@ -71,7 +72,13 @@ class BackupManager: ) return - timestamp = int(time.time()) + now = time.time() + interval = self.config_manager.get_backup_interval() + if interval > 0 and now - self._last_backup_time < interval: + logger.info("Skipping backup due to interval throttle") + return + + timestamp = int(now) backup_filename = self.BACKUP_FILENAME_TEMPLATE.format(timestamp=timestamp) backup_file = self.backup_dir / backup_filename @@ -81,6 +88,7 @@ class BackupManager: print(colored(f"Backup created successfully at '{backup_file}'.", "green")) self._create_additional_backup(backup_file) + self._last_backup_time = now except Exception as e: logger.error(f"Failed to create backup: {e}", exc_info=True) print(colored(f"Error: Failed to create backup: {e}", "red")) diff --git a/src/password_manager/config_manager.py b/src/password_manager/config_manager.py index 68a154e..c2c23a1 100644 --- a/src/password_manager/config_manager.py +++ b/src/password_manager/config_manager.py @@ -46,6 +46,7 @@ class ConfigManager: "inactivity_timeout": INACTIVITY_TIMEOUT, "kdf_iterations": 100_000, "additional_backup_path": "", + "backup_interval": 0, "secret_mode_enabled": False, "clipboard_clear_delay": 45, } @@ -60,6 +61,7 @@ class ConfigManager: data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT) data.setdefault("kdf_iterations", 100_000) data.setdefault("additional_backup_path", "") + data.setdefault("backup_interval", 0) data.setdefault("secret_mode_enabled", False) data.setdefault("clipboard_clear_delay", 45) @@ -85,6 +87,7 @@ class ConfigManager: def save_config(self, config: dict) -> None: """Encrypt and save configuration.""" try: + config.setdefault("backup_interval", 0) self.vault.save_config(config) except Exception as exc: logger.error(f"Failed to save config: {exc}") @@ -187,3 +190,16 @@ class ConfigManager: """Retrieve clipboard clear delay in seconds.""" config = self.load_config(require_pin=False) return int(config.get("clipboard_clear_delay", 45)) + + def set_backup_interval(self, interval: int | float) -> None: + """Persist the minimum interval in seconds between automatic backups.""" + if interval < 0: + raise ValueError("Interval cannot be negative") + config = self.load_config(require_pin=False) + config["backup_interval"] = interval + self.save_config(config) + + def get_backup_interval(self) -> float: + """Retrieve the backup interval in seconds.""" + config = self.load_config(require_pin=False) + return float(config.get("backup_interval", 0)) diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index 92f46a5..5112f85 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -461,6 +461,7 @@ def config_set(ctx: typer.Context, key: str, value: str) -> None: "relays": lambda v: cfg.set_relays( [r.strip() for r in v.split(",") if r.strip()], require_pin=False ), + "backup_interval": lambda v: cfg.set_backup_interval(float(v)), } action = mapping.get(key) diff --git a/src/tests/test_backup_interval.py b/src/tests/test_backup_interval.py new file mode 100644 index 0000000..f7ce39a --- /dev/null +++ b/src/tests/test_backup_interval.py @@ -0,0 +1,34 @@ +import time +from pathlib import Path +from tempfile import TemporaryDirectory + +from helpers import create_vault, TEST_SEED, TEST_PASSWORD + +from password_manager.backup import BackupManager +from password_manager.config_manager import ConfigManager + + +def test_backup_interval(monkeypatch): + with TemporaryDirectory() as tmpdir: + fp_dir = Path(tmpdir) + vault, _ = create_vault(fp_dir, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, fp_dir) + cfg_mgr.set_backup_interval(10) + backup_mgr = BackupManager(fp_dir, cfg_mgr) + + vault.save_index({"entries": {}}) + + monkeypatch.setattr(time, "time", lambda: 1000) + backup_mgr.create_backup() + first = fp_dir / "backups" / "entries_db_backup_1000.json.enc" + assert first.exists() + + monkeypatch.setattr(time, "time", lambda: 1005) + backup_mgr.create_backup() + second = fp_dir / "backups" / "entries_db_backup_1005.json.enc" + assert not second.exists() + + monkeypatch.setattr(time, "time", lambda: 1012) + backup_mgr.create_backup() + third = fp_dir / "backups" / "entries_db_backup_1012.json.enc" + assert third.exists() diff --git a/src/tests/test_cli_config_set_extra.py b/src/tests/test_cli_config_set_extra.py index 21e8309..999df2e 100644 --- a/src/tests/test_cli_config_set_extra.py +++ b/src/tests/test_cli_config_set_extra.py @@ -14,6 +14,7 @@ runner = CliRunner() ("secret_mode_enabled", "true", "set_secret_mode_enabled", True), ("clipboard_clear_delay", "10", "set_clipboard_clear_delay", 10), ("additional_backup_path", "", "set_additional_backup_path", None), + ("backup_interval", "5", "set_backup_interval", 5.0), ( "relays", "wss://a.com, wss://b.com", diff --git a/src/tests/test_config_manager.py b/src/tests/test_config_manager.py index 385657d..799ea84 100644 --- a/src/tests/test_config_manager.py +++ b/src/tests/test_config_manager.py @@ -158,3 +158,14 @@ def test_kdf_iterations_round_trip(): cfg_mgr.set_kdf_iterations(200_000) assert cfg_mgr.get_kdf_iterations() == 200_000 + + +def test_backup_interval_round_trip(): + with TemporaryDirectory() as tmpdir: + vault, _ = create_vault(Path(tmpdir), TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, Path(tmpdir)) + + assert cfg_mgr.get_backup_interval() == 0 + + cfg_mgr.set_backup_interval(15) + assert cfg_mgr.get_backup_interval() == 15