#!/usr/bin/env python3 """ Migrate existing uploads to organized structure Moves files from uploads/ to uploads/{hostname}/images or uploads/{hostname}/files """ import os import shutil import sys from pathlib import Path # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent)) from sqlalchemy.orm import Session from app.db.session import SessionLocal from app.core.config import settings from app.models.device import Device from app.models.document import Document from app.utils.file_organizer import ( sanitize_hostname, is_image_file, ensure_device_directories ) def migrate_files(dry_run: bool = True): """ Migrate existing files to organized structure Args: dry_run: If True, only print what would be done """ db: Session = SessionLocal() try: # Get all documents documents = db.query(Document).all() print(f"Found {len(documents)} documents to migrate") print(f"Mode: {'DRY RUN' if dry_run else 'ACTUAL MIGRATION'}") print("-" * 80) migrated_count = 0 error_count = 0 skipped_count = 0 for doc in documents: # Get device device = db.query(Device).filter(Device.id == doc.device_id).first() if not device: print(f"❌ Document {doc.id}: Device {doc.device_id} not found - SKIPPING") error_count += 1 continue # Check if file exists if not os.path.exists(doc.stored_path): print(f"⚠️ Document {doc.id}: File not found at {doc.stored_path} - SKIPPING") skipped_count += 1 continue # Determine if image is_image = is_image_file(doc.filename, doc.mime_type) file_type = "image" if is_image else "file" # Get new path sanitized_hostname = sanitize_hostname(device.hostname) subdir = "images" if is_image else "files" filename = os.path.basename(doc.stored_path) new_path = os.path.join( settings.UPLOAD_DIR, sanitized_hostname, subdir, filename ) # Check if already in correct location if doc.stored_path == new_path: print(f"✓ Document {doc.id}: Already in correct location") skipped_count += 1 continue print(f"📄 Document {doc.id} ({file_type}):") print(f" Device: {device.hostname} (ID: {device.id})") print(f" From: {doc.stored_path}") print(f" To: {new_path}") if not dry_run: try: # Create target directory os.makedirs(os.path.dirname(new_path), exist_ok=True) # Move file shutil.move(doc.stored_path, new_path) # Update database doc.stored_path = new_path db.add(doc) print(f" ✅ Migrated successfully") migrated_count += 1 except Exception as e: print(f" ❌ Error: {e}") error_count += 1 else: print(f" [DRY RUN - would migrate]") migrated_count += 1 print() if not dry_run: db.commit() print("Database updated") print("-" * 80) print(f"Summary:") print(f" Migrated: {migrated_count}") print(f" Skipped: {skipped_count}") print(f" Errors: {error_count}") print(f" Total: {len(documents)}") if dry_run: print() print("This was a DRY RUN. To actually migrate files, run:") print(" python backend/migrate_file_organization.py --execute") finally: db.close() def cleanup_empty_directories(base_dir: str): """Remove empty directories after migration""" for root, dirs, files in os.walk(base_dir, topdown=False): for dir_name in dirs: dir_path = os.path.join(root, dir_name) try: if not os.listdir(dir_path): # Directory is empty os.rmdir(dir_path) print(f"Removed empty directory: {dir_path}") except Exception as e: print(f"Could not remove {dir_path}: {e}") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Migrate uploads to organized structure") parser.add_argument( "--execute", action="store_true", help="Actually perform the migration (default is dry-run)" ) parser.add_argument( "--cleanup", action="store_true", help="Clean up empty directories after migration" ) args = parser.parse_args() print("=" * 80) print("File Organization Migration") print("=" * 80) print() migrate_files(dry_run=not args.execute) if args.execute and args.cleanup: print() print("=" * 80) print("Cleaning up empty directories") print("=" * 80) cleanup_empty_directories(settings.UPLOAD_DIR) print() print("Done!")