Files
media-downloader/scripts/move_immich_deleted_to_recycle.py
Todd 0d7b2b1aab Initial commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 22:42:55 -04:00

128 lines
3.7 KiB
Python

#!/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/<uuid>.<ext>
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()