-
Notifications
You must be signed in to change notification settings - Fork 77
/
handlers.py
124 lines (91 loc) · 4.2 KB
/
handlers.py
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
120
121
122
123
124
# coding: utf-8
'''
Signal handlers to manage FileField files.
'''
from __future__ import unicode_literals
import logging
from django.db.models.signals import post_delete, post_init, post_save, pre_save
from . import cache
from .signals import cleanup_post_delete, cleanup_pre_delete
try:
from django.db.transaction import on_commit
except ImportError:
# remove after django 1.8 is deprecated(which will be awhile since it's LTS)
def on_commit(func, using=None):
func()
logger = logging.getLogger(__name__)
class FakeInstance(object):
'''A Fake model instance to ensure an instance is not modified'''
pass
def ensure_delete_ready(instance, field_name, file_):
'''Ensure the file is ready for deletion'''
# add a fake instance to the file being deleted to avoid
# any changes to the real instance.
file_.instance = FakeInstance()
model_name = cache.get_model_name(instance)
# pickled filefields lose lots of data, and contrary to how it is
# documented, the file descriptor does not recover them
if not hasattr(file_, 'field'):
file_.field = cache.get_field(model_name, field_name)()
file_.field.name = field_name
if not hasattr(file_, 'storage'):
file_.storage = cache.get_field_storage(model_name, field_name)()
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 or created:
return
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
# this will run after a successful commit for django 1.9+
# 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.warning(
'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_)
ensure_delete_ready(instance, field_name, file_)
on_commit(run_on_commit, using)
def connect():
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'))