|
@@ -1,8 +1,11 @@
|
|
-import os
|
|
|
|
-import errno
|
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
|
+import errno
|
|
|
|
+from inspect import getargspec
|
|
|
|
+import os
|
|
|
|
+import warnings
|
|
|
|
|
|
from django.conf import settings
|
|
from django.conf import settings
|
|
|
|
+from django.core.exceptions import SuspiciousFileOperation
|
|
from django.core.files import locks, File
|
|
from django.core.files import locks, File
|
|
from django.core.files.move import file_move_safe
|
|
from django.core.files.move import file_move_safe
|
|
from django.utils.crypto import get_random_string
|
|
from django.utils.crypto import get_random_string
|
|
@@ -13,6 +16,7 @@ from django.utils.six.moves.urllib.parse import urljoin
|
|
from django.utils.text import get_valid_filename
|
|
from django.utils.text import get_valid_filename
|
|
from django.utils._os import safe_join, abspathu
|
|
from django.utils._os import safe_join, abspathu
|
|
from django.utils.deconstruct import deconstructible
|
|
from django.utils.deconstruct import deconstructible
|
|
|
|
+from django.utils.deprecation import RemovedInDjango20Warning
|
|
|
|
|
|
|
|
|
|
__all__ = ('Storage', 'FileSystemStorage', 'DefaultStorage', 'default_storage')
|
|
__all__ = ('Storage', 'FileSystemStorage', 'DefaultStorage', 'default_storage')
|
|
@@ -33,7 +37,7 @@ class Storage(object):
|
|
"""
|
|
"""
|
|
return self._open(name, mode)
|
|
return self._open(name, mode)
|
|
|
|
|
|
- def save(self, name, content):
|
|
|
|
|
|
+ def save(self, name, content, max_length=None):
|
|
"""
|
|
"""
|
|
Saves new content to the file specified by name. The content should be
|
|
Saves new content to the file specified by name. The content should be
|
|
a proper File object or any python file-like object, ready to be read
|
|
a proper File object or any python file-like object, ready to be read
|
|
@@ -46,7 +50,18 @@ class Storage(object):
|
|
if not hasattr(content, 'chunks'):
|
|
if not hasattr(content, 'chunks'):
|
|
content = File(content)
|
|
content = File(content)
|
|
|
|
|
|
- name = self.get_available_name(name)
|
|
|
|
|
|
+ args, varargs, varkw, defaults = getargspec(self.get_available_name)
|
|
|
|
+ if 'max_length' in args:
|
|
|
|
+ name = self.get_available_name(name, max_length=max_length)
|
|
|
|
+ else:
|
|
|
|
+ warnings.warn(
|
|
|
|
+ 'Backwards compatibility for storage backends without '
|
|
|
|
+ 'support for the `max_length` argument in '
|
|
|
|
+ 'Storage.get_available_name() will be removed in Django 2.0.',
|
|
|
|
+ RemovedInDjango20Warning, stacklevel=2
|
|
|
|
+ )
|
|
|
|
+ name = self.get_available_name(name)
|
|
|
|
+
|
|
name = self._save(name, content)
|
|
name = self._save(name, content)
|
|
|
|
|
|
# Store filenames with forward slashes, even on Windows
|
|
# Store filenames with forward slashes, even on Windows
|
|
@@ -61,7 +76,7 @@ class Storage(object):
|
|
"""
|
|
"""
|
|
return get_valid_filename(name)
|
|
return get_valid_filename(name)
|
|
|
|
|
|
- def get_available_name(self, name):
|
|
|
|
|
|
+ def get_available_name(self, name, max_length=None):
|
|
"""
|
|
"""
|
|
Returns a filename that's free on the target storage system, and
|
|
Returns a filename that's free on the target storage system, and
|
|
available for new content to be written to.
|
|
available for new content to be written to.
|
|
@@ -71,10 +86,25 @@ class Storage(object):
|
|
# If the filename already exists, add an underscore and a random 7
|
|
# If the filename already exists, add an underscore and a random 7
|
|
# character alphanumeric string (before the file extension, if one
|
|
# character alphanumeric string (before the file extension, if one
|
|
# exists) to the filename until the generated filename doesn't exist.
|
|
# exists) to the filename until the generated filename doesn't exist.
|
|
- while self.exists(name):
|
|
|
|
|
|
+ # Truncate original name if required, so the new filename does not
|
|
|
|
+ # exceed the max_length.
|
|
|
|
+ while self.exists(name) or (max_length and len(name) > max_length):
|
|
# file_ext includes the dot.
|
|
# file_ext includes the dot.
|
|
name = os.path.join(dir_name, "%s_%s%s" % (file_root, get_random_string(7), file_ext))
|
|
name = os.path.join(dir_name, "%s_%s%s" % (file_root, get_random_string(7), file_ext))
|
|
-
|
|
|
|
|
|
+ if max_length is None:
|
|
|
|
+ continue
|
|
|
|
+ # Truncate file_root if max_length exceeded.
|
|
|
|
+ truncation = len(name) - max_length
|
|
|
|
+ if truncation > 0:
|
|
|
|
+ file_root = file_root[:-truncation]
|
|
|
|
+ # Entire file_root was truncated in attempt to find an available filename.
|
|
|
|
+ if not file_root:
|
|
|
|
+ raise SuspiciousFileOperation(
|
|
|
|
+ 'Storage can not find an available filename for "%s". '
|
|
|
|
+ 'Please make sure that the corresponding file field '
|
|
|
|
+ 'allows sufficient "max_length".' % name
|
|
|
|
+ )
|
|
|
|
+ name = os.path.join(dir_name, "%s_%s%s" % (file_root, get_random_string(7), file_ext))
|
|
return name
|
|
return name
|
|
|
|
|
|
def path(self, name):
|
|
def path(self, name):
|