# SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2025 Allaun """Apply tool for NoDupeLabs. This tool provides the apply functionality as a tool that can be loaded by the core system. It demonstrates how to convert existing modules to tools. Key Features: - File management actions - Duplicate handling - Progress tracking - Tool integration Dependencies: - Core modules """ from typing import Any, Dict import argparse from pathlib import Path from nodupe.core.tool_system.base import Tool from nodupe.tools.os_filesystem.filesystem import Filesystem from nodupe.tools.databases.files import FileRepository from nodupe.tools.databases.connection import DatabaseConnection class ApplyTool(Tool): """Apply tool implementation.""" name = "apply" version = "1.0.0" dependencies = [] def __init__(self): """Initialize apply tool.""" self.description = "Apply actions to duplicate files" def initialize(self, container: Any) -> None: """Initialize the tool.""" def shutdown(self) -> None: """Shutdown the tool.""" def get_capabilities(self) -> Dict[str, Any]: """Get tool capabilities.""" return {'commands': ['apply']} @property def api_methods(self) -> Dict[str, Any]: """Get API methods exposed by this tool.""" return {} def describe_usage(self) -> str: """Return human-readable usage instructions.""" return "Apply file management actions to duplicate files: delete, move, copy, or list" def run_standalone(self, args: Any) -> int: """Execute the tool in standalone mode.""" return self.execute_apply(args) def _on_apply_start(self, **kwargs: Any) -> None: """Handle apply start event.""" print(f"[TOOL] Apply started: {kwargs.get('action', 'unknown')}") def _on_apply_complete(self, **kwargs: Any) -> None: """Handle apply complete event.""" print(f"[TOOL] Apply completed: {kwargs.get('files_processed', 0)} files processed") def register_commands(self, subparsers: Any) -> None: """Register apply command with argument parser.""" apply_parser = subparsers.add_parser('apply', help='Apply actions to duplicates') apply_parser.add_argument( 'action', choices=['delete', 'move', 'copy', 'list'], help='Action to perform' ) apply_parser.add_argument( '--destination', '-d', help='Destination directory (required for move/copy)' ) apply_parser.add_argument('--dry-run', action='store_true', help='Dry run (no changes)') apply_parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') apply_parser.set_defaults(func=self.execute_apply) def execute_apply(self, args: argparse.Namespace) -> int: """Execute apply command. Args: args: Command arguments including injected 'container' """ try: # Validation # Check if input is provided (for compatibility with tests) if hasattr(args, 'input') and args.input is None: print("[ERROR] Input file is required") return 1 # Validate input file exists if hasattr(args, 'input') and args.input: import os if not os.path.exists(args.input): print(f"[ERROR] Input file does not exist: {args.input}") return 1 # Validate action is one of the allowed choices valid_actions = ['delete', 'move', 'copy', 'list'] if args.action not in valid_actions: print(f"[ERROR] Invalid action: {args.action}. Must be one of: {', '.join(valid_actions)}") return 1 # Validate destination for move/copy actions if args.action in ['move', 'copy'] and not args.destination: print("[ERROR] --destination is required for move/copy actions") return 1 # Validate target directory for move/copy if args.action in ['move', 'copy']: if hasattr(args, 'destination') and args.destination: import os if not os.path.exists(args.destination): print(f"[ERROR] Target directory does not exist: {args.destination}") return 1 print(f"[TOOL] Executing apply command: {args.action}") # 1. Get services container = getattr(args, 'container', None) if not container: print("[ERROR] Dependency container not available") return 1 db_connection = container.get_service('database') if not db_connection: print("[ERROR] Database service not available") # Fallback to default db_connection = DatabaseConnection.get_instance() file_repo = FileRepository(db_connection) # 2. Get duplicates duplicates = file_repo.get_duplicate_files() if not duplicates: print("[TOOL] No items marked as duplicates in database.") print(" (Did you run 'scan' and 'sim' commands first?)") return 0 print(f"[TOOL] Found {len(duplicates)} duplicate files identified in database") files_processed = 0 if args.action == 'list': print("\nIdentified Duplicates:") for dup in duplicates: original_id = dup['duplicate_of'] original = file_repo.get_file(original_id) orig_path = original['path'] if original else "?" print(f" {dup['path']} (Duplicate of: {orig_path})") return 0 # 3. Process Actions for dup in duplicates: path = Path(dup['path']) try: if not path.exists(): print(f"[WARN] File not found: {path} (skipping)") continue if args.action == 'delete': if args.dry_run: print(f"[DRY-RUN] Would delete: {path}") else: Filesystem.remove_file(path) file_repo.delete_file(dup['id']) print(f"[DELETED] {path}") elif args.action in ['move', 'copy']: if not args.destination: # Should verify logic above continue dest_dir = Path(args.destination) dest_path = dest_dir / path.name # Handle collision if dest_path.exists(): # Simple rename logic stem = dest_path.stem suffix = dest_path.suffix dest_path = dest_dir / f"{stem}_{dup['id']}{suffix}" if args.action == 'move': if args.dry_run: print(f"[DRY-RUN] Would move: {path} -> {dest_path}") else: Filesystem.ensure_directory(dest_dir) Filesystem.move_file(path, dest_path) # Update DB to point to new location or just delete entry? # Usually duplicates are processed to get rid of them or archive them. # If moved, we might update the path in DB or remove if 'archived' implies removal from active working set. # Let's remove from DB as "processed duplicate". file_repo.delete_file(dup['id']) print(f"[MOVED] {path} -> {dest_path}") elif args.action == 'copy': if args.dry_run: print(f"[DRY-RUN] Would copy: {path} -> {dest_path}") else: Filesystem.ensure_directory(dest_dir) Filesystem.copy_file(path, dest_path) print(f"[COPIED] {path} -> {dest_path}") files_processed += 1 except Exception as e: print(f"[ERROR] Failed to process {path}: {e}") if args.dry_run: print(f"\n[TOOL] Dry run complete. Would process {files_processed} files.") else: print(f"\n[TOOL] Apply complete. Processed {files_processed} files.") self._on_apply_complete(files_processed=files_processed) return 0 except Exception as e: print(f"[TOOL ERROR] Apply failed: {e}") if args.verbose: import traceback traceback.print_exc() return 1 # Create tool instance when module is loaded apply_tool = ApplyTool() # Register tool with core system def register_tool(): """Register tool with core system.""" return apply_tool # Export tool interface __all__ = ['apply_tool', 'register_tool']