mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
248 lines
9.2 KiB
Python
248 lines
9.2 KiB
Python
# 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']
|