1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
|
'''
Signal handlers to manage FileField files.
'''
import logging
from django.db.models.signals import post_delete, post_init, post_save, pre_save
from django.db.transaction import on_commit
from . import cache
from .signals import cleanup_post_delete, cleanup_pre_delete
logger = logging.getLogger(__name__)
class FakeInstance:
'''A Fake model instance to ensure an instance is not modified'''
def cache_original_post_init(sender, instance, **kwargs):
'''Post_init on all models with file fields, saves original values'''
cache.make_cleanup_cache(instance)
def fallback_pre_save(sender, instance, raw, update_fields, using, **kwargs):
'''Fallback to the database to remake the cleanup cache if there is none'''
if raw: # pragma: no cover
return
if instance.pk and not cache.has_cache(instance):
try:
db_instance = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist: # pragma: no cover
return
cache.make_cleanup_cache(instance, source=db_instance)
def delete_old_post_save(sender, instance, raw, created, update_fields, using,
**kwargs):
'''Post_save on all models with file fields, deletes old files'''
if raw:
return
if not created:
for field_name, new_file in cache.fields_for_model_instance(instance):
if update_fields is None or field_name in update_fields:
old_file = cache.get_field_attr(instance, field_name)
if old_file != new_file:
delete_file(instance, field_name, old_file, using)
# reset cache
cache.make_cleanup_cache(instance)
def delete_all_post_delete(sender, instance, using, **kwargs):
'''Post_delete on all models with file fields, deletes all files'''
for field_name, file_ in cache.fields_for_model_instance(instance):
delete_file(instance, field_name, file_, using)
def delete_file(instance, field_name, file_, using):
'''Deletes a file'''
if not file_.name:
return
# add a fake instance to the file being deleted to avoid
# any changes to the real instance.
file_.instance = FakeInstance()
# pickled filefields lose lots of data, and contrary to how it is
# documented, the file descriptor does not recover them
model_name = cache.get_model_name(instance)
# recover the 'field' if necessary
if not hasattr(file_, 'field'):
file_.field = cache.get_field(model_name, field_name)()
file_.field.name = field_name
# if our file name is default don't delete
default = file_.field.default if not callable(file_.field.default) else file_.field.default()
if file_.name == default:
return
# recover the 'storage' if necessary
if not hasattr(file_, 'storage'):
file_.storage = cache.get_field_storage(model_name, field_name)()
# this will run after a successful commit
# assuming you are in a transaction and on a database that supports
# transactions, otherwise it will run immediately
def run_on_commit():
cleanup_pre_delete.send(sender=None, file=file_)
try:
file_.delete(save=False)
except Exception:
opts = instance._meta
logger.exception(
'There was an exception deleting the file `%s` on field `%s.%s.%s`',
file_, opts.app_label, opts.model_name, field_name)
cleanup_post_delete.send(sender=None, file=file_)
on_commit(run_on_commit, using)
def connect():
'''Connect signals to the cleanup models'''
for model in cache.cleanup_models():
key = '{{}}_django_cleanup_{}'.format(cache.get_model_name(model))
post_init.connect(cache_original_post_init, sender=model,
dispatch_uid=key.format('post_init'))
pre_save.connect(fallback_pre_save, sender=model,
dispatch_uid=key.format('pre_save'))
post_save.connect(delete_old_post_save, sender=model,
dispatch_uid=key.format('post_save'))
post_delete.connect(delete_all_post_delete, sender=model,
dispatch_uid=key.format('post_delete'))
|