Files
seedPass/src/password_manager/backup.py
2025-07-05 12:10:41 -04:00

191 lines
7.0 KiB
Python

# password_manager/backup.py
"""
Backup Manager Module
This module implements the BackupManager class, responsible for creating backups,
restoring from backups, and listing available backups for the encrypted password
index file. It ensures data integrity and provides mechanisms to recover from
corrupted or lost data by maintaining timestamped backups.
Ensure that all dependencies are installed and properly configured in your environment.
"""
import logging
import os
import shutil
import time
import traceback
from pathlib import Path
from termcolor import colored
from password_manager.config_manager import ConfigManager
from utils.file_lock import exclusive_lock
from constants import APP_DIR
# Instantiate the logger
logger = logging.getLogger(__name__)
class BackupManager:
"""
BackupManager Class
Handles the creation, restoration, and listing of backups for the encrypted password
index file. Backups are stored in the application directory with
timestamped filenames to facilitate easy identification and retrieval.
"""
BACKUP_FILENAME_TEMPLATE = "entries_db_backup_{timestamp}.json.enc"
def __init__(self, fingerprint_dir: Path, config_manager: ConfigManager):
"""Initialize BackupManager for a specific profile.
Parameters
----------
fingerprint_dir : Path
Directory for this profile.
config_manager : ConfigManager
Configuration manager used for retrieving settings.
"""
self.fingerprint_dir = fingerprint_dir
self.config_manager = config_manager
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"
logger.debug(
f"BackupManager initialized with backup directory at {self.backup_dir}"
)
def create_backup(self) -> None:
try:
index_file = self.index_file
if not index_file.exists():
logger.warning("Index file does not exist. No backup created.")
print(
colored(
"Warning: Index file does not exist. No backup created.",
"yellow",
)
)
return
timestamp = int(time.time())
backup_filename = self.BACKUP_FILENAME_TEMPLATE.format(timestamp=timestamp)
backup_file = self.backup_dir / backup_filename
shutil.copy2(index_file, backup_file)
logger.info(f"Backup created successfully at '{backup_file}'.")
print(colored(f"Backup created successfully at '{backup_file}'.", "green"))
self._create_additional_backup(backup_file)
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"))
def _create_additional_backup(self, backup_file: Path) -> None:
"""Write a copy of *backup_file* to the configured secondary location."""
path = self.config_manager.get_additional_backup_path()
if not path:
return
try:
dest_dir = Path(path).expanduser()
dest_dir.mkdir(parents=True, exist_ok=True)
dest_file = dest_dir / f"{self.fingerprint_dir.name}_{backup_file.name}"
shutil.copy2(backup_file, dest_file)
logger.info(f"Additional backup created at '{dest_file}'.")
except Exception as e: # pragma: no cover - best-effort logging
logger.error(
f"Failed to write additional backup to '{path}': {e}",
exc_info=True,
)
def restore_latest_backup(self) -> None:
try:
backup_files = sorted(
self.backup_dir.glob("entries_db_backup_*.json.enc"),
key=lambda x: x.stat().st_mtime,
reverse=True,
)
if not backup_files:
logger.error("No backup files found to restore.")
print(colored("Error: No backup files found to restore.", "red"))
return
latest_backup = backup_files[0]
index_file = self.index_file
shutil.copy2(latest_backup, index_file)
logger.info(f"Restored the index file from backup '{latest_backup}'.")
print(
colored(
f"Restored the index file from backup '{latest_backup}'.", "green"
)
)
except Exception as e:
logger.error(
f"Failed to restore from backup '{latest_backup}': {e}", exc_info=True
)
print(
colored(
f"Error: Failed to restore from backup '{latest_backup}': {e}",
"red",
)
)
def list_backups(self) -> None:
try:
backup_files = sorted(
self.backup_dir.glob("entries_db_backup_*.json.enc"),
key=lambda x: x.stat().st_mtime,
reverse=True,
)
if not backup_files:
logger.info("No backup files available.")
print(colored("No backup files available.", "yellow"))
return
print(colored("Available Backups:", "cyan"))
for backup in backup_files:
creation_time = time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime(backup.stat().st_mtime)
)
print(colored(f"- {backup.name} (Created on: {creation_time})", "cyan"))
except Exception as e:
logger.error(f"Failed to list backups: {e}", exc_info=True)
print(colored(f"Error: Failed to list backups: {e}", "red"))
def restore_backup_by_timestamp(self, timestamp: int) -> None:
backup_filename = self.BACKUP_FILENAME_TEMPLATE.format(timestamp=timestamp)
backup_file = self.backup_dir / backup_filename
if not backup_file.exists():
logger.error(f"No backup found with timestamp {timestamp}.")
print(colored(f"Error: No backup found with timestamp {timestamp}.", "red"))
return
try:
with exclusive_lock(backup_file) as fh_src, open(
self.index_file, "wb"
) as dst:
fh_src.seek(0)
shutil.copyfileobj(fh_src, dst)
logger.info(f"Restored the index file from backup '{backup_file}'.")
print(
colored(
f"Restored the index file from backup '{backup_file}'.", "green"
)
)
except Exception as e:
logger.error(
f"Failed to restore from backup '{backup_file}': {e}", exc_info=True
)
print(
colored(
f"Error: Failed to restore from backup '{backup_file}': {e}", "red"
)
)