File Cleanup
The django_cleanup module automatically deletes files from storage when Django model instances with FileField or ImageField are deleted or updated. It works via Django signals, schedules deletions inside transaction.on_commit() for safety, and requires no changes to existing models.
What It Handles
| Trigger | Behavior |
|---|---|
| Instance deleted | All file fields on the instance are deleted from storage |
| File field replaced | Old file is deleted when a new value is saved |
| Soft-deleted instance | Files are not deleted — soft-delete fields are detected automatically |
Model in exclude_models | Signals not connected — files never touched |
Field in exclude_fields | That field skipped — others cleaned up normally |
Installation
django_cleanup is a built-in module in Django-CFG and is always included in INSTALLED_APPS automatically. No manual setup is needed.
Zero-Config Usage
With default settings, all FileField and ImageField on all models are covered automatically:
class Report(models.Model):
pdf = models.FileField(upload_to="reports/")
cover = models.ImageField(upload_to="covers/", null=True)
# Delete instance → pdf and cover files deleted from storage
report.delete()
# Replace pdf → old file deleted, new file kept
report.pdf = new_file
report.save()Configuration
Configure cleanup behavior via StorageConfig on your DjangoConfig:
from django_cfg import DjangoConfig, StorageConfig
class MyConfig(DjangoConfig):
storage = StorageConfig(
auto_cleanup=True,
delete_on_replace=True,
log_deletions=True,
exclude_models={"backups.DatabaseBackup"},
exclude_fields={"docs.Contract.original_scan"},
check_shared_files=False,
respect_soft_delete=True,
soft_delete_fields={"deleted_at", "is_deleted", "deleted"},
)Disabling the Module
class MyConfig(DjangoConfig):
storage = StorageConfig(enabled=False)Configuration Fields
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | True | Enable the cleanup module entirely |
auto_cleanup | bool | True | Register signals for all models with file fields at startup |
delete_on_replace | bool | True | Delete old file when a field is updated with a new value |
exclude_models | set[str] | set() | Models to skip. Format: "app_label.ModelName" |
exclude_fields | set[str] | set() | Fields to skip. Format: "app_label.ModelName.field_name" |
log_deletions | bool | False | Log each deleted file at INFO level |
check_shared_files | bool | False | Check if another record references the same file before deleting |
respect_soft_delete | bool | True | Skip file deletion for soft-deleted instances |
soft_delete_fields | set[str] | {"deleted_at", "is_deleted", "deleted"} | Field names that indicate a soft-delete pattern |
check_shared_files=True adds one extra DB query per file field at deletion time. Enable only when your data model shares file paths across rows.
Exclusion Formats
StorageConfig(
exclude_models={
"backups.DatabaseBackup", # entire model excluded
"audit.AuditLog",
},
exclude_fields={
"documents.Contract.original_scan", # only this field excluded
"users.Profile.legacy_avatar",
},
)Both fields accept lists — they are automatically converted to sets on initialization.
How Signals Work
1. pre_save → cache original file values before save
2. post_save → compare with originals, schedule deletion for replaced files
3. post_delete → schedule deletion for all files on deleted instance
All deletions run inside transaction.on_commit() — files survive if the
transaction rolls back.When auto_cleanup=True, the DjangoCleanupConfig.ready() method iterates all registered models, checks for file fields, and connects the three signals using dispatch_uid to prevent duplicate connections.
Soft-Delete Support
If a model has any field whose name appears in soft_delete_fields, the module detects a soft-delete pattern. Files are not deleted when the instance is soft-deleted — only when it is hard-deleted.
class Article(models.Model):
cover = models.ImageField(upload_to="articles/")
deleted_at = models.DateTimeField(null=True)
# Soft delete — file preserved
article.deleted_at = timezone.now()
article.save()
# Hard delete — file deleted
article.delete()Custom soft-delete field names:
StorageConfig(
respect_soft_delete=True,
soft_delete_fields={"deleted_at", "is_deleted", "removed_at"},
)Storage Backend Compatibility
Works with any Django storage backend that implements the standard Storage interface:
FileSystemStorage(default)S3Boto3Storagefromdjango-storagesGoogleCloudStoragefromdjango-storages- Azure Blob storage from
django-storages - Any custom storage backend
Per-Model Control with FileCleanupMixin
When auto_cleanup=False, use FileCleanupMixin on models that need explicit cleanup:
from django_cfg.modules.django_cleanup import FileCleanupMixin
class Contract(FileCleanupMixin, models.Model):
signed_pdf = models.FileField(upload_to="contracts/")
draft_pdf = models.FileField(upload_to="drafts/")
# Exclude specific fields from cleanup
file_cleanup_exclude_fields = ["draft_pdf"]The mixin overrides delete() and save() to handle cleanup. You can also trigger cleanup manually:
contract.cleanup_file("signed_pdf") # delete one field
contract.cleanup_all_files() # delete all fieldsBulk Delete with FileCleanupQuerySet
Regular QuerySet.delete() issues a single SQL DELETE without loading instances — files are never cleaned up. Use FileCleanupQuerySet to fix this:
from django_cfg.modules.django_cleanup import FileCleanupQuerySet
class Report(models.Model):
file = models.FileField(upload_to="reports/")
objects = models.Manager.from_queryset(FileCleanupQuerySet)()
# Loads all instances first, then deletes — files are cleaned up
Report.objects.filter(archived=True).delete()FileCleanupQuerySet.delete() fetches all matching instances before deleting. It is more expensive than regular bulk delete but ensures files are cleaned up.
Public API
All public symbols are importable from django_cfg.modules.django_cleanup:
from django_cfg.modules.django_cleanup import (
# Config
StorageCleanupConfig,
get_config,
clear_config_cache,
# Cleanup functions
cleanup_instance_files,
delete_file,
delete_file_by_name,
get_file_fields,
is_file_shared,
is_model_excluded,
has_soft_delete,
is_soft_deleted,
# Mixins (lazy loaded)
FileCleanupMixin,
FileCleanupQuerySet,
# Signal management
connect_signals_for_model,
disconnect_signals_for_model,
)Fallback: settings.DJANGO_STORAGE
If DjangoConfig is not used, configure via Django settings:
# settings.py
DJANGO_STORAGE = {
"auto_cleanup": True,
"delete_on_replace": True,
"log_deletions": True,
"exclude_models": ["backups.DatabaseBackup"],
"exclude_fields": ["documents.Contract.original_scan"],
"check_shared_files": False,
"respect_soft_delete": True,
"soft_delete_fields": ["deleted_at", "is_deleted"],
}Configuration priority: DjangoConfig.storage > settings.DJANGO_STORAGE > built-in defaults.
Troubleshooting
Files Not Being Deleted
- Check if model is excluded:
is_model_excluded(MyModel, get_config()) - Check if soft-delete is active:
is_soft_deleted(instance, get_config()) - Enable logging:
StorageConfig(log_deletions=True)
Files Deleted Unexpectedly
- Add model to exclusions:
exclude_models={"myapp.MyModel"} - Add field to exclusions:
exclude_fields={"myapp.MyModel.keep_this"}
TAGS: file-cleanup, storage, filefield, imagefield, signals DEPENDS_ON: [configuration]