Ticket #2983: 2983.2.diff
File 2983.2.diff, 19.6 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 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
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. … … 86 88 # to further manipulate the underlying file, as well as update the 87 89 # associated model instance. 88 90 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 90 101 name = self.field.generate_filename(self.instance, name) 91 102 self.name = self.storage.save(name, content) 92 setattr(self.instance, self.field.name, self .name)103 setattr(self.instance, self.field.name, self) 93 104 94 105 # Update the filesize cache 95 106 self._size = len(content) … … 100 111 self.instance.save() 101 112 save.alters_data = True 102 113 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 104 125 # Only close the file if it's already open, which we know by the 105 126 # presence of self._file 106 127 if hasattr(self, '_file'): … … 110 131 self.storage.delete(self.name) 111 132 112 133 self.name = None 113 setattr(self.instance, self.field.name, self .name)134 setattr(self.instance, self.field.name, self) 114 135 115 136 # Delete the filesize cache 116 137 if hasattr(self, '_size'): … … 121 142 self.instance.save() 122 143 delete.alters_data = True 123 144 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 124 188 def _get_closed(self): 125 189 file = getattr(self, '_file', None) 126 190 return file is None or file.closed … … 136 200 # it's attached to in order to work properly, but the only necessary 137 201 # data to be pickled is the file's name itself. Everything else will 138 202 # 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} 140 206 141 207 class FileDescriptor(object): 142 208 """ … … 194 260 file_copy._committed = False 195 261 instance.__dict__[self.field.name] = file_copy 196 262 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. 200 267 elif isinstance(file, FieldFile) and not hasattr(file, 'field'): 201 268 file.instance = instance 202 269 file.field = self.field 203 270 file.storage = self.field.storage 204 271 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 205 280 # That was fun, wasn't it? 206 281 return instance.__dict__[self.field.name] 207 282 208 283 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 209 288 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) 210 298 211 299 class FileField(Field): 212 300 # The class to wrap instance attributes in. Accessing the file object off … … 216 304 # The descriptor to use for accessing the attribute off of the class. 217 305 descriptor_class = FileDescriptor 218 306 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): 220 309 for arg in ('primary_key', 'unique'): 221 310 if arg in kwargs: 222 311 raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__)) … … 225 314 self.upload_to = upload_to 226 315 if callable(upload_to): 227 316 self.generate_filename = upload_to 317 self.delete_replaced = delete_replaced 228 318 229 319 kwargs['max_length'] = kwargs.get('max_length', 100) 230 320 super(FileField, self).__init__(verbose_name, name, **kwargs) … … 258 348 signals.post_delete.connect(self.delete_file, sender=cls) 259 349 260 350 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 """ 261 355 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()) 271 359 272 360 def get_directory_name(self): 273 361 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)