Ticket #11663: 11663.2.diff

File 11663.2.diff, 21.0 KB (added by Chris Beaven, 15 years ago)
  • 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 with a standard FileField doesn't delete replaced
     346        files by default. To delete replaced files explicitly, you can set
     347        ``delete_replaced_files=True``.
     348        """
     349        obj = FileModel.objects.create(testfile=self.file_a)
     350        obj.testfile = self.file_b
     351        obj.testfile.delete()
     352        self.assertEqual(os.listdir(UPLOAD_TO), ['alpha.txt'])
     353        # Explicitly delete replaced files.
     354        obj.testfile.delete(delete_replaced_files=True)
     355        self.assertEqual(os.listdir(UPLOAD_TO), [])
     356
     357    def test_delete_replaced_on_delete(self):
     358        """
     359        Deleting a FieldFile which its FileField set ``delete_replaced=True``
     360        deletes replaced files which are orphans.
     361        """
     362        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     363        obj.testfile = self.file_b
     364        obj.testfile.delete()
     365        self.assertEqual(os.listdir(UPLOAD_TO), [])
     366        # If another instance has a reference to the file it won't be deleted.
     367        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     368        obj2 = FileModelDeleteReplaced.objects.create(testfile=obj.testfile)
     369        obj.testfile = self.file_b
     370        obj.testfile.delete()
     371        self.assertEqual(os.listdir(UPLOAD_TO), ['alpha.txt'])
     372
     373    def test_default_on_delete_if_orphan(self):
     374        """
     375        Calling ``delete_if_orphan`` on a FieldFile with a standard FileField
     376        doesn't delete replaced files by default. To delete replaced files
     377        explicitly, you can set ``delete_replaced_files=True``.
     378        """
     379        obj = FileModel.objects.create(testfile=self.file_a)
     380        obj.testfile = self.file_b
     381        obj.testfile.delete_if_orphan()
     382        expected = ['alpha.txt']
     383        self.assertEqual(os.listdir(UPLOAD_TO), expected)
     384        # If another instance has a reference to the file it won't be deleted.
     385        obj = FileModel.objects.create(testfile=self.file_b)
     386        obj.testfile = self.file_g
     387        obj2 = FileModel.objects.create(testfile=obj.testfile)
     388        obj.testfile.delete_if_orphan()
     389        files = os.listdir(UPLOAD_TO)
     390        files.sort()
     391        expected.extend(['beta.txt', 'gamma.txt'])
     392        self.assertEqual(files, expected)
     393        # Explicitly delete replaced files.
     394        obj.testfile.delete(delete_replaced_files=True)
     395        expected.remove('beta.txt')
     396        self.assertEqual(os.listdir(UPLOAD_TO), expected)
     397
     398    def test_delete_replaced_on_delete_if_orphan(self):
     399        """
     400        Deleting a FieldFile which its FileField set ``delete_replaced=True``
     401        deletes replaced files which are orphans.
     402        """
     403        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     404        obj.testfile = self.file_b
     405        obj.testfile.delete()
     406        self.assertEqual(os.listdir(UPLOAD_TO), [])
     407        # If another instance has a reference to the file it won't be deleted.
     408        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_b)
     409        obj.testfile = self.file_g
     410        obj2 = FileModelDeleteReplaced.objects.create(testfile=obj.testfile)
     411        obj.testfile.delete_if_orphan()
     412        self.assertEqual(os.listdir(UPLOAD_TO), ['gamma.txt'])
     413
     414    def test_default_on_save(self):
     415        """
     416        Saving a FieldFile with a standard FileField doesn't delete replaced
     417        files.
     418        """
     419        obj = FileModel.objects.create(testfile=self.file_a)
     420        obj.testfile.save(name=self.file_b.name, content=self.file_b,
     421                          save=False)
     422        files = os.listdir(UPLOAD_TO)
     423        files.sort()
     424        self.assertEqual(files, ['alpha.txt', 'beta.txt'])
     425
     426    def test_delete_replaced_on_save(self):
     427        """
     428        Saving a FieldFile which its FileField set ``delete_replaced=True``
     429        deletes replaced files which are orphans.
     430        """
     431        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     432        obj.testfile.save(name=self.file_b.name, content=self.file_b,
     433                          save=False)
     434        expected = ['beta.txt']
     435        self.assertEqual(os.listdir(UPLOAD_TO), expected)
     436        # If another instance has a reference to the file it won't be deleted.
     437        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     438        obj2 = FileModelDeleteReplaced.objects.create(testfile=obj.testfile)
     439        obj.testfile.save(name=self.file_g.name, content=self.file_g,
     440                          save=False)
     441        files = os.listdir(UPLOAD_TO)
     442        files.sort()
     443        expected.extend(['alpha.txt', 'gamma.txt'])
     444        expected.sort()
     445        self.assertEqual(files, expected)
     446
     447    def test_delete_replaced(self):
     448        """
     449        Explicitly calling the ``delete_replaced`` method of a ``FieldFile``
     450        recursively deletes replaced files which are orphans.
     451       
     452        This happens regardless of the `FileField``'s ``delete_replaced``
     453        attribute.
     454       
     455        To delete all replaced files without considering if they are orphans,
     456        set ``only_orphans=False``.
     457        """
     458        obj = FileModel.objects.create(testfile=self.file_a)
     459        obj.testfile = self.file_b
     460        obj.save()
     461        obj2 = FileModel.objects.create(testfile=obj.testfile)
     462        obj.testfile = self.file_g
     463        obj.save()
     464        files = os.listdir(UPLOAD_TO)
     465        files.sort()
     466        self.assertEqual(files, ['alpha.txt', 'beta.txt', 'gamma.txt'])
     467        # Now delete_replaced.
     468        obj.testfile.delete_replaced()
     469        files = os.listdir(UPLOAD_TO)
     470        files.sort()
     471        self.assertEqual(files, ['beta.txt', 'gamma.txt'])
     472
     473    def test_delete_replaced_all(self):
     474        """
     475        Calling the ``delete_replaced`` method of a ``FieldFile`` with
     476        ``only_orphans=False`` recursively deletes replaced files without
     477        considering if they are orphans.
     478        """
     479        obj = FileModel.objects.create(testfile=self.file_a)
     480        obj.testfile = self.file_b
     481        obj.save()
     482        obj2 = FileModel.objects.create(testfile=obj.testfile)
     483        obj.testfile = self.file_g
     484        obj.save()
     485        files = os.listdir(UPLOAD_TO)
     486        files.sort()
     487        self.assertEqual(files, ['alpha.txt', 'beta.txt', 'gamma.txt'])
     488        # Now delete_replaced.
     489        obj.testfile.delete_replaced(only_orphans=False)
     490        self.assertEqual(os.listdir(UPLOAD_TO), ['gamma.txt'])
     491
     492    def test_replace_avoids_loop(self):
     493        """
     494        Avoid an infinite loop when A replaces B which replaced A
     495        """
     496        obj = FileModel.objects.create(testfile=self.file_a)
     497        fieldfile_a = obj.testfile
     498        obj.testfile = self.file_b
     499        obj.save()
     500        obj.testfile = fieldfile_a
     501        obj.testfile.delete_replaced()
     502        self.assertEqual(os.listdir(UPLOAD_TO), ['alpha.txt'])
     503
     504    def test_instance_delete(self):
     505        """
     506        Deleting an instance deletes replaced files. For backwards
     507        compatibility, this is regardless of the `FileField``'s
     508        ``delete_replaced`` attribute. Files are only deleted if no other
     509        instance of the same model type references that file.
     510        """
     511        obj = FileModel.objects.create(testfile=self.file_a)
     512        obj.delete()
     513        self.assertEqual(os.listdir(UPLOAD_TO), [])
     514
     515        obj = FileModel.objects.create(testfile=self.file_a)
     516        obj2 = FileModel.objects.create(testfile=obj.testfile)
     517        self.assertEqual(os.listdir(UPLOAD_TO), ['alpha.txt'])
     518
     519    def test_default_instance_replace_file(self):
     520        """
     521        Saving a model with a standard FileField doesn't delete replaced files
     522        when the instance is saved.
     523        """
     524        obj = FileModel.objects.create(testfile=self.file_a)
     525        obj.testfile = self.file_b
     526        obj.save()
     527        files = os.listdir(UPLOAD_TO)
     528        files.sort()
     529        self.assertEqual(files, ['alpha.txt', 'beta.txt'])
     530
     531    def test_delete_replaced_instance_replace_file(self):
     532        """
     533        If the model's FileField sets ``delete_replaced=True``, replacing an
     534        instance's file with another file will cause the old file to be deleted
     535        when the instance is saved.
     536       
     537        Files are only deleted if no other instance of the same model type
     538        references that file.
     539        """
     540        obj = FileModelDeleteReplaced.objects.create(testfile=self.file_a)
     541        obj.testfile = self.file_b
     542        obj.save()
     543        self.assertEqual(os.listdir(UPLOAD_TO), ['beta.txt'])
     544
     545        obj2 = FileModelDeleteReplaced.objects.create(testfile=obj.testfile)
     546        obj.testfile = self.file_g
     547        obj.save()
     548        files = os.listdir(UPLOAD_TO)
     549        files.sort()
     550        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.
     
    8789    # associated model instance.
    8890
    8991    def save(self, name, content, save=True):
     92        if self.delete_replaced_files:
     93            self.delete_replaced()
     94            if self._committed:
     95                # Delete this file as well (since we're saving a new one).
     96                self.delete_if_orphan(save=False)
     97
    9098        name = self.field.generate_filename(self.instance, name)
    9199        self.name = self.storage.save(name, content)
    92         setattr(self.instance, self.field.name, self.name)
     100        setattr(self.instance, self.field.name, self)
    93101
    94102        # Update the filesize cache
    95103        self._size = len(content)
     
    100108            self.instance.save()
    101109    save.alters_data = True
    102110
    103     def delete(self, save=True):
     111    def delete(self, save=True, delete_replaced_files=None):
     112        """
     113        Deletes the file from the backend.
     114       
     115        If ``save`` is ``True`` (default), the file's instance will be saved
     116        after the file is deleted.
     117       
     118        ``delete_replaced_files`` determines whether to also delete replaced
     119        files which are orphans. If set to ``None``, the field's default
     120        setting applies.
     121        """
     122        if delete_replaced_files is None:
     123            delete_replaced_files = self.delete_replaced_files
     124        if delete_replaced_files:
     125            self.delete_replaced()
     126
    104127        # Only close the file if it's already open, which we know by the
    105128        # presence of self._file
    106129        if hasattr(self, '_file'):
     
    110133        self.storage.delete(self.name)
    111134
    112135        self.name = None
    113         setattr(self.instance, self.field.name, self.name)
     136        setattr(self.instance, self.field.name, self)
    114137
    115138        # Delete the filesize cache
    116139        if hasattr(self, '_size'):
     
    121144            self.instance.save()
    122145    delete.alters_data = True
    123146
     147    def delete_if_orphan(self, save=True, queryset=None,
     148                         delete_replaced_files=None):
     149        """
     150        Deletes the file from the backend if no objects in the queryset
     151        reference the file and it's not the default value for future objects.
     152
     153        Otherwise, the file is simply closed so it doesn't tie up resources.
     154       
     155        If ``save`` is ``True`` (default), the file's instance will be saved
     156        if the file is deleted.
     157
     158        Under most circumstances, ``queryset`` does not need to be passed -
     159        it will be calculated based on the current instance.
     160       
     161        ``delete_replaced_files`` determines whether to also delete replaced
     162        files which are orphans. If set to ``None``, the field's default
     163        setting applies.
     164        """
     165        if delete_replaced_files is None:
     166            delete_replaced_files = self.delete_replaced_files
     167        if delete_replaced_files:
     168            self.delete_replaced()
     169
     170        if queryset is None:
     171            queryset = self.instance._default_manager.all()
     172            if self.instance.pk:
     173                queryset = queryset.exclude(pk=self.instance.pk)
     174        queryset = queryset.filter(**{self.field.name: self.name})
     175
     176        if self.name != self.field.default and not queryset:
     177            self.delete(save=save, delete_replaced_files=False)
     178        else:
     179            self.close()
     180    delete_if_orphan.alters_data = True
     181
     182    def delete_replaced(self, only_orphans=True, _seen=None):
     183        seen = _seen or []
     184        seen.append(self.name)
     185        for file in self._replaced:
     186            if file._committed and file.name not in seen:
     187                file.delete_replaced(only_orphans=only_orphans, _seen=seen)
     188                if only_orphans:
     189                    file.delete_if_orphan(save=False,
     190                                          delete_replaced_files=False)
     191                else:
     192                    file.delete(save=False, delete_replaced_files=False)
     193        self._replaced = []
     194    delete_replaced.alters_data = True
     195
    124196    def _get_closed(self):
    125197        file = getattr(self, '_file', None)
    126198        return file is None or file.closed
     
    136208        # it's attached to in order to work properly, but the only necessary
    137209        # data to be pickled is the file's name itself. Everything else will
    138210        # be restored later, by FileDescriptor below.
    139         return {'name': self.name, 'closed': False, '_committed': True, '_file': None}
     211        return {'name': self.name, 'closed': False, '_committed': True,
     212                '_file': None,
     213                'delete_replaced_files': self.delete_replaced_files}
    140214
    141215class FileDescriptor(object):
    142216    """
     
    194268            file_copy._committed = False
    195269            instance.__dict__[self.field.name] = file_copy
    196270
    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.
     271        # Because of the (some would say boneheaded) way pickle works, the
     272        # underlying FieldFile might not actually itself have an associated
     273        # file. So we need to reset the details of the FieldFile in those
     274        # cases.
    200275        elif isinstance(file, FieldFile) and not hasattr(file, 'field'):
    201276            file.instance = instance
    202277            file.field = self.field
    203278            file.storage = self.field.storage
    204279
     280        # Finally, the file set may have been a FieldFile from another
     281        # instance, so copy it if the instance doesn't match.
     282        elif isinstance(file, FieldFile) and file.instance != instance:
     283            file_copy = self.field.attr_class(instance, self.field, file.name)
     284            file_copy.file = file
     285            file_copy._committed = file._committed
     286            instance.__dict__[self.field.name] = file_copy
     287
    205288        # That was fun, wasn't it?
    206289        return instance.__dict__[self.field.name]
    207290
    208291    def __set__(self, instance, value):
     292        if self.field.name in instance.__dict__:
     293            previous_file = getattr(instance, self.field.name)
     294        else:
     295            previous_file = None
    209296        instance.__dict__[self.field.name] = value
     297        if previous_file:
     298            # Rather than just using value, we get the file from the instance,
     299            # so that the __get__ logic of the file descriptor is processed.
     300            # This ensures we will be dealing with a FileField (or subclass of
     301            # FileField) instance.
     302            file = getattr(instance, self.field.name)
     303            if previous_file is not file:
     304                # Remember that the previous file was replaced.
     305                file._replaced.append(previous_file)
    210306
    211307class FileField(Field):
    212308    # The class to wrap instance attributes in. Accessing the file object off
     
    216312    # The descriptor to use for accessing the attribute off of the class.
    217313    descriptor_class = FileDescriptor
    218314
    219     def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs):
     315    def __init__(self, verbose_name=None, name=None, upload_to='',
     316                 storage=None, delete_replaced=False, **kwargs):
    220317        for arg in ('primary_key', 'unique'):
    221318            if arg in kwargs:
    222319                raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__))
     
    225322        self.upload_to = upload_to
    226323        if callable(upload_to):
    227324            self.generate_filename = upload_to
     325        self.delete_replaced = delete_replaced
    228326
    229327        kwargs['max_length'] = kwargs.get('max_length', 100)
    230328        super(FileField, self).__init__(verbose_name, name, **kwargs)
     
    258356        signals.post_delete.connect(self.delete_file, sender=cls)
    259357
    260358    def delete_file(self, instance, sender, **kwargs):
     359        """
     360        Signal receiver which deletes an attached file from the backend when
     361        the model is deleted.
     362        """
    261363        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()
     364        if file:
     365            file.delete_if_orphan(save=False,
     366                                  queryset=sender._default_manager.all())
    271367
    272368    def get_directory_name(self):
    273369        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