Fixed #10258: handle duplicate file names better.

Instead of just continually appending "_" to duplicate file names, Django's
default storage now appends `_1`, `_2`, `_3`, etc.

Thanks to ianschenck and Thilo.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12552 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2010-02-23 22:39:22 +00:00
parent 43b47a87d3
commit 5366aa96fe
4 changed files with 29 additions and 16 deletions

View File

@ -1,6 +1,7 @@
import os import os
import errno import errno
import urlparse import urlparse
import itertools
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
@ -65,13 +66,14 @@ class Storage(object):
""" """
dir_name, file_name = os.path.split(name) dir_name, file_name = os.path.split(name)
file_root, file_ext = os.path.splitext(file_name) file_root, file_ext = os.path.splitext(file_name)
# If the filename already exists, keep adding an underscore (before the # If the filename already exists, add an underscore and a number (before
# file extension, if one exists) to the filename until the generated # the file extension, if one exists) to the filename until the generated
# filename doesn't exist. # filename doesn't exist.
count = itertools.count(1)
while self.exists(name): while self.exists(name):
file_root += '_'
# file_ext includes the dot. # file_ext includes the dot.
name = os.path.join(dir_name, file_root + file_ext) name = os.path.join(dir_name, "%s_%s%s" % (file_root, count.next(), file_ext))
return name return name
def path(self, name): def path(self, name):

View File

@ -88,5 +88,5 @@ the provided filename into account. The ``name`` argument passed to this method
will have already cleaned to a filename valid for the storage system, according will have already cleaned to a filename valid for the storage system, according
to the ``get_valid_name()`` method described above. to the ``get_valid_name()`` method described above.
The code provided on ``Storage`` simply appends underscores to the filename The code provided on ``Storage`` simply appends ``"_1"``, ``"_2"``, etc. to the
until it finds one that's available in the destination directory. filename until it finds one that's available in the destination directory.

View File

@ -5,14 +5,11 @@
and where files should be stored. and where files should be stored.
""" """
import shutil
import random import random
import tempfile import tempfile
from django.db import models from django.db import models
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.core.cache import cache
temp_storage_location = tempfile.mkdtemp() temp_storage_location = tempfile.mkdtemp()
temp_storage = FileSystemStorage(location=temp_storage_location) temp_storage = FileSystemStorage(location=temp_storage_location)
@ -64,6 +61,7 @@ ValueError: The 'normal' attribute has no file associated with it.
# File objects can be assigned to FileField attributes, but shouldn't get # File objects can be assigned to FileField attributes, but shouldn't get
# committed until the model it's attached to is saved. # committed until the model it's attached to is saved.
>>> from django.core.files.uploadedfile import SimpleUploadedFile
>>> obj1.normal = SimpleUploadedFile('assignment.txt', 'content') >>> obj1.normal = SimpleUploadedFile('assignment.txt', 'content')
>>> dirs, files = temp_storage.listdir('tests') >>> dirs, files = temp_storage.listdir('tests')
>>> dirs >>> dirs
@ -93,16 +91,17 @@ ValueError: The 'normal' attribute has no file associated with it.
>>> obj2 = Storage() >>> obj2 = Storage()
>>> obj2.normal.save('django_test.txt', ContentFile('more content')) >>> obj2.normal.save('django_test.txt', ContentFile('more content'))
>>> obj2.normal >>> obj2.normal
<FieldFile: tests/django_test_.txt> <FieldFile: tests/django_test_1.txt>
>>> obj2.normal.size >>> obj2.normal.size
12 12
# Push the objects into the cache to make sure they pickle properly # Push the objects into the cache to make sure they pickle properly
>>> from django.core.cache import cache
>>> cache.set('obj1', obj1) >>> cache.set('obj1', obj1)
>>> cache.set('obj2', obj2) >>> cache.set('obj2', obj2)
>>> cache.get('obj2').normal >>> cache.get('obj2').normal
<FieldFile: tests/django_test_.txt> <FieldFile: tests/django_test_1.txt>
# Deleting an object deletes the file it uses, if there are no other objects # Deleting an object deletes the file it uses, if there are no other objects
# still using that file. # still using that file.
@ -110,7 +109,17 @@ ValueError: The 'normal' attribute has no file associated with it.
>>> obj2.delete() >>> obj2.delete()
>>> obj2.normal.save('django_test.txt', ContentFile('more content')) >>> obj2.normal.save('django_test.txt', ContentFile('more content'))
>>> obj2.normal >>> obj2.normal
<FieldFile: tests/django_test_.txt> <FieldFile: tests/django_test_1.txt>
# Multiple files with the same name get _N appended to them.
>>> objs = [Storage() for i in range(3)]
>>> for o in objs:
... o.normal.save('multiple_files.txt', ContentFile('Same Content'))
>>> [o.normal for o in objs]
[<FieldFile: tests/multiple_files.txt>, <FieldFile: tests/multiple_files_1.txt>, <FieldFile: tests/multiple_files_2.txt>]
>>> for o in objs:
... o.delete()
# Default values allow an object to access a single file. # Default values allow an object to access a single file.
@ -139,5 +148,7 @@ ValueError: The 'normal' attribute has no file associated with it.
>>> obj2.normal.delete() >>> obj2.normal.delete()
>>> obj3.default.delete() >>> obj3.default.delete()
>>> obj4.random.delete() >>> obj4.random.delete()
>>> import shutil
>>> shutil.rmtree(temp_storage_location) >>> shutil.rmtree(temp_storage_location)
"""} """}

View File

@ -126,9 +126,9 @@ class FileSaveRaceConditionTest(TestCase):
name = self.save_file('conflict') name = self.save_file('conflict')
self.thread.join() self.thread.join()
self.assert_(self.storage.exists('conflict')) self.assert_(self.storage.exists('conflict'))
self.assert_(self.storage.exists('conflict_')) self.assert_(self.storage.exists('conflict_1'))
self.storage.delete('conflict') self.storage.delete('conflict')
self.storage.delete('conflict_') self.storage.delete('conflict_1')
class FileStoragePermissions(TestCase): class FileStoragePermissions(TestCase):
def setUp(self): def setUp(self):
@ -167,7 +167,7 @@ class FileStoragePathParsing(TestCase):
self.failIf(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path'))) self.failIf(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path')))
self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test'))) self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test')))
self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_'))) self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_1')))
def test_first_character_dot(self): def test_first_character_dot(self):
""" """
@ -183,7 +183,7 @@ class FileStoragePathParsing(TestCase):
if sys.version_info < (2, 6): if sys.version_info < (2, 6):
self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/_.test'))) self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/_.test')))
else: else:
self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_'))) self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_1')))
if Image is not None: if Image is not None:
class DimensionClosingBug(TestCase): class DimensionClosingBug(TestCase):