diff --git a/django/utils/_os.py b/django/utils/_os.py
a
|
b
|
|
21 | 21 | path = join(os.getcwdu(), path) |
22 | 22 | return normpath(path) |
23 | 23 | |
| 24 | if os.name == 'nt': |
| 25 | _normcase = lambda s: s.replace("/", "\\") |
| 26 | else: |
| 27 | _normcase = normcase |
| 28 | |
24 | 29 | def safe_join(base, *paths): |
25 | 30 | """ |
26 | 31 | Joins one or more path components to the base path component intelligently. |
… |
… |
|
29 | 34 | The final path must be located inside of the base path component (otherwise |
30 | 35 | a ValueError is raised). |
31 | 36 | """ |
32 | | # We need to use normcase to ensure we don't false-negative on case |
33 | | # insensitive operating systems (like Windows). |
34 | 37 | base = force_unicode(base) |
35 | 38 | paths = [force_unicode(p) for p in paths] |
36 | | final_path = normcase(abspathu(join(base, *paths))) |
37 | | base_path = normcase(abspathu(base)) |
| 39 | final_path = abspathu(join(base, *paths)) |
| 40 | fp = normcase(final_path) |
| 41 | final_path = _normcase(final_path) |
| 42 | |
| 43 | base_path = abspathu(base) |
| 44 | bp = normcase(base_path) |
| 45 | base_path = _normcase(base_path) |
| 46 | |
38 | 47 | base_path_len = len(base_path) |
39 | 48 | # Ensure final_path starts with base_path and that the next character after |
40 | | # the final path is os.sep (or nothing, in which case final_path must be |
41 | | # equal to base_path). |
42 | | if not final_path.startswith(base_path) \ |
43 | | or final_path[base_path_len:base_path_len+1] not in ('', sep): |
| 49 | # the base path portion of final_path is os.sep (or nothing, in which case |
| 50 | # final_path must be equal to base_path). |
| 51 | # We need to use normcase in these checks to ensure we don't false-negative |
| 52 | # on case insensitive operating systems (like Windows). |
| 53 | if not fp.startswith(bp) \ |
| 54 | or fp[base_path_len:base_path_len+1] not in ('', sep): |
44 | 55 | raise ValueError('the joined path is located outside of the base path' |
45 | 56 | ' component') |
46 | 57 | return final_path |
diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py
a
|
b
|
|
32 | 32 | self.temp_dir = tempfile.mktemp() |
33 | 33 | os.makedirs(self.temp_dir) |
34 | 34 | self.storage = self.storage_class(location=self.temp_dir) |
35 | | |
| 35 | |
| 36 | # Set up a second temporary directory with a unique mixed case name |
| 37 | self.temp_dir2 = tempfile.mktemp() |
| 38 | dn, bn = os.path.split(self.temp_dir2) |
| 39 | bn = bn[0].swapcase() + bn[1:-1] + bn[-1].swapcase() |
| 40 | self.temp_dir2 = os.path.join(dn, bn) |
| 41 | os.makedirs(self.temp_dir2) |
| 42 | |
36 | 43 | def tearDown(self): |
37 | 44 | os.rmdir(self.temp_dir) |
38 | | |
| 45 | os.rmdir(self.temp_dir2) |
| 46 | |
39 | 47 | def test_file_access_options(self): |
40 | 48 | """ |
41 | 49 | Standard file access options are available, and work as expected. |
… |
… |
|
61 | 69 | self.assertRaises(SuspiciousOperation, self.storage.exists, '..') |
62 | 70 | self.assertRaises(SuspiciousOperation, self.storage.exists, '/etc/passwd') |
63 | 71 | |
| 72 | def test_file_storage_preservers_filename_case(self): |
| 73 | """The storage backend should preserve case of filenames.""" |
| 74 | # Create a storage backend associated with the mixed case name directory |
| 75 | temp_storage = self.storage_class(location=self.temp_dir2) |
| 76 | # Ask that storage backend to store a file with a mixed case filename |
| 77 | mixed_case = 'CaSe_SeNsItIvE' |
| 78 | file = temp_storage.open(mixed_case, 'w') |
| 79 | file.write('storage contents') |
| 80 | file.close() |
| 81 | self.assertEqual(os.path.join(self.temp_dir2, mixed_case), temp_storage.path(mixed_case)) |
| 82 | temp_storage.delete(mixed_case) |
| 83 | |
64 | 84 | class CustomStorage(FileSystemStorage): |
65 | 85 | def get_available_name(self, name): |
66 | 86 | """ |
diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py
a
|
b
|
|
1 | 1 | #! -*- coding: utf-8 -*- |
2 | 2 | import os |
| 3 | import os.path |
3 | 4 | import errno |
4 | 5 | import shutil |
5 | 6 | import unittest |
… |
… |
|
251 | 252 | # CustomUploadError is the error that should have been raised |
252 | 253 | self.assertEqual(err.__class__, uploadhandler.CustomUploadError) |
253 | 254 | |
| 255 | def test_filename_case_preservation(self): |
| 256 | """ |
| 257 | The storage backend shouldn't mess with the case of the filenames |
| 258 | uploaded. |
| 259 | """ |
| 260 | # Synthetize the contents of a file upload with a mixed |
| 261 | # case filename so we don't have to carry such a file |
| 262 | # in the Django tests source code tree |
| 263 | OUR_BOUNDARY='oUrBoUnDaRyStRiNg' |
| 264 | post_data = [ |
| 265 | '--%s' % OUR_BOUNDARY, |
| 266 | 'Content-Disposition: form-data; name="file_field"; filename="MiXeD_cAsE.txt"', |
| 267 | 'Content-Type: application/octet-stream', |
| 268 | '', |
| 269 | 'file contents\n' |
| 270 | '', |
| 271 | '--%s--\r\n' % OUR_BOUNDARY, |
| 272 | ] |
| 273 | response = self.client.post('/file_uploads/filename_case/', '\r\n'.join(post_data), |
| 274 | 'multipart/form-data; boundary=%s' % OUR_BOUNDARY) |
| 275 | self.assertEqual(response.status_code, 200) |
| 276 | id = int(response.content) |
| 277 | obj = FileModel.objects.get(pk=id) |
| 278 | # The name of the file uploaded and the file stored in the server-side |
| 279 | # shouldn't differ |
| 280 | self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt') |
| 281 | |
254 | 282 | class DirectoryCreationTests(unittest.TestCase): |
255 | 283 | """ |
256 | 284 | Tests for error handling during directory creation |
diff --git a/tests/regressiontests/file_uploads/urls.py b/tests/regressiontests/file_uploads/urls.py
a
|
b
|
|
10 | 10 | (r'^quota/broken/$', views.file_upload_quota_broken), |
11 | 11 | (r'^getlist_count/$', views.file_upload_getlist_count), |
12 | 12 | (r'^upload_errors/$', views.file_upload_errors), |
| 13 | (r'^filename_case/$', views.file_upload_filename_case_view), |
13 | 14 | ) |
diff --git a/tests/regressiontests/file_uploads/views.py b/tests/regressiontests/file_uploads/views.py
a
|
b
|
|
111 | 111 | def file_upload_errors(request): |
112 | 112 | request.upload_handlers.insert(0, ErroringUploadHandler()) |
113 | 113 | return file_upload_echo(request) |
| 114 | |
| 115 | def file_upload_filename_case_view(request): |
| 116 | # Adding the file to the database should succeed |
| 117 | file = request.FILES['file_field'] |
| 118 | obj = FileModel() |
| 119 | obj.testfile.save(file.name, file) |
| 120 | |
| 121 | return HttpResponse('%d' % obj.pk) |
| 122 | |
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
a
|
b
|
|
146 | 146 | if os.path.normcase('/TEST') == os.path.normpath('/test'): |
147 | 147 | template_dirs = ['/dir1', '/DIR2'] |
148 | 148 | test_template_sources('index.html', template_dirs, |
149 | | ['/dir1/index.html', '/dir2/index.html']) |
| 149 | ['/dir1/index.html', '/DIR2/index.html']) |
150 | 150 | test_template_sources('/DIR1/index.HTML', template_dirs, |
151 | | ['/dir1/index.html']) |
| 151 | ['/DIR1/index.HTML']) |
152 | 152 | |
153 | 153 | def test_token_smart_split(self): |
154 | 154 | # Regression test for #7027 |