diff --git a/README.md b/README.md index 8140118..263d1bc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

# DockerVault diff --git a/dockervault/cli.py b/dockervault/cli.py index c93d4e7..e69de29 100644 --- a/dockervault/cli.py +++ b/dockervault/cli.py @@ -1,322 +0,0 @@ -import argparse -import json -import sys -from pathlib import Path - -from . import __version__ - - -def entry_to_dict(entry): - if isinstance(entry, dict): - data = dict(entry) - else: - data = {} - for attr in ( - "path", - "source", - "source_path", - "host_path", - "mount_source", - "classification", - "service", - "target", - "exists", - "reason", - "compose_file", - ): - if hasattr(entry, attr): - data[attr] = getattr(entry, attr) - - real_path = ( - data.get("path") - or data.get("source") - or data.get("source_path") - or data.get("host_path") - or data.get("mount_source") - ) - - if real_path is not None: - real_path = str(real_path) - - compose_file = data.get("compose_file") - if compose_file is not None: - compose_file = str(compose_file) - - return { - "path": real_path, - "classification": data.get("classification"), - "service": data.get("service"), - "target": data.get("target"), - "exists": data.get("exists"), - "reason": data.get("reason"), - "compose_file": compose_file, - "source": str(data.get("source")) if data.get("source") is not None else None, - } - - -def normalize_entries(entries): - normalized = [] - for entry in entries: - item = entry_to_dict(entry) - - classification = item.get("classification", "review") - if classification == "critical": - bucket = "include" - elif classification == "skip": - bucket = "skip" - else: - bucket = "review" - - normalized.append( - { - "path": item.get("path") or "?", - "class": classification, - "bucket": bucket, - "service": item.get("service", "?"), - "target": item.get("target", "?"), - "exists": item.get("exists", True), - "reason": item.get("reason"), - "compose_file": item.get("compose_file"), - "source": item.get("source"), - } - ) - return normalized - - -def build_plan(scan_root, entries, compose_count): - normalized = normalize_entries(entries) - - return { - "root": str(scan_root), - "compose_files_found": compose_count, - "include": [e for e in normalized if e["bucket"] == "include"], - "review": [e for e in normalized if e["bucket"] == "review"], - "skip": [e for e in normalized if e["bucket"] == "skip"], - "summary": { - "include_count": sum(1 for e in normalized if e["bucket"] == "include"), - "review_count": sum(1 for e in normalized if e["bucket"] == "review"), - "skip_count": sum(1 for e in normalized if e["bucket"] == "skip"), - "missing_critical_count": sum( - 1 - for e in normalized - if e["bucket"] == "include" and not e.get("exists", True) - ), - }, - } - - -def print_plan(plan): - print("\nDockerVault Backup Plan") - print("=======================") - print(f"Scan root: {plan['root']}") - print(f"Compose files found: {plan.get('compose_files_found', 0)}") - - print("\nINCLUDE PATHS:") - if plan["include"]: - for item in plan["include"]: - extra = f" reason={item['reason']}" if item.get("reason") else "" - print( - f" - {item['path']} [{item['class']}] " - f"service={item['service']} target={item['target']}{extra}" - ) - else: - print(" - (none)") - - print("\nREVIEW PATHS:") - if plan["review"]: - for item in plan["review"]: - extra = f" reason={item['reason']}" if item.get("reason") else "" - print( - f" - {item['path']} [{item['class']}] " - f"service={item['service']} target={item['target']}{extra}" - ) - else: - print(" - (none)") - - print("\nSKIP PATHS:") - if plan["skip"]: - for item in plan["skip"]: - extra = f" reason={item['reason']}" if item.get("reason") else "" - print( - f" - {item['path']} [{item['class']}] " - f"service={item['service']} target={item['target']}{extra}" - ) - else: - print(" - (none)") - - -def print_warnings(plan): - missing = [item for item in plan["include"] if not item.get("exists", True)] - if missing: - print("\nWARNING: Missing critical paths detected") - for item in missing: - print(f" - {item['path']} (service={item['service']})") - - -def build_json_output(plan, borg_command=None): - output = { - "tool": "DockerVault", - "version": __version__, - "plan": plan, - "status": { - "ok": True, - "has_missing_critical": any( - not item.get("exists", True) for item in plan["include"] - ), - }, - } - - if borg_command is not None: - output["borg_command"] = borg_command - - return output - - -def build_parser(): - parser = argparse.ArgumentParser( - description="DockerVault - Intelligent Docker backup discovery" - ) - - parser.add_argument( - "--version", - action="version", - version=f"DockerVault {__version__}", - ) - - parser.add_argument( - "path", - nargs="?", - help="Path to scan (folder or docker-compose.yml)", - ) - - parser.add_argument( - "--repo", - help="Borg repository path", - ) - - parser.add_argument( - "--borg", - action="store_true", - help="Generate borg command", - ) - - parser.add_argument( - "--json", - action="store_true", - help="Output machine-readable JSON", - ) - - parser.add_argument( - "--dry-run", - action="store_true", - help="Show what would be done without executing", - ) - - parser.add_argument( - "--quiet", - action="store_true", - help="Minimal output", - ) - - parser.add_argument( - "--automation", - action="store_true", - help="Automation-friendly mode", - ) - - parser.add_argument( - "--exclude", - action="append", - default=[], - help="Exclude directory name or path fragment during discovery (repeatable)", - ) - - return parser - - -def main(): - parser = build_parser() - args = parser.parse_args() - - if not args.path: - parser.error("the following arguments are required: path") - - scan_root = Path(args.path) - - if not scan_root.exists(): - print(f"ERROR: Path does not exist: {scan_root}") - sys.exit(2) - - try: - from .discovery import discover_compose_files - from .classifier import classify_compose - except ImportError as e: - print(f"ERROR: Discovery/classifier import problem: {e}") - sys.exit(2) - - entries = [] - compose_files = [] - - try: - if scan_root.is_file(): - compose_files = [scan_root] - else: - compose_files = discover_compose_files( - scan_root, - excludes=args.exclude, - ) - - for compose_file in compose_files: - entries.extend(classify_compose(compose_file)) - except Exception as e: - print(f"ERROR: Failed during discovery/classification: {e}") - sys.exit(2) - - plan = build_plan(scan_root, entries, len(compose_files)) - - borg_shell_command = None - if args.repo: - try: - from .borg import build_borg_create_command, command_to_shell - except ImportError as e: - print(f"ERROR: Borg import problem: {e}") - sys.exit(2) - - try: - include_paths = [ - item["path"] for item in plan["include"] if item["path"] != "?" - ] - borg_command = build_borg_create_command(args.repo, include_paths) - borg_shell_command = command_to_shell(borg_command) - except Exception as e: - print(f"ERROR: Failed to build borg command: {e}") - sys.exit(2) - - if args.json: - print( - json.dumps( - build_json_output(plan, borg_shell_command), - indent=2, - sort_keys=False, - ) - ) - else: - if not args.quiet: - print_plan(plan) - print_warnings(plan) - - if borg_shell_command: - print("\nSuggested borg create command") - print("============================") - print(borg_shell_command) - - if args.automation: - has_missing = any(not item.get("exists", True) for item in plan["include"]) - if has_missing: - sys.exit(1) - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/images/dockervault-logo.png b/images/dockervault-logo.png index 1432c1e..c4a83a3 100644 Binary files a/images/dockervault-logo.png and b/images/dockervault-logo.png differ