From 90e234f651641eb9d6e9a46ef1cf972fa472ef05 Mon Sep 17 00:00:00 2001
From: Ernesto Rico Schmidt <ernesto@helsinki.at>
Date: Thu, 30 Mar 2023 17:13:10 -0400
Subject: [PATCH] Add management command to remove stale images

---
 .../management/commands/removestaleimages.py  | 49 +++++++++++++++++++
 1 file changed, 49 insertions(+)
 create mode 100644 program/management/commands/removestaleimages.py

diff --git a/program/management/commands/removestaleimages.py b/program/management/commands/removestaleimages.py
new file mode 100644
index 00000000..8e1d5f65
--- /dev/null
+++ b/program/management/commands/removestaleimages.py
@@ -0,0 +1,49 @@
+import os
+
+from versatileimagefield.fields import VersatileImageField
+
+from django.apps import apps
+from django.conf import settings
+from django.core.management.base import BaseCommand
+from django.db.models import ImageField, Q
+
+
+class Command(BaseCommand):
+    help = "removes images from the MEDIA_ROOT that are not referenced in the database."
+
+    def handle(self, *args, **options):
+        def remove_file(path: str) -> None:
+            print(f"REMOVED {path}")
+            os.remove(path)
+
+        image_names = []  # names (without extensions) of the images in the database
+
+        for model in apps.get_models():
+            for field in model._meta.get_fields():
+                if isinstance(field, VersatileImageField) or isinstance(field, ImageField):
+                    is_empty = Q(**{f"{field.name}": ""})
+                    images = model.objects.exclude(is_empty).values_list(field.name, flat=True)
+
+                    for image in images:
+                        basename = os.path.basename(image)
+                        filename, ext = os.path.splitext(basename)
+                        image_names.append(filename)
+
+        media_root = getattr(settings, "MEDIA_ROOT")
+
+        for relative_root, dirs, files in os.walk(media_root):
+            for file_ in files:
+                relative_path = os.path.join(os.path.relpath(relative_root, media_root), file_)
+                head, tail = os.path.split(relative_path)
+                filename, ext = os.path.splitext(tail)
+
+                fullpath = os.path.join(media_root, relative_path)
+
+                if not head.startswith("__sized__"):
+                    if filename not in image_names:
+                        remove_file(fullpath)
+                else:
+                    # cropped images
+                    name, _ = filename.split("-crop-")
+                    if name not in image_names:
+                        remove_file(fullpath)
-- 
GitLab