mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 15:29:36 +02:00
added-fix-image-deletion (#681)
* added-fix-image-deletion * feat(commands): add image cleanup command to find and delete unused files * fix(models): ensure associated AdventureImages are deleted and files cleaned up on Adventure deletion * fix(models): ensure associated Attachment files are deleted and their filesystem cleaned up on Adventure deletion --------- Co-authored-by: ferdousahmed <taninme@gmail.com> Co-authored-by: Sean Morley
This commit is contained in:
parent
295ecd1362
commit
5308ec21d6
3 changed files with 115 additions and 1 deletions
|
@ -0,0 +1,94 @@
|
|||
import os
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from adventures.models import AdventureImage, Attachment
|
||||
from users.models import CustomUser
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Find and prompt for deletion of unused image files and attachments in filesystem'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Show files that would be deleted without actually deleting them',
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
dry_run = options['dry_run']
|
||||
|
||||
# Get all image and attachment file paths from database
|
||||
used_files = set()
|
||||
|
||||
# Get AdventureImage file paths
|
||||
for img in AdventureImage.objects.all():
|
||||
if img.image and img.image.name:
|
||||
used_files.add(os.path.join(settings.MEDIA_ROOT, img.image.name))
|
||||
|
||||
# Get Attachment file paths
|
||||
for attachment in Attachment.objects.all():
|
||||
if attachment.file and attachment.file.name:
|
||||
used_files.add(os.path.join(settings.MEDIA_ROOT, attachment.file.name))
|
||||
|
||||
# Get user profile picture file paths
|
||||
for user in CustomUser.objects.all():
|
||||
if user.profile_pic and user.profile_pic.name:
|
||||
used_files.add(os.path.join(settings.MEDIA_ROOT, user.profile_pic.name))
|
||||
|
||||
# Find all files in media/images and media/attachments directories
|
||||
media_root = settings.MEDIA_ROOT
|
||||
all_files = []
|
||||
|
||||
# Scan images directory
|
||||
images_dir = os.path.join(media_root, 'images')
|
||||
# Scan attachments directory
|
||||
attachments_dir = os.path.join(media_root, 'attachments')
|
||||
if os.path.exists(attachments_dir):
|
||||
for root, _, files in os.walk(attachments_dir):
|
||||
for file in files:
|
||||
all_files.append(os.path.join(root, file))
|
||||
|
||||
# Scan profile-pics directory
|
||||
profile_pics_dir = os.path.join(media_root, 'profile-pics')
|
||||
if os.path.exists(profile_pics_dir):
|
||||
for root, _, files in os.walk(profile_pics_dir):
|
||||
for file in files:
|
||||
all_files.append(os.path.join(root, file))
|
||||
attachments_dir = os.path.join(media_root, 'attachments')
|
||||
if os.path.exists(attachments_dir):
|
||||
for root, _, files in os.walk(attachments_dir):
|
||||
for file in files:
|
||||
all_files.append(os.path.join(root, file))
|
||||
|
||||
# Find unused files
|
||||
unused_files = [f for f in all_files if f not in used_files]
|
||||
|
||||
if not unused_files:
|
||||
self.stdout.write(self.style.SUCCESS('No unused files found.'))
|
||||
return
|
||||
|
||||
self.stdout.write(f'Found {len(unused_files)} unused files:')
|
||||
for file_path in unused_files:
|
||||
self.stdout.write(f' {file_path}')
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('Dry run mode - no files were deleted.'))
|
||||
return
|
||||
|
||||
# Prompt for deletion
|
||||
confirm = input('\nDo you want to delete these files? (yes/no): ')
|
||||
if confirm.lower() in ['yes', 'y']:
|
||||
deleted_count = 0
|
||||
for file_path in unused_files:
|
||||
try:
|
||||
os.remove(file_path)
|
||||
self.stdout.write(f'Deleted: {file_path}')
|
||||
deleted_count += 1
|
||||
except OSError as e:
|
||||
self.stdout.write(self.style.ERROR(f'Error deleting {file_path}: {e}'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'Successfully deleted {deleted_count} files.'))
|
||||
else:
|
||||
self.stdout.write('Operation cancelled.')
|
||||
|
|
@ -656,6 +656,15 @@ class Adventure(models.Model):
|
|||
|
||||
return result
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Delete all associated AdventureImages first to trigger their filesystem cleanup
|
||||
for image in self.images.all():
|
||||
image.delete()
|
||||
# Delete all associated Attachment files first to trigger their filesystem cleanup
|
||||
for attachment in self.attachments.all():
|
||||
attachment.delete()
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -838,6 +847,12 @@ class AdventureImage(models.Model):
|
|||
self.full_clean() # This calls clean() method
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Remove file from disk when deleting AdventureImage
|
||||
if self.image and os.path.isfile(self.image.path):
|
||||
os.remove(self.image.path)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.image.url if self.image else f"Immich ID: {self.immich_id or 'No image'}"
|
||||
|
||||
|
@ -849,6 +864,11 @@ class Attachment(models.Model):
|
|||
adventure = models.ForeignKey(Adventure, related_name='attachments', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=200, null=True, blank=True)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if self.file and os.path.isfile(self.file.path):
|
||||
os.remove(self.file.path)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.file.url
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from adventures.serializers import AdventureImageSerializer
|
|||
from integrations.models import ImmichIntegration
|
||||
import uuid
|
||||
import requests
|
||||
import os
|
||||
|
||||
class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = AdventureImageSerializer
|
||||
|
@ -155,7 +156,6 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
print("perform_destroy")
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue