Merge pull request #119 from PR0M3TH3AN/codex/implement-export/import-database-features

Add portable backup export/import CLI
This commit is contained in:
thePR0M3TH3AN
2025-07-01 21:35:17 -04:00
committed by GitHub
3 changed files with 115 additions and 3 deletions

View File

@@ -13,6 +13,7 @@ from termcolor import colored
import traceback
from password_manager.manager import PasswordManager
from password_manager.portable_backup import PortableMode
from nostr.client import NostrClient
from constants import INACTIVITY_TIMEOUT
from utils.key_derivation import EncryptionMode
@@ -457,8 +458,10 @@ def handle_settings(password_manager: PasswordManager) -> None:
print("3. Change password")
print("4. Verify Script Checksum")
print("5. Backup Parent Seed")
print("6. Lock Vault")
print("7. Back")
print("6. Export database")
print("7. Import database")
print("8. Lock Vault")
print("9. Back")
choice = input("Select an option: ").strip()
if choice == "1":
handle_profiles_menu(password_manager)
@@ -471,10 +474,16 @@ def handle_settings(password_manager: PasswordManager) -> None:
elif choice == "5":
password_manager.handle_backup_reveal_parent_seed()
elif choice == "6":
password_manager.handle_export_database()
elif choice == "7":
path = input("Enter path to backup file: ").strip()
if path:
password_manager.handle_import_database(Path(path))
elif choice == "8":
password_manager.lock_vault()
print(colored("Vault locked. Please re-enter your password.", "yellow"))
password_manager.unlock_vault()
elif choice == "7":
elif choice == "9":
break
else:
print(colored("Invalid choice.", "red"))
@@ -565,11 +574,25 @@ if __name__ == "__main__":
# Load config from disk and parse command-line arguments
cfg = load_global_config()
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command")
parser.add_argument(
"--encryption-mode",
choices=[m.value for m in EncryptionMode],
help="Select encryption mode",
)
exp = sub.add_parser("export")
exp.add_argument(
"--mode",
choices=[m.value for m in PortableMode],
default=PortableMode.SEED_ONLY.value,
)
exp.add_argument("--file")
imp = sub.add_parser("import")
imp.add_argument("--file")
args = parser.parse_args()
mode_value = cfg.get("encryption_mode", EncryptionMode.SEED_ONLY.value)
@@ -591,6 +614,14 @@ if __name__ == "__main__":
print(colored(f"Error: Failed to initialize PasswordManager: {e}", "red"))
sys.exit(1)
if args.command == "export":
mode = PortableMode(args.mode)
password_manager.handle_export_database(mode, Path(args.file))
sys.exit(0)
elif args.command == "import":
password_manager.handle_import_database(Path(args.file))
sys.exit(0)
# Register signal handlers for graceful shutdown
def signal_handler(sig, frame):
"""

View File

@@ -24,6 +24,11 @@ from password_manager.entry_management import EntryManager
from password_manager.password_generation import PasswordGenerator
from password_manager.backup import BackupManager
from password_manager.vault import Vault
from password_manager.portable_backup import (
export_backup,
import_backup,
PortableMode,
)
from utils.key_derivation import (
derive_key_from_parent_seed,
derive_key_from_password,
@@ -1137,6 +1142,35 @@ class PasswordManager:
logging.error(f"Failed to restore backup: {e}", exc_info=True)
print(colored(f"Error: Failed to restore backup: {e}", "red"))
def handle_export_database(
self,
mode: "PortableMode" = PortableMode.SEED_ONLY,
dest: Path | None = None,
) -> Path | None:
"""Export the current database to an encrypted portable file."""
try:
path = export_backup(
self.vault,
self.backup_manager,
mode,
dest,
)
print(colored(f"Database exported to '{path}'.", "green"))
return path
except Exception as e:
logging.error(f"Failed to export database: {e}", exc_info=True)
print(colored(f"Error: Failed to export database: {e}", "red"))
return None
def handle_import_database(self, src: Path) -> None:
"""Import a portable database file, replacing the current index."""
try:
import_backup(self.vault, self.backup_manager, src)
print(colored("Database imported successfully.", "green"))
except Exception as e:
logging.error(f"Failed to import database: {e}", exc_info=True)
print(colored(f"Error: Failed to import database: {e}", "red"))
def handle_backup_reveal_parent_seed(self) -> None:
"""
Handles the backup and reveal of the parent seed.

View File

@@ -0,0 +1,47 @@
import sys
from pathlib import Path
import runpy
import pytest
sys.path.append(str(Path(__file__).resolve().parents[1]))
import main
from password_manager.portable_backup import PortableMode
from password_manager.manager import PasswordManager
def _run(argv, monkeypatch):
monkeypatch.setattr(sys, "argv", ["seedpass"] + argv)
monkeypatch.setattr(main, "load_global_config", lambda: {})
called = {}
def fake_init(self, encryption_mode):
called["init"] = True
def fake_export(self, mode, dest):
called["export"] = (mode, Path(dest))
def fake_import(self, src):
called["import"] = Path(src)
monkeypatch.setattr(PasswordManager, "__init__", fake_init)
monkeypatch.setattr(PasswordManager, "handle_export_database", fake_export)
monkeypatch.setattr(PasswordManager, "handle_import_database", fake_import)
with pytest.raises(SystemExit):
runpy.run_module("main", run_name="__main__")
return called
def test_export_command_invokes_handler(monkeypatch):
called = _run(["export", "--mode", "pw-only", "--file", "out.json"], monkeypatch)
assert called["export"] == (PortableMode.PW_ONLY, Path("out.json"))
assert "import" not in called
def test_import_command_invokes_handler(monkeypatch):
called = _run(["import", "--file", "backup.json"], monkeypatch)
assert called["import"] == Path("backup.json")
assert "export" not in called