diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 7963d60de5..14e3b657b2 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -8,7 +8,7 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.core.files import locks, File from django.core.files.move import file_move_safe -from django.utils.encoding import force_unicode +from django.utils.encoding import force_unicode, filepath_to_uri from django.utils.functional import LazyObject from django.utils.importlib import import_module from django.utils.text import get_valid_filename @@ -240,7 +240,7 @@ class FileSystemStorage(Storage): def url(self, name): if self.base_url is None: raise ValueError("This file is not accessible via a URL.") - return urlparse.urljoin(self.base_url, name).replace('\\', '/') + return urlparse.urljoin(self.base_url, filepath_to_uri(name)) def accessed_time(self, name): return datetime.fromtimestamp(os.path.getatime(self.path(name))) diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 9301419b5c..96d915a433 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -156,6 +156,24 @@ def iri_to_uri(iri): return iri return urllib.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~") +def filepath_to_uri(path): + """Convert an file system path to a URI portion that is suitable for + inclusion in a URL. + + We are assuming input is either UTF-8 or unicode already. + + This method will encode certain chars that would normally be recognized as + special chars for URIs. Note that this method does not encode the ' + character, as it is a valid character within URIs. See + encodeURIComponent() JavaScript function for more details. + + Returns an ASCII string containing the encoded result. + """ + if path is None: + return path + # I know about `os.sep` and `os.altsep` but I want to leave + # some flexibility for hardcoding separators. + return urllib.quote(smart_str(path).replace("\\", "/"), safe="/~!*()'") # The encoding of the default system locale but falls back to the # given fallback encoding if the encoding is unsupported by python or could diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index 44466fe975..a4f118f2df 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -204,6 +204,15 @@ class FileStorageTests(unittest.TestCase): self.assertEqual(self.storage.url('test.file'), '%s%s' % (self.storage.base_url, 'test.file')) + # should encode special chars except ~!*()' + # like encodeURIComponent() JavaScript function do + self.assertEqual(self.storage.url(r"""~!*()'@#$%^&*abc`+=.file"""), + """/test_media_url/~!*()'%40%23%24%25%5E%26*abc%60%2B%3D.file""") + + # should stanslate os path separator(s) to the url path separator + self.assertEqual(self.storage.url("""a/b\\c.file"""), + """/test_media_url/a/b/c.file""") + self.storage.base_url = None self.assertRaises(ValueError, self.storage.url, 'test.file')