Ticket #11663: 11663.3.diff
File 11663.3.diff, 23.1 KB (added by , 14 years ago) |
---|
-
django/db/models/fields/files.py
23 23 self.field = field 24 24 self.storage = field.storage 25 25 self._committed = True 26 self._replaced = [] 27 self.delete_replaced_files = self.field.delete_replaced 26 28 27 29 def __eq__(self, other): 28 30 # Older code may be expecting FileField values to be simple strings. … … 88 90 # associated model instance. 89 91 90 92 def save(self, name, content, save=True): 93 if self.delete_replaced_files: 94 self.delete_replaced() 95 if self._committed: 96 # Delete this file as well (since we're saving a new one). 97 self.delete_if_orphan(save=False) 98 91 99 name = self.field.generate_filename(self.instance, name) 92 100 self.name = self.storage.save(name, content) 93 setattr(self.instance, self.field.name, self .name)101 setattr(self.instance, self.field.name, self) 94 102 95 103 # Update the filesize cache 96 104 self._size = len(content) … … 101 109 self.instance.save() 102 110 save.alters_data = True 103 111 104 def delete(self, save=True): 112 def delete(self, save=True, delete_replaced_files=None): 113 """ 114 Deletes the file from the backend. 115 116 If ``save`` is ``True`` (default), the file's instance will be saved 117 after the file is deleted. 118 119 ``delete_replaced_files`` determines whether to also delete replaced 120 files which are orphans. If set to ``None``, the field's default 121 setting applies. 122 """ 123 if delete_replaced_files is None: 124 delete_replaced_files = self.delete_replaced_files 125 if delete_replaced_files: 126 self.delete_replaced() 127 105 128 # Only close the file if it's already open, which we know by the 106 129 # presence of self._file 107 130 if hasattr(self, '_file'): … … 111 134 self.storage.delete(self.name) 112 135 113 136 self.name = None 114 setattr(self.instance, self.field.name, self .name)137 setattr(self.instance, self.field.name, self) 115 138 116 139 # Delete the filesize cache 117 140 if hasattr(self, '_size'): … … 122 145 self.instance.save() 123 146 delete.alters_data = True 124 147 148 def delete_if_orphan(self, save=True, queryset=None, 149 delete_replaced_files=None): 150 """ 151 Deletes the file from the backend if no objects in the queryset 152 reference the file and it's not the default value for future objects. 153 154 Otherwise, the file is simply closed so it doesn't tie up resources. 155 156 If ``save`` is ``True`` (default), the file's instance will be saved 157 if the file is deleted. 158 159 Under most circumstances, ``queryset`` does not need to be passed - 160 it will be calculated based on the current instance. 161 162 ``delete_replaced_files`` determines whether to also delete replaced 163 files which are orphans. If set to ``None``, the field's default 164 setting applies. 165 """ 166 if delete_replaced_files is None: 167 delete_replaced_files = self.delete_replaced_files 168 if delete_replaced_files: 169 self.delete_replaced() 170 171 if queryset is None: 172 queryset = self.instance._default_manager.all() 173 if self.instance.pk: 174 queryset = queryset.exclude(pk=self.instance.pk) 175 queryset = queryset.filter(**{self.field.name: self.name}) 176 177 if self.name != self.field.default and not queryset: 178 self.delete(save=save, delete_replaced_files=False) 179 else: 180 self.close() 181 delete_if_orphan.alters_data = True 182 183 def delete_replaced(self, only_orphans=True, _seen=None): 184 seen = _seen or [] 185 seen.append(self.name) 186 for file in self._replaced: 187 if file._committed and file.name not in seen: 188 file.delete_replaced(only_orphans=only_orphans, _seen=seen) 189 if only_orphans: 190 file.delete_if_orphan(save=False, 191 delete_replaced_files=False) 192 else: 193 file.delete(save=False, delete_replaced_files=False) 194 self._replaced = [] 195 delete_replaced.alters_data = True 196 125 197 def _get_closed(self): 126 198 file = getattr(self, '_file', None) 127 199 return file is None or file.closed … … 137 209 # it's attached to in order to work properly, but the only necessary 138 210 # data to be pickled is the file's name itself. Everything else will 139 211 # be restored later, by FileDescriptor below. 140 return {'name': self.name, 'closed': False, '_committed': True, '_file': None} 212 return {'name': self.name, 'closed': False, '_committed': True, 213 '_file': None, 214 'delete_replaced_files': self.delete_replaced_files} 141 215 142 216 class FileDescriptor(object): 143 217 """ … … 195 269 file_copy._committed = False 196 270 instance.__dict__[self.field.name] = file_copy 197 271 198 # Finally, because of the (some would say boneheaded) way pickle works, 199 # the underlying FieldFile might not actually itself have an associated 200 # file. So we need to reset the details of the FieldFile in those cases. 272 # Because of the (some would say boneheaded) way pickle works, the 273 # underlying FieldFile might not actually itself have an associated 274 # file. So we need to reset the details of the FieldFile in those 275 # cases. 201 276 elif isinstance(file, FieldFile) and not hasattr(file, 'field'): 202 277 file.instance = instance 203 278 file.field = self.field 204 279 file.storage = self.field.storage 205 280 281 # Finally, the file set may have been a FieldFile from another 282 # instance, so copy it if the instance doesn't match. 283 elif isinstance(file, FieldFile) and file.instance != instance: 284 file_copy = self.field.attr_class(instance, self.field, file.name) 285 file_copy.file = file 286 file_copy._committed = file._committed 287 instance.__dict__[self.field.name] = file_copy 288 206 289 # That was fun, wasn't it? 207 290 return instance.__dict__[self.field.name] 208 291 209 292 def __set__(self, instance, value): 293 if self.field.name in instance.__dict__: 294 previous_file = getattr(instance, self.field.name) 295 else: 296 previous_file = None 210 297 instance.__dict__[self.field.name] = value 298 if previous_file: 299 # Rather than just using value, we get the file from the instance, 300 # so that the __get__ logic of the file descriptor is processed. 301 # This ensures we will be dealing with a FileField (or subclass of 302 # FileField) instance. 303 file = getattr(instance, self.field.name) 304 if previous_file is not file: 305 # Remember that the previous file was replaced. 306 file._replaced.append(previous_file) 211 307 212 308 class FileField(Field): 213 309 # The class to wrap instance attributes in. Accessing the file object off … … 219 315 220 316 description = ugettext_lazy("File path") 221 317 222 def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs): 318 def __init__(self, verbose_name=None, name=None, upload_to='', 319 storage=None, delete_replaced=False, **kwargs): 223 320 for arg in ('primary_key', 'unique'): 224 321 if arg in kwargs: 225 322 raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__)) … … 228 325 self.upload_to = upload_to 229 326 if callable(upload_to): 230 327 self.generate_filename = upload_to 328 self.delete_replaced = delete_replaced 231 329 232 330 kwargs['max_length'] = kwargs.get('max_length', 100) 233 331 super(FileField, self).__init__(verbose_name, name, **kwargs) … … 261 359 signals.post_delete.connect(self.delete_file, sender=cls) 262 360 263 361 def delete_file(self, instance, sender, **kwargs): 362 """ 363 Signal receiver which deletes an attached file from the backend when 364 the model is deleted. 365 """ 264 366 file = getattr(instance, self.attname) 265 # If no other object of this type references the file, 266 # and it's not the default value for future objects, 267 # delete it from the backend. 268 if file and file.name != self.default and \ 269 not sender._default_manager.filter(**{self.name: file.name}): 270 file.delete(save=False) 271 elif file: 272 # Otherwise, just close the file, so it doesn't tie up resources. 273 file.close() 367 if file: 368 file.delete_if_orphan(save=False, 369 queryset=sender._default_manager.all()) 274 370 275 371 def get_directory_name(self): 276 372 return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to)))) -
tests/regressiontests/file_uploads/models.py
4 4 from django.core.files.storage import FileSystemStorage 5 5 6 6 temp_storage = FileSystemStorage(tempfile.mkdtemp()) 7 UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload') 7 UPLOAD_TO_NAME = 'test_upload' 8 UPLOAD_TO = os.path.join(temp_storage.location, UPLOAD_TO_NAME) 8 9 9 10 class 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 13 class FileModelDeleteReplaced(models.Model): 14 testfile = models.FileField(storage=temp_storage, upload_to=UPLOAD_TO_NAME, 15 delete_replaced=True) -
tests/regressiontests/file_uploads/tests.py
12 12 from django.utils.hashcompat import sha_constructor 13 13 from django.http.multipartparser import MultiPartParser 14 14 15 from models import FileModel, temp_storage, UPLOAD_TO15 from models import FileModel, FileModelDeleteReplaced, temp_storage, UPLOAD_TO 16 16 import uploadhandler 17 17 18 18 UNICODE_FILENAME = u'test-0123456789_中文_Orléans.jpg' … … 302 302 'CONTENT_TYPE': 'multipart/form-data; boundary=_foo', 303 303 'CONTENT_LENGTH': '1' 304 304 }, StringIO('x'), [], 'utf-8') 305 306 class 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']) -
docs/ref/models/fields.txt
465 465 ``FileField`` 466 466 ------------- 467 467 468 .. class:: FileField(upload_to=None, [max_length=100, **options])468 .. class:: FileField(upload_to=None, [max_length=100, storage=None, delete_replaced=False, **options]) 469 469 470 470 A file-upload field. 471 471 … … 511 511 when determining the final destination path. 512 512 ====================== =============================================== 513 513 514 Also has one optional argument:514 Also has two optional arguments: 515 515 516 516 .. attribute:: FileField.storage 517 517 … … 520 520 Optional. A storage object, which handles the storage and retrieval of your 521 521 files. See :ref:`topics-files` for details on how to provide this object. 522 522 523 .. attribute:: FileField.delete_replaced 524 525 .. versionadded:: 1.3 526 527 Delete replaced files (if they are `orphaned`__) when the model instance is 528 saved. 529 530 .. __: `deletion of orphaned files`_ 531 523 532 The admin represents this field as an ``<input type="file">`` (a file-upload 524 533 widget). 525 534 … … 611 620 The optional ``save`` argument controls whether or not the instance is saved 612 621 after the file has been deleted. Defaults to ``True``. 613 622 623 Deletion of Orphaned Files 624 ~~~~~~~~~~~~~~~~~~~~~~~~~~ 625 626 When a model is deleted, the underlying file is deleted if it is considered an 627 orphan. 628 629 A file's orphan status is decided by checking the field all other instances for 630 this model for a reference to the same file. 631 632 To remove this default behavior, remove the signal:: 633 634 from django.db.signals import post_delete 635 post_delete.disconnect(SomeModel._meta.get_field('the_file_field').delete_file) 636 637 .. versionadded:: 1.3 638 Deleting replaced files is now possible. 639 640 When a file field is changed on a model instance, the replaced file it is *not* 641 deleted by default. 642 643 To automatically delete replaced files that are orphaned when the model instance 644 is saved, set ``delete_replaced=True`` on the ``FileField``. 645 614 646 ``FilePathField`` 615 647 ----------------- 616 648