Code

Ticket #2983: 2983.diff

File 2983.diff, 4.9 KB (added by SmileyChris, 5 years ago)
Line 
1Index: django/db/models/fields/files.py
2===================================================================
3--- django/db/models/fields/files.py    (revision 11327)
4+++ django/db/models/fields/files.py    (working copy)
5@@ -22,6 +22,7 @@
6         self.field = field
7         self.storage = field.storage
8         self._committed = True
9+        self._replaced = []
10 
11     def __eq__(self, other):
12         # Older code may be expecting FileField values to be simple strings.
13@@ -206,7 +207,20 @@
14         return instance.__dict__[self.field.name]
15 
16     def __set__(self, instance, value):
17+        if self.field.delete_replaced:
18+            previous_file = instance.__dict__.get(self.field.name)
19         instance.__dict__[self.field.name] = value
20+        if self.field.delete_replaced and previous_file:
21+            if previous_file:
22+                # Rather than just using value, we get the file from the
23+                # instance, so that the __get__ logic of the file descriptor is
24+                # processed. This ensures we will be dealing with a FileField
25+                # (or subclass of FileField) instance.
26+                file = getattr(instance, self.field.name)
27+                # Remember that the previous file was replaced (along with any
28+                # files which the previous file replaced too).
29+                file._replaced += previous_file._replaced
30+                file._replaced.append(previous_file)
31 
32 class FileField(Field):
33     # The class to wrap instance attributes in. Accessing the file object off
34@@ -216,7 +230,8 @@
35     # The descriptor to use for accessing the attribute off of the class.
36     descriptor_class = FileDescriptor
37 
38-    def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs):
39+    def __init__(self, verbose_name=None, name=None, upload_to='',
40+                 storage=None, delete_replaced=False, **kwargs):
41         for arg in ('primary_key', 'unique'):
42             if arg in kwargs:
43                 raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__))
44@@ -225,6 +240,7 @@
45         self.upload_to = upload_to
46         if callable(upload_to):
47             self.generate_filename = upload_to
48+        self.delete_replaced = delete_replaced
49 
50         kwargs['max_length'] = kwargs.get('max_length', 100)
51         super(FileField, self).__init__(verbose_name, name, **kwargs)
52@@ -247,9 +263,18 @@
53     def pre_save(self, model_instance, add):
54         "Returns field's value just before saving."
55         file = super(FileField, self).pre_save(model_instance, add)
56-        if file and not file._committed:
57-            # Commit the file to storage prior to saving the model
58-            file.save(file.name, file, save=False)
59+        if file:
60+            if self.delete_replaced:
61+                # Delete any files which this one replaced.
62+                queryset = model_instance._default_manager.exclude(
63+                                                    pk=model_instance.pk)
64+                for replaced_file in file._replaced:
65+                    if replaced_file._committed:
66+                        self.safe_delete_file(replaced_file, queryset)
67+                file._replaced = []
68+            if not file._committed:
69+                # Commit the file to storage prior to saving the model
70+                file.save(file.name, file, save=False)
71         return file
72 
73     def contribute_to_class(self, cls, name):
74@@ -258,16 +283,26 @@
75         signals.post_delete.connect(self.delete_file, sender=cls)
76 
77     def delete_file(self, instance, sender, **kwargs):
78+        """
79+        Signal receiver which deletes an attached file from the backend when
80+        the model is deleted.
81+        """
82         file = getattr(instance, self.attname)
83-        # If no other object of this type references the file,
84-        # and it's not the default value for future objects,
85-        # delete it from the backend.
86-        if file and file.name != self.default and \
87-            not sender._default_manager.filter(**{self.name: file.name}):
88+        self.safe_delete_file(file, sender._default_manager.all())
89+   
90+    def safe_delete_file(self, file, queryset):
91+        """
92+        Deletes the file from the backend if no objects in the queryset
93+        reference the file and it's not the default value for future objects.
94+       
95+        Otherwise, the file is simply closed so it doesn't tie up resources.
96+        """
97+        if file:
98+            queryset = queryset.filter(**{self.name: file.name})
99+            if file.name != self.default and not queryset:
100                 file.delete(save=False)
101-        elif file:
102-            # Otherwise, just close the file, so it doesn't tie up resources.
103-            file.close()
104+            else:
105+                file.close()
106 
107     def get_directory_name(self):
108         return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))