diff --git a/program/management/commands/removestaleimages.py b/program/management/commands/removestaleimages.py new file mode 100644 index 0000000000000000000000000000000000000000..8e1d5f6512fa544a395b7677d9ddcd9d6109859d --- /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)