Skip to Content
FeaturesModulesFile Cleanup

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

TriggerBehavior
Instance deletedAll file fields on the instance are deleted from storage
File field replacedOld file is deleted when a new value is saved
Soft-deleted instanceFiles are not deleted — soft-delete fields are detected automatically
Model in exclude_modelsSignals not connected — files never touched
Field in exclude_fieldsThat 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

FieldTypeDefaultDescription
enabledboolTrueEnable the cleanup module entirely
auto_cleanupboolTrueRegister signals for all models with file fields at startup
delete_on_replaceboolTrueDelete old file when a field is updated with a new value
exclude_modelsset[str]set()Models to skip. Format: "app_label.ModelName"
exclude_fieldsset[str]set()Fields to skip. Format: "app_label.ModelName.field_name"
log_deletionsboolFalseLog each deleted file at INFO level
check_shared_filesboolFalseCheck if another record references the same file before deleting
respect_soft_deleteboolTrueSkip file deletion for soft-deleted instances
soft_delete_fieldsset[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)
  • S3Boto3Storage from django-storages
  • GoogleCloudStorage from django-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 fields

Bulk 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

  1. Check if model is excluded: is_model_excluded(MyModel, get_config())
  2. Check if soft-delete is active: is_soft_deleted(instance, get_config())
  3. Enable logging: StorageConfig(log_deletions=True)

Files Deleted Unexpectedly

  1. Add model to exclusions: exclude_models={"myapp.MyModel"}
  2. Add field to exclusions: exclude_fields={"myapp.MyModel.keep_this"}

TAGS: file-cleanup, storage, filefield, imagefield, signals DEPENDS_ON: [configuration]

Last updated on