Ticket #11663: 11663.2.diff
File 11663.2.diff, 21.0 KB (added by , 15 years ago) |
---|
-
tests/regressiontests/file_uploads/tests.py
### Eclipse Workspace Patch 1.0 #P Django trunk
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']) -
django/db/models/fields/files.py
22 22 self.field = field 23 23 self.storage = field.storage 24 24 self._committed = True 25 self._replaced = [] 26 self.delete_replaced_files = self.field.delete_replaced 25 27 26 28 def __eq__(self, other): 27 29 # Older code may be expecting FileField values to be simple strings. … … 87 89 # associated model instance. 88 90 89 91 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 90 98 name = self.field.generate_filename(self.instance, name) 91 99 self.name = self.storage.save(name, content) 92 setattr(self.instance, self.field.name, self .name)100 setattr(self.instance, self.field.name, self) 93 101 94 102 # Update the filesize cache 95 103 self._size = len(content) … … 100 108 self.instance.save() 101 109 save.alters_data = True 102 110 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 104 127 # Only close the file if it's already open, which we know by the 105 128 # presence of self._file 106 129 if hasattr(self, '_file'): … … 110 133 self.storage.delete(self.name) 111 134 112 135 self.name = None 113 setattr(self.instance, self.field.name, self .name)136 setattr(self.instance, self.field.name, self) 114 137 115 138 # Delete the filesize cache 116 139 if hasattr(self, '_size'): … … 121 144 self.instance.save() 122 145 delete.alters_data = True 123 146 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 124 196 def _get_closed(self): 125 197 file = getattr(self, '_file', None) 126 198 return file is None or file.closed … … 136 208 # it's attached to in order to work properly, but the only necessary 137 209 # data to be pickled is the file's name itself. Everything else will 138 210 # 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} 140 214 141 215 class FileDescriptor(object): 142 216 """ … … 194 268 file_copy._committed = False 195 269 instance.__dict__[self.field.name] = file_copy 196 270 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. 200 275 elif isinstance(file, FieldFile) and not hasattr(file, 'field'): 201 276 file.instance = instance 202 277 file.field = self.field 203 278 file.storage = self.field.storage 204 279 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 205 288 # That was fun, wasn't it? 206 289 return instance.__dict__[self.field.name] 207 290 208 291 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 209 296 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) 210 306 211 307 class FileField(Field): 212 308 # The class to wrap instance attributes in. Accessing the file object off … … 216 312 # The descriptor to use for accessing the attribute off of the class. 217 313 descriptor_class = FileDescriptor 218 314 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): 220 317 for arg in ('primary_key', 'unique'): 221 318 if arg in kwargs: 222 319 raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__)) … … 225 322 self.upload_to = upload_to 226 323 if callable(upload_to): 227 324 self.generate_filename = upload_to 325 self.delete_replaced = delete_replaced 228 326 229 327 kwargs['max_length'] = kwargs.get('max_length', 100) 230 328 super(FileField, self).__init__(verbose_name, name, **kwargs) … … 258 356 signals.post_delete.connect(self.delete_file, sender=cls) 259 357 260 358 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 """ 261 363 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()) 271 367 272 368 def get_directory_name(self): 273 369 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)