128 lines
3.7 KiB
Python
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()
|