File: handlers.py

package info (click to toggle)
django-cleanup 5.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 144 kB
  • sloc: python: 270; makefile: 3
file content (119 lines) | stat: -rw-r--r-- 4,239 bytes parent folder | download
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'))