#!/usr/bin/env python3 """ Move Immich soft-deleted files from /opt/immich/el/ and /opt/immich/elv/ into the actual recycle bin (/opt/immich/recycle/) with proper DB entries. For each file with location='recycle' in file_inventory: 1. Move the file to /opt/immich/recycle/. 2. Insert a row into recycle_bin 3. Delete from file_inventory """ import os import shutil import sys import time import uuid from pathlib import Path import psycopg2 APP_DB_DSN = "postgresql://media_downloader:PNsihOXvvuPwWiIvGlsc9Fh2YmMmB@localhost/media_downloader" RECYCLE_DIR = Path("/opt/immich/recycle") BATCH_SIZE = 500 def main(): start = time.time() print("Moving Immich soft-deleted files to recycle bin") print("=" * 60) conn = psycopg2.connect(APP_DB_DSN) cur = conn.cursor() # Get all recycled entries from el/elv cur.execute(""" SELECT id, file_path, filename, file_size, file_hash, created_date FROM file_inventory WHERE (file_path LIKE '/opt/immich/el/%' OR file_path LIKE '/opt/immich/elv/%') AND location = 'recycle' ORDER BY id """) rows = cur.fetchall() total = len(rows) print(f" Found {total:,} recycled entries to move") if total == 0: print(" Nothing to do.") conn.close() return RECYCLE_DIR.mkdir(parents=True, exist_ok=True) moved = 0 missing = 0 errors = 0 for i, (inv_id, file_path, filename, file_size, file_hash, created_date) in enumerate(rows): src = Path(file_path) if not src.exists(): # File doesn't exist on disk — just remove from file_inventory cur.execute("DELETE FROM file_inventory WHERE id = %s", (inv_id,)) missing += 1 if missing <= 5: print(f" MISSING (removed from DB): {file_path}") continue # Generate recycle path ext = src.suffix or "" recycle_id = str(uuid.uuid4()) recycle_path = RECYCLE_DIR / f"{recycle_id}{ext}" try: # Get file mtime before moving mtime = src.stat().st_mtime actual_size = src.stat().st_size # Move the file shutil.move(str(src), str(recycle_path)) # Insert into recycle_bin cur.execute(""" INSERT INTO recycle_bin (id, original_path, original_filename, recycle_path, file_extension, file_size, original_mtime, deleted_from, deleted_by, file_hash) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( recycle_id, file_path, filename, str(recycle_path), ext.lstrip(".") if ext else None, actual_size or file_size, mtime, "immich_deleted", "immich_migration", file_hash, )) # Delete from file_inventory cur.execute("DELETE FROM file_inventory WHERE id = %s", (inv_id,)) moved += 1 except Exception as e: errors += 1 if errors <= 5: print(f" ERROR moving {file_path}: {e}") if (i + 1) % BATCH_SIZE == 0: conn.commit() print(f" Progress: {i + 1:,}/{total:,} — moved: {moved:,}, missing: {missing:,}, errors: {errors:,}") conn.commit() cur.close() conn.close() elapsed = time.time() - start print(f"\n DONE in {elapsed:.1f}s:") print(f" Moved to recycle: {moved:,}") print(f" Missing on disk: {missing:,}") print(f" Errors: {errors:,}") if __name__ == "__main__": main()