django-cleanup
django-cleanup is a lightweight Django library that automatically deletes files associated with `FileField` and `ImageField` when their corresponding model instances are deleted or when the file itself is changed. This prevents orphaned files from cluttering storage. It integrates seamlessly into existing Django projects with minimal configuration, supporting bulk deletions and various storage backends (e.g., local filesystem, Amazon S3). The current version is 9.0.0, released on September 18, 2024, and it aims to maintain compatibility with currently supported Django and Python versions.
Warnings
- gotcha The `django_cleanup.apps.CleanupConfig` must be placed *last* in your `INSTALLED_APPS` setting. This ensures that it can correctly discover all `FileField`s across all your applications and that file deletion integrity within transactions is maintained, even if other apps' signal handlers raise exceptions.
- gotcha If your project uses a database that does not support transactions, or if a transaction rolls back, files *may* be lost without their corresponding model instance being fully deleted or updated. While `django-cleanup` mitigates this using `post_save` and `post_delete` signals, it's a known limitation to be aware of if your database setup is unusual.
- gotcha `django-cleanup` relies on Django's application registry to discover models and their `FileField`s. If your models are not properly loaded (e.g., if they are not defined or imported in your app's `models.py` or `models/__init__.py`), `django-cleanup` may not be able to discover them and thus won't remove files as expected.
- gotcha Files that are explicitly set as a `FileField`'s `default` value will *not* be deleted by `django-cleanup`. This is by design to prevent accidental deletion of common default files that might be referenced by many instances.
Install
-
pip install django-cleanup
Imports
- django_cleanup
INSTALLED_APPS = ['...', 'django_cleanup']
- CleanupConfig
INSTALLED_APPS = ['...', 'django_cleanup.apps.CleanupConfig']
- cleanup_pre_delete, cleanup_post_delete
from django_cleanup.signals import cleanup_pre_delete, cleanup_post_delete
Quickstart
import os
import django
from django.conf import settings
from django.db import models
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
# Minimal Django settings for demonstration
if not settings.configured:
settings.configure(
INSTALLED_APPS=[
'django.contrib.auth',
'django.contrib.contenttypes',
'django_cleanup.apps.CleanupConfig',
'myapp' # Your app where models reside
],
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}},
MEDIA_ROOT=os.path.join(os.getcwd(), 'media_test'),
DEFAULT_AUTO_FIELD='django.db.models.AutoField',
)
django.setup()
# Create a dummy app to hold models
class MyAppConfig(django.apps.AppConfig):
name = 'myapp'
label = 'myapp'
# Add dummy app to apps registry
if not django.apps.apps.get_app_config('myapp'):
django.apps.apps.populate(settings.INSTALLED_APPS)
# Define a simple model with a FileField
class MyModel(models.Model):
name = models.CharField(max_length=100)
image = models.ImageField(upload_to='images')
def __str__(self):
return self.name
# Ensure media directory exists for testing
os.makedirs(settings.MEDIA_ROOT, exist_ok=True)
# Simulate model usage
print(f"Media root: {settings.MEDIA_ROOT}")
# 1. Create instance and save a dummy file
instance = MyModel(name='Test Item')
# Create a dummy image file
dummy_image_path = os.path.join(settings.MEDIA_ROOT, 'images', 'test_image.jpg')
os.makedirs(os.path.dirname(dummy_image_path), exist_ok=True)
with open(dummy_image_path, 'w') as f:
f.write('dummy image content')
instance.image.name = 'images/test_image.jpg'
instance.save()
print(f"File exists after creation: {os.path.exists(dummy_image_path)}")
# 2. Update instance with a new file (old one should be deleted)
new_dummy_image_path = os.path.join(settings.MEDIA_ROOT, 'images', 'new_test_image.jpg')
with open(new_dummy_image_path, 'w') as f:
f.write('new dummy image content')
old_image_name = instance.image.name
instance.image.name = 'images/new_test_image.jpg'
instance.save()
print(f"Old file '{old_image_name}' exists after update: {os.path.exists(os.path.join(settings.MEDIA_ROOT, old_image_name))}")
print(f"New file exists after update: {os.path.exists(new_dummy_image_path)}")
# 3. Delete instance (associated file should be deleted)
instance_pk = instance.pk
instance.delete()
print(f"New file exists after instance deletion: {os.path.exists(new_dummy_image_path)}")
# Clean up test media directory (optional)
import shutil
shutil.rmtree(settings.MEDIA_ROOT)
print("Cleaned up media_test directory.")