{ "sourceFile": "monitor.py", "activeCommit": 0, "commits": [ { "activePatchIndex": 0, "patches": [ { "date": 1745506961196, "content": "Index: \n===================================================================\n--- \n+++ \n" } ], "date": 1745506961196, "name": "Commit-0", "content": "# monitor.py\n\nimport os\nimport sys\nimport time\nimport logging\nimport re\nimport shutil\nfrom pathlib import Path\nfrom watchdog.observers.polling import PollingObserver as Observer # Use polling for better compatibility\nfrom watchdog.events import FileSystemEventHandler, FileCreatedEvent\n\n# --- Import from local modules ---\ntry:\n # Assuming main.py is in the same directory\n from main import run_processing, setup_logging, ConfigurationError, AssetProcessingError\nexcept ImportError as e:\n print(f\"ERROR: Failed to import required functions/classes from main.py: {e}\")\n print(\"Ensure main.py is in the same directory as monitor.py.\")\n sys.exit(1)\n\n# --- Configuration ---\n# Read from environment variables with defaults\nINPUT_DIR = Path(os.environ.get('INPUT_DIR', '/data/input'))\nOUTPUT_DIR = Path(os.environ.get('OUTPUT_DIR', '/data/output'))\nPROCESSED_DIR = Path(os.environ.get('PROCESSED_DIR', '/data/processed'))\nERROR_DIR = Path(os.environ.get('ERROR_DIR', '/data/error'))\nLOG_LEVEL_STR = os.environ.get('LOG_LEVEL', 'INFO').upper()\nPOLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '5'))\nPROCESS_DELAY = int(os.environ.get('PROCESS_DELAY', '2'))\n# Default workers for monitor - can be overridden if needed via env var\nDEFAULT_WORKERS = max(1, os.cpu_count() // 2 if os.cpu_count() else 1)\nNUM_WORKERS = int(os.environ.get('NUM_WORKERS', str(DEFAULT_WORKERS)))\n\n# --- Logging Setup ---\nlog_level = getattr(logging, LOG_LEVEL_STR, logging.INFO)\n# Use the setup_logging from main.py but configure the level directly\n# We don't have a 'verbose' flag here, so call basicConfig directly\nlog_format = '%(asctime)s [%(levelname)-8s] %(name)s: %(message)s'\ndate_format = '%Y-%m-%d %H:%M:%S'\nlogging.basicConfig(level=log_level, format=log_format, datefmt=date_format, handlers=[logging.StreamHandler(sys.stdout)])\nlog = logging.getLogger(\"monitor\")\nlog.info(f\"Logging level set to: {logging.getLevelName(log_level)}\")\nlog.info(f\"Monitoring Input Directory: {INPUT_DIR}\")\nlog.info(f\"Output Directory: {OUTPUT_DIR}\")\nlog.info(f\"Processed Files Directory: {PROCESSED_DIR}\")\nlog.info(f\"Error Files Directory: {ERROR_DIR}\")\nlog.info(f\"Polling Interval: {POLL_INTERVAL}s\")\nlog.info(f\"Processing Delay: {PROCESS_DELAY}s\")\nlog.info(f\"Max Workers: {NUM_WORKERS}\")\n\n\n# --- Preset Validation ---\nPRESET_DIR = Path(__file__).parent / \"Presets\"\nPRESET_FILENAME_REGEX = re.compile(r\"^\\[?([a-zA-Z0-9_-]+)\\]?_.*\\.(zip|rar|7z)$\", re.IGNORECASE)\n\ndef validate_preset(preset_name: str) -> bool:\n \"\"\"Checks if the preset JSON file exists.\"\"\"\n if not preset_name:\n return False\n preset_file = PRESET_DIR / f\"{preset_name}.json\"\n exists = preset_file.is_file()\n if not exists:\n log.warning(f\"Preset file not found: {preset_file}\")\n return exists\n\n# --- Watchdog Event Handler ---\nclass ZipHandler(FileSystemEventHandler):\n \"\"\"Handles file system events for new ZIP files.\"\"\"\n\n def __init__(self, input_dir: Path, output_dir: Path, processed_dir: Path, error_dir: Path):\n self.input_dir = input_dir.resolve()\n self.output_dir = output_dir.resolve()\n self.processed_dir = processed_dir.resolve()\n self.error_dir = error_dir.resolve()\n # Ensure target directories exist\n self.output_dir.mkdir(parents=True, exist_ok=True)\n self.processed_dir.mkdir(parents=True, exist_ok=True)\n self.error_dir.mkdir(parents=True, exist_ok=True)\n log.info(\"Handler initialized, target directories ensured.\")\n\n def on_created(self, event: FileCreatedEvent):\n \"\"\"Called when a file or directory is created.\"\"\"\n if event.is_directory:\n return\n\n src_path = Path(event.src_path)\n log.debug(f\"File creation event detected: {src_path}\")\n\n # Check if the file has a supported archive extension\n supported_suffixes = ['.zip', '.rar', '.7z']\n if src_path.suffix.lower() not in supported_suffixes:\n log.debug(f\"Ignoring file with unsupported extension: {src_path.name}\")\n return\n\n log.info(f\"Detected new ZIP file: {src_path.name}. Waiting {PROCESS_DELAY}s before processing...\")\n time.sleep(PROCESS_DELAY)\n\n # Re-check if file still exists (might have been temporary)\n if not src_path.exists():\n log.warning(f\"File disappeared after delay: {src_path.name}\")\n return\n\n log.info(f\"Processing file: {src_path.name}\")\n\n # --- Extract Preset Name ---\n match = PRESET_FILENAME_REGEX.match(src_path.name)\n if not match:\n log.warning(f\"Filename '{src_path.name}' does not match expected format '[preset]_filename.zip'. Ignoring.\")\n # Optionally move to an 'ignored' or 'error' directory? For now, leave it.\n return\n\n preset_name = match.group(1)\n log.info(f\"Extracted preset name: '{preset_name}' from {src_path.name}\")\n\n # --- Validate Preset ---\n if not validate_preset(preset_name):\n log.error(f\"Preset '{preset_name}' is not valid (missing {PRESET_DIR / f'{preset_name}.json'}). Ignoring file {src_path.name}.\")\n # Move to error dir if preset is invalid? Let's do that.\n self.move_file(src_path, self.error_dir, \"invalid_preset\")\n return\n\n # --- Run Processing ---\n try:\n log.info(f\"Starting asset processing for '{src_path.name}' using preset '{preset_name}'...\")\n # run_processing expects a list of inputs\n results = run_processing(\n valid_inputs=[str(src_path)],\n preset_name=preset_name,\n output_dir_for_processor=str(self.output_dir), # Pass absolute output path\n overwrite=False, # Default to no overwrite for monitored files? Or make configurable? Let's default to False.\n num_workers=NUM_WORKERS\n )\n\n # --- Handle Results ---\n # Check overall status based on counts\n processed = results.get(\"processed\", 0)\n skipped = results.get(\"skipped\", 0)\n failed = results.get(\"failed\", 0)\n pool_error = results.get(\"pool_error\")\n\n if pool_error:\n log.error(f\"Processing pool error for {src_path.name}: {pool_error}\")\n self.move_file(src_path, self.error_dir, \"pool_error\")\n elif failed > 0:\n log.error(f\"Processing failed for {src_path.name}. Check worker logs for details.\")\n # Log specific errors if available in results_list\n for res_path, status, err_msg in results.get(\"results_list\", []):\n if status == \"failed\":\n log.error(f\" - Failure reason: {err_msg}\")\n self.move_file(src_path, self.error_dir, \"processing_failed\")\n elif processed > 0:\n log.info(f\"Successfully processed {src_path.name}.\")\n self.move_file(src_path, self.processed_dir, \"processed\")\n elif skipped > 0:\n log.info(f\"Processing skipped for {src_path.name} (likely already exists).\")\n self.move_file(src_path, self.processed_dir, \"skipped\")\n else:\n # Should not happen if input was valid zip\n log.warning(f\"Processing finished for {src_path.name} with unexpected status (0 processed, 0 skipped, 0 failed). Moving to error dir.\")\n self.move_file(src_path, self.error_dir, \"unknown_status\")\n\n except (ConfigurationError, AssetProcessingError) as e:\n log.error(f\"Asset processing error for {src_path.name}: {e}\", exc_info=True)\n self.move_file(src_path, self.error_dir, \"processing_exception\")\n except Exception as e:\n log.exception(f\"Unexpected error during processing trigger for {src_path.name}: {e}\")\n self.move_file(src_path, self.error_dir, \"monitor_exception\")\n\n\n def move_file(self, src: Path, dest_dir: Path, reason: str):\n \"\"\"Safely moves a file, handling potential name collisions.\"\"\"\n if not src.exists():\n log.warning(f\"Source file {src} does not exist, cannot move for reason: {reason}.\")\n return\n try:\n dest_path = dest_dir / src.name\n # Handle potential name collision in destination\n counter = 1\n while dest_path.exists():\n dest_path = dest_dir / f\"{src.stem}_{counter}{src.suffix}\"\n counter += 1\n if counter > 100: # Safety break\n log.error(f\"Could not find unique name for {src.name} in {dest_dir} after 100 attempts. Aborting move.\")\n return\n\n log.info(f\"Moving '{src.name}' to '{dest_dir.name}/' directory (Reason: {reason}). Final path: {dest_path.name}\")\n shutil.move(str(src), str(dest_path))\n except Exception as e:\n log.exception(f\"Failed to move file {src.name} to {dest_dir}: {e}\")\n\n\n# --- Main Monitor Loop ---\nif __name__ == \"__main__\":\n # Ensure input directory exists\n if not INPUT_DIR.is_dir():\n log.error(f\"Input directory does not exist or is not a directory: {INPUT_DIR}\")\n log.error(\"Please create the directory or mount a volume correctly.\")\n sys.exit(1)\n\n event_handler = ZipHandler(INPUT_DIR, OUTPUT_DIR, PROCESSED_DIR, ERROR_DIR)\n observer = Observer()\n observer.schedule(event_handler, str(INPUT_DIR), recursive=False) # Don't watch subdirectories\n\n log.info(\"Starting file system monitor...\")\n observer.start()\n log.info(\"Monitor started. Press Ctrl+C to stop.\")\n\n try:\n while True:\n # Keep the main thread alive, observer runs in background thread\n time.sleep(1)\n except KeyboardInterrupt:\n log.info(\"Keyboard interrupt received, stopping monitor...\")\n observer.stop()\n except Exception as e:\n log.exception(f\"An unexpected error occurred in the main loop: {e}\")\n observer.stop()\n\n observer.join()\n log.info(\"Monitor stopped.\")" } ] }