mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""Garbage collect orphan flavor image files.
|
|
|
|
Walks MEDIA_ROOT/flavors/ and removes directories whose paths are not referenced
|
|
by any Flavor.local_image_paths or Flavor.main_image_path. Use --dry-run to
|
|
preview without deleting.
|
|
|
|
Files referenced by the legacy Flavor.image ImageField are also preserved (for
|
|
backwards compatibility) until that field is dropped.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from django.conf import settings
|
|
from django.core.management.base import BaseCommand
|
|
|
|
from api.models import Flavor
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = "Remove orphan flavor image files / directories not referenced by any Flavor."
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print what would be deleted but don't touch the filesystem.",
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
dry_run: bool = options["dry_run"]
|
|
flavors_root = Path(settings.MEDIA_ROOT) / "flavors"
|
|
if not flavors_root.exists():
|
|
self.stdout.write(self.style.WARNING(f"No flavors dir at {flavors_root}"))
|
|
return
|
|
|
|
# Collect every relative path/directory that should be kept.
|
|
referenced_files: set[str] = set()
|
|
referenced_dirs: set[str] = set()
|
|
legacy_image_files: set[str] = set()
|
|
|
|
for flavor in Flavor.objects.all().only(
|
|
"local_image_paths", "main_image_path", "image"
|
|
):
|
|
paths = list(flavor.local_image_paths or [])
|
|
if flavor.main_image_path:
|
|
paths.append(flavor.main_image_path)
|
|
for rel in paths:
|
|
referenced_files.add(rel)
|
|
referenced_dirs.add(os.path.dirname(rel))
|
|
if flavor.image:
|
|
legacy_image_files.add(flavor.image.name)
|
|
|
|
# Walk the flavors tree and decide what's unreferenced.
|
|
deleted_dirs: list[str] = []
|
|
deleted_files: list[str] = []
|
|
kept = 0
|
|
|
|
for entry in sorted(flavors_root.iterdir()):
|
|
rel_dir = f"flavors/{entry.name}"
|
|
if entry.is_dir():
|
|
if rel_dir in referenced_dirs:
|
|
# Drop only orphan files inside the referenced dir.
|
|
for f in entry.iterdir():
|
|
rel_file = f"{rel_dir}/{f.name}"
|
|
if f.is_file() and rel_file not in referenced_files:
|
|
deleted_files.append(rel_file)
|
|
if not dry_run:
|
|
f.unlink()
|
|
else:
|
|
kept += 1
|
|
else:
|
|
deleted_dirs.append(rel_dir)
|
|
if not dry_run:
|
|
shutil.rmtree(entry)
|
|
elif entry.is_file():
|
|
rel_file = f"flavors/{entry.name}"
|
|
if rel_file in legacy_image_files or rel_file in referenced_files:
|
|
kept += 1
|
|
else:
|
|
deleted_files.append(rel_file)
|
|
if not dry_run:
|
|
entry.unlink()
|
|
|
|
verb = "Would delete" if dry_run else "Deleted"
|
|
self.stdout.write(self.style.SUCCESS(f"{verb}: {len(deleted_dirs)} dirs, {len(deleted_files)} files"))
|
|
for path in deleted_dirs:
|
|
self.stdout.write(f" dir {path}")
|
|
for path in deleted_files:
|
|
self.stdout.write(f" file {path}")
|
|
self.stdout.write(f"Kept: {kept} entries")
|