Ticket #11663: 11663.diff

File 11663.diff, 19.6 KB (added by Chris Beaven, 15 years ago)

Patch with comprehensive tests

  • tests/regressiontests/file_uploads/tests.py

    ### Eclipse Workspace Patch 1.0
    #P Django trunk
     
    1212from django.utils.hashcompat import sha_constructor
    1313from django.http.multipartparser import MultiPartParser
    1414
    15 from models import FileModel, temp_storage, UPLOAD_TO
     15from models import FileModel, FileModelDeleteReplaced, temp_storage, UPLOAD_TO
    1616import uploadhandler
    1717
    1818UNICODE_FILENAME = u'test-0123456789_中文_Orléans.jpg'
     
    302302            'CONTENT_TYPE':     'multipart/form-data; boundary=_foo',
    303303            'CONTENT_LENGTH':   '1'
    304304        }, StringIO('x'), [], 'utf-8')
     305
     306class DeleteReplacedTests(TestCase):
     307    def setUp(self):
     308        if not os.path.isdir(temp_storage.location):
     309            os.makedirs(temp_storage.location)
     310        if os.path.isdir(UPLOAD_TO):
     311            os.chmod(UPLOAD_TO, 0700)
     312            shutil.rmtree(UPLOAD_TO)
     313        self.file_a = SimpleUploadedFile('alpha.txt', 'A')
     314        self.file_b = SimpleUploadedFile('beta.txt', 'B')
     315        self.file_g = SimpleUploadedFile('gamma.txt', 'G')
     316
     317    def tearDown(self):
     318        os.chmod(temp_storage.location, 0700)
     319        shutil.rmtree(temp_storage.location)
     320
     321    def test_instance_track_replaced(self):
     322        """
     323        Setting a new file into a instance's ``FileField`` attribute keeps
     324        track of the old file in the new file's ``_replaced`` list.
     325        """
     326        obj = FileModel()
     327        obj.testfile = self.file_a
     328        fieldfile_a = obj.testfile
     329        self.assertEqual(obj.testfile._replaced, [])
     330        # After a save, nothing changes.
     331        obj.save()
     332        self.assertEqual(obj.testfile._replaced, [])
     333        # Set to B, B replaces A
     334        obj.testfile = self.file_b
     335        fieldfile_b = obj.testfile
     336        self.assertEqual(obj.testfile._replaced, [fieldfile_a])
     337        # Set to G, G replaces B (which in turn replaces A)
     338        obj.testfile = self.file_g
     339        fieldfile_g = obj.testfile
     340        self.assertEqual(obj.testfile._replaced, [fieldfile_b])
     341        self.assertEqual(obj.testfile._replaced[0]._replaced, [fieldfile_a])
     342
     343    def test_default_on_delete(self):
     344        """
     345        Deleting a FieldFile which has a default FileField doesn't delete
     346        replaced files.
     347        """
     348        obj = FileModel.objects.create(testfile=self.file_a)
     349        obj.testfile = self.file_b
     350        obj.testfile.delete()
     351        self.assertEqual(os.listdir(UPLOAD_TO), ['alpha.txt'])
     352
     353    def test_delete_replaced_on_delete(self):
     354        """
     355        Deleting a FieldFile which its FileField set ``delete_replaced=True``
     356        deletes replaced files (without any "safe" tests).
     357        """
     358        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     359        obj.testfile = self.file_b
     360        obj.testfile.delete()
     361        self.assertEqual(os.listdir(UPLOAD_TO), [])
     362        # Even if another instance has a reference to the file it will still be
     363        # deleted.
     364        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     365        obj2 = FileModelDeleteReplaced.objects.create(testfile=obj.testfile)
     366        obj.testfile = self.file_b
     367        obj.testfile.delete()
     368        self.assertEqual(os.listdir(UPLOAD_TO), [])
     369
     370    def test_default_on_safe_delete(self):
     371        """
     372        Calling ``safe_delete`` on a FieldFile which has a default FileField
     373        doesn't delete replaced files.
     374        """
     375        obj = FileModel.objects.create(testfile=self.file_a)
     376        obj.testfile = self.file_b
     377        obj.testfile.safe_delete()
     378        self.assertEqual(os.listdir(UPLOAD_TO), ['alpha.txt'])
     379        # If another instance has a reference to the file it won't be deleted.
     380        obj = FileModel.objects.create(testfile=self.file_b)
     381        obj.testfile = self.file_g
     382        obj2 = FileModel.objects.create(testfile=obj.testfile)
     383        obj.testfile.safe_delete()
     384        files = os.listdir(UPLOAD_TO)
     385        files.sort()
     386        self.assertEqual(files, ['alpha.txt', 'beta.txt', 'gamma.txt'])
     387
     388    def test_delete_replaced_on_safe_delete(self):
     389        """
     390        Deleting a FieldFile which its FileField set ``delete_replaced=True``
     391        "safely" deletes replaced files.
     392        """
     393        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     394        obj.testfile = self.file_b
     395        obj.testfile.safe_delete()
     396        self.assertEqual(os.listdir(UPLOAD_TO), [])
     397        # If another instance has a reference to the file it won't be deleted.
     398        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_b)
     399        obj.testfile = self.file_g
     400        obj2 = FileModelDeleteReplaced.objects.create(testfile=obj.testfile)
     401        obj.testfile.safe_delete()
     402        self.assertEqual(os.listdir(UPLOAD_TO), ['gamma.txt'])
     403
     404    def test_default_on_save(self):
     405        """
     406        Saving a FieldFile which has a default FileField doesn't delete
     407        replaced files.
     408        """
     409        obj = FileModel.objects.create(testfile=self.file_a)
     410        obj.testfile.save(name=self.file_b.name, content=self.file_b,
     411                          save=False)
     412        files = os.listdir(UPLOAD_TO)
     413        files.sort()
     414        self.assertEqual(files, ['alpha.txt', 'beta.txt'])
     415
     416    def test_delete_replaced_on_save(self):
     417        """
     418        Saving a FieldFile which its FileField set ``delete_replaced=True``
     419        deletes replaced files.
     420       
     421        The ``safe_delete_replaced`` attribute defaults to ``True``, which
     422        causes related files to be "safely" deleted. Setting to ``False``
     423        causes related files to be deleted without any "safe" tests.
     424        """
     425        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     426        obj.testfile.save(name=self.file_b.name, content=self.file_b,
     427                          save=False)
     428        self.assertEqual(os.listdir(UPLOAD_TO), ['beta.txt'])
     429        # If another instance has a reference to the file it won't be deleted.
     430        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     431        obj2 = FileModelDeleteReplaced.objects.create(testfile=obj.testfile)
     432        obj.testfile.save(name=self.file_g.name, content=self.file_g,
     433                          save=False)
     434        files = os.listdir(UPLOAD_TO)
     435        files.sort()
     436        self.assertEqual(files, ['alpha.txt', 'beta.txt', 'gamma.txt'])
     437
     438    def test_delete_replaced(self):
     439        """
     440        Calling the ``delete_replaced`` method of a ``FieldFile`` recursively
     441        deletes replaced files (regardless of the `FileField``'s
     442        ``delete_replaced`` attribute).
     443       
     444        The ``safe_delete`` argument defaults to ``True``, which causes related
     445        files to be "safely" deleted. Setting to ``False`` causes related files
     446        to be deleted without any "safe" tests.
     447        """
     448        obj = FileModel.objects.create(testfile=self.file_a)
     449        obj.testfile = self.file_b
     450        obj.save()
     451        obj.testfile = self.file_g
     452        obj.save()
     453        files = os.listdir(UPLOAD_TO)
     454        files.sort()
     455        self.assertEqual(files, ['alpha.txt', 'beta.txt', 'gamma.txt'])
     456        obj.testfile.delete_replaced()
     457        self.assertEqual(os.listdir(UPLOAD_TO), ['gamma.txt'])
     458
     459    def test_replace_avoids_loop(self):
     460        """
     461        Avoid an infinite loop when A replaces B which replaced A
     462        """
     463        obj = FileModel.objects.create(testfile=self.file_a)
     464        fieldfile_a = obj.testfile
     465        obj.testfile = self.file_b
     466        obj.save()
     467        obj.testfile = fieldfile_a
     468        obj.testfile.delete_replaced()
     469        self.assertEqual(os.listdir(UPLOAD_TO), ['alpha.txt'])
     470
     471    def test_instance_delete(self):
     472        """
     473        Deleting an instance deletes replaced files. For backwards
     474        compatibility, this is regardless of the `FileField``'s
     475        ``delete_replaced`` attribute. Files are only deleted if no other
     476        instance of the same model type references that file.
     477        """
     478        obj = FileModel.objects.create(testfile=self.file_a)
     479        obj.delete()
     480        self.assertEqual(os.listdir(UPLOAD_TO), [])
     481
     482        obj = FileModel.objects.create(testfile=self.file_a)
     483        obj2 = FileModel.objects.create(testfile=obj.testfile)
     484        self.assertEqual(os.listdir(UPLOAD_TO), ['alpha.txt'])
     485
     486    def test_default_instance_replace_file(self):
     487        """
     488        Saving a FieldFile which has a default FileField doesn't delete
     489        replaced files when the instance is saved.
     490        """
     491        obj = FileModel.objects.create(testfile=self.file_a)
     492        obj.testfile = self.file_b
     493        obj.save()
     494        files = os.listdir(UPLOAD_TO)
     495        files.sort()
     496        self.assertEqual(files, ['alpha.txt', 'beta.txt'])
     497
     498    def test_delete_replaced_instance_replace_file(self):
     499        """
     500        If the model's FileField sets ``delete_replaced=True``, replacing an
     501        instance's file with another file will cause the old file to be deleted
     502        when the instance is saved.
     503       
     504        Files are only deleted if no other instance of the same model type
     505        references that file.
     506        """
     507        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     508        obj.testfile = self.file_b
     509        obj.save()
     510        self.assertEqual(os.listdir(UPLOAD_TO), ['beta.txt'])
     511
     512        obj2 = FileModelDeleteReplaced.objects.create(testfile=obj.testfile)
     513        obj.testfile = self.file_g
     514        obj.save()
     515        files = os.listdir(UPLOAD_TO)
     516        files.sort()
     517        self.assertEqual(files, ['beta.txt', 'gamma.txt'])
  • django/db/models/fields/files.py

     
    2222        self.field = field
    2323        self.storage = field.storage
    2424        self._committed = True
     25        self._replaced = []
     26        self.delete_replaced_files = self.field.delete_replaced
    2527
    2628    def __eq__(self, other):
    2729        # Older code may be expecting FileField values to be simple strings.
     
    8688    # to further manipulate the underlying file, as well as update the
    8789    # associated model instance.
    8890
    89     def save(self, name, content, save=True):
     91    def save(self, name, content, save=True, safe_delete_replaced=True):
     92        if self.delete_replaced_files:
     93            self.delete_replaced(safe_delete=safe_delete_replaced)
     94            if self._committed:
     95                # Delete this file as well (since we're saving a new one).
     96                if safe_delete_replaced:
     97                    self.safe_delete(save=False)
     98                else:
     99                    self.delete(save=False)
     100
    90101        name = self.field.generate_filename(self.instance, name)
    91102        self.name = self.storage.save(name, content)
    92         setattr(self.instance, self.field.name, self.name)
     103        setattr(self.instance, self.field.name, self)
    93104
    94105        # Update the filesize cache
    95106        self._size = len(content)
     
    100111            self.instance.save()
    101112    save.alters_data = True
    102113
    103     def delete(self, save=True):
     114    def delete(self, save=True, _delete_replaced_files=None):
     115        """
     116        Deletes the file from the backend.
     117       
     118        If ``save`` is ``True`` (default), the file's instance will be saved
     119        after the file is deleted.
     120        """
     121        if ((_delete_replaced_files is not None and _delete_replaced_files) or
     122            (_delete_replaced_files is None and self.delete_replaced_files)):
     123            self.delete_replaced(safe_delete=False)
     124
    104125        # Only close the file if it's already open, which we know by the
    105126        # presence of self._file
    106127        if hasattr(self, '_file'):
     
    110131        self.storage.delete(self.name)
    111132
    112133        self.name = None
    113         setattr(self.instance, self.field.name, self.name)
     134        setattr(self.instance, self.field.name, self)
    114135
    115136        # Delete the filesize cache
    116137        if hasattr(self, '_size'):
     
    121142            self.instance.save()
    122143    delete.alters_data = True
    123144
     145    def safe_delete(self, save=True, queryset=None,
     146                    _delete_replaced_files=None):
     147        """
     148        Deletes the file from the backend if no objects in the queryset
     149        reference the file and it's not the default value for future objects.
     150
     151        Otherwise, the file is simply closed so it doesn't tie up resources.
     152       
     153        If ``save`` is ``True`` (default), the file's instance will be saved
     154        if the file is deleted.
     155
     156        Under most circumstances, ``queryset`` does not need to be passed -
     157        it will be calculated based on the current instance.
     158        """
     159        if ((_delete_replaced_files is not None and _delete_replaced_files) or
     160            (_delete_replaced_files is None and self.delete_replaced_files)):
     161            self.delete_replaced(safe_delete=True)
     162
     163        if queryset is None:
     164            queryset = self.instance._default_manager.all()
     165            if self.instance.pk:
     166                queryset = queryset.exclude(pk=self.instance.pk)
     167        queryset = queryset.filter(**{self.field.name: self.name})
     168
     169        if self.name != self.field.default and not queryset:
     170            self.delete(save=save, _delete_replaced_files=False)
     171        else:
     172            self.close()
     173    safe_delete.alters_data = True
     174
     175    def delete_replaced(self, safe_delete=True, _seen=None):
     176        seen = _seen or []
     177        seen.append(self.name)
     178        for file in self._replaced:
     179            if file._committed and file.name not in seen:
     180                file.delete_replaced(safe_delete=safe_delete, _seen=seen)
     181                if safe_delete:
     182                    file.safe_delete(save=False, _delete_replaced_files=False)
     183                else:
     184                    file.delete(save=False, _delete_replaced_files=False)
     185        self._replaced = []
     186    delete_replaced.alters_data = True
     187
    124188    def _get_closed(self):
    125189        file = getattr(self, '_file', None)
    126190        return file is None or file.closed
     
    136200        # it's attached to in order to work properly, but the only necessary
    137201        # data to be pickled is the file's name itself. Everything else will
    138202        # be restored later, by FileDescriptor below.
    139         return {'name': self.name, 'closed': False, '_committed': True, '_file': None}
     203        return {'name': self.name, 'closed': False, '_committed': True,
     204                '_file': None,
     205                'delete_replaced_files': self.delete_replaced_files}
    140206
    141207class FileDescriptor(object):
    142208    """
     
    194260            file_copy._committed = False
    195261            instance.__dict__[self.field.name] = file_copy
    196262
    197         # Finally, because of the (some would say boneheaded) way pickle works,
    198         # the underlying FieldFile might not actually itself have an associated
    199         # file. So we need to reset the details of the FieldFile in those cases.
     263        # Because of the (some would say boneheaded) way pickle works, the
     264        # underlying FieldFile might not actually itself have an associated
     265        # file. So we need to reset the details of the FieldFile in those
     266        # cases.
    200267        elif isinstance(file, FieldFile) and not hasattr(file, 'field'):
    201268            file.instance = instance
    202269            file.field = self.field
    203270            file.storage = self.field.storage
    204271
     272        # Finally, the file set may have been a FieldFile from another
     273        # instance, so copy it if the instance doesn't match.
     274        elif isinstance(file, FieldFile) and file.instance != instance:
     275            file_copy = self.field.attr_class(instance, self.field, file.name)
     276            file_copy.file = file
     277            file_copy._committed = file._committed
     278            instance.__dict__[self.field.name] = file_copy
     279
    205280        # That was fun, wasn't it?
    206281        return instance.__dict__[self.field.name]
    207282
    208283    def __set__(self, instance, value):
     284        if self.field.name in instance.__dict__:
     285            previous_file = getattr(instance, self.field.name)
     286        else:
     287            previous_file = None
    209288        instance.__dict__[self.field.name] = value
     289        if previous_file:
     290            # Rather than just using value, we get the file from the instance,
     291            # so that the __get__ logic of the file descriptor is processed.
     292            # This ensures we will be dealing with a FileField (or subclass of
     293            # FileField) instance.
     294            file = getattr(instance, self.field.name)
     295            if previous_file is not file:
     296                # Remember that the previous file was replaced.
     297                file._replaced.append(previous_file)
    210298
    211299class FileField(Field):
    212300    # The class to wrap instance attributes in. Accessing the file object off
     
    216304    # The descriptor to use for accessing the attribute off of the class.
    217305    descriptor_class = FileDescriptor
    218306
    219     def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs):
     307    def __init__(self, verbose_name=None, name=None, upload_to='',
     308                 storage=None, delete_replaced=False, **kwargs):
    220309        for arg in ('primary_key', 'unique'):
    221310            if arg in kwargs:
    222311                raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__))
     
    225314        self.upload_to = upload_to
    226315        if callable(upload_to):
    227316            self.generate_filename = upload_to
     317        self.delete_replaced = delete_replaced
    228318
    229319        kwargs['max_length'] = kwargs.get('max_length', 100)
    230320        super(FileField, self).__init__(verbose_name, name, **kwargs)
     
    258348        signals.post_delete.connect(self.delete_file, sender=cls)
    259349
    260350    def delete_file(self, instance, sender, **kwargs):
     351        """
     352        Signal receiver which deletes an attached file from the backend when
     353        the model is deleted.
     354        """
    261355        file = getattr(instance, self.attname)
    262         # If no other object of this type references the file,
    263         # and it's not the default value for future objects,
    264         # delete it from the backend.
    265         if file and file.name != self.default and \
    266             not sender._default_manager.filter(**{self.name: file.name}):
    267                 file.delete(save=False)
    268         elif file:
    269             # Otherwise, just close the file, so it doesn't tie up resources.
    270             file.close()
     356        if file:
     357            file.safe_delete(save=False,
     358                             queryset=sender._default_manager.all())
    271359
    272360    def get_directory_name(self):
    273361        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
  • tests/regressiontests/file_uploads/models.py

     
    44from django.core.files.storage import FileSystemStorage
    55
    66temp_storage = FileSystemStorage(tempfile.mkdtemp())
    7 UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload')
     7UPLOAD_TO_NAME = 'test_upload'
     8UPLOAD_TO = os.path.join(temp_storage.location, UPLOAD_TO_NAME)
    89
    910class FileModel(models.Model):
    10     testfile = models.FileField(storage=temp_storage, upload_to='test_upload')
     11    testfile = models.FileField(storage=temp_storage, upload_to=UPLOAD_TO_NAME)
     12
     13class FileModelDeleteReplaced(models.Model):
     14    testfile = models.FileField(storage=temp_storage, upload_to=UPLOAD_TO_NAME,
     15                                delete_replaced=True)
Back to Top