mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[1.10.x] Fixed #27658 -- Prevented collectstatic from overwriting newer files in remote storages.
Thanks revimi for the initial patch.
Backport of c85831e4b7 from master
			
			
This commit is contained in:
		| @@ -269,12 +269,24 @@ class Command(BaseCommand): | ||||
|                     # The full path of the target file | ||||
|                     if self.local: | ||||
|                         full_path = self.storage.path(prefixed_path) | ||||
|                         # If it's --link mode and the path isn't a link (i.e. | ||||
|                         # the previous collectstatic wasn't with --link) or if | ||||
|                         # it's non-link mode and the path is a link (i.e. the | ||||
|                         # previous collectstatic was with --link), the old | ||||
|                         # links/files must be deleted so it's not safe to skip | ||||
|                         # unmodified files. | ||||
|                         can_skip_unmodified_files = not (self.symlink ^ os.path.islink(full_path)) | ||||
|                     else: | ||||
|                         full_path = None | ||||
|                     # Skip the file if the source file is younger | ||||
|                         # In remote storages, skipping is only based on the | ||||
|                         # modified times since symlinks aren't relevant. | ||||
|                         can_skip_unmodified_files = True | ||||
|                     # Avoid sub-second precision (see #14665, #19540) | ||||
|                     if (target_last_modified.replace(microsecond=0) >= source_last_modified.replace(microsecond=0) and | ||||
|                             full_path and not (self.symlink ^ os.path.islink(full_path))): | ||||
|                     file_is_unmodified = ( | ||||
|                         target_last_modified.replace(microsecond=0) >= | ||||
|                         source_last_modified.replace(microsecond=0) | ||||
|                     ) | ||||
|                     if file_is_unmodified and can_skip_unmodified_files: | ||||
|                         if prefixed_path not in self.unmodified_files: | ||||
|                             self.unmodified_files.append(prefixed_path) | ||||
|                         self.log("Skipping '%s' (not modified)" % path) | ||||
|   | ||||
| @@ -17,3 +17,6 @@ Bugfixes | ||||
|  | ||||
| * Fixed a regression in the ``timesince`` and ``timeuntil`` filters that caused | ||||
|   incorrect results for dates in a leap year (:ticket:`27637`). | ||||
|  | ||||
| * Fixed a regression where ``collectstatic`` overwrote newer files in remote | ||||
|   storages (:ticket:`27658`). | ||||
|   | ||||
| @@ -82,7 +82,8 @@ class CollectionTestCase(BaseStaticFilesMixin, SimpleTestCase): | ||||
|         super(CollectionTestCase, self).tearDown() | ||||
|  | ||||
|     def run_collectstatic(self, **kwargs): | ||||
|         call_command('collectstatic', interactive=False, verbosity=0, | ||||
|         verbosity = kwargs.pop('verbosity', 0) | ||||
|         call_command('collectstatic', interactive=False, verbosity=verbosity, | ||||
|                      ignore_patterns=['*.ignoreme'], **kwargs) | ||||
|  | ||||
|     def _get_file(self, filepath): | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import errno | ||||
| import os | ||||
| from datetime import datetime | ||||
| from datetime import datetime, timedelta | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.staticfiles.storage import CachedStaticFilesStorage | ||||
| @@ -59,6 +59,14 @@ class PathNotImplementedStorage(storage.Storage): | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| class NeverCopyRemoteStorage(PathNotImplementedStorage): | ||||
|     """ | ||||
|     Return a future modified time for all files so that nothing is collected. | ||||
|     """ | ||||
|     def get_modified_time(self, name): | ||||
|         return datetime.now() + timedelta(days=30) | ||||
|  | ||||
|  | ||||
| class SimpleCachedStaticFilesStorage(CachedStaticFilesStorage): | ||||
|  | ||||
|     def file_hash(self, name, content=None): | ||||
|   | ||||
| @@ -337,6 +337,23 @@ class TestCollectionNonLocalStorage(TestNoFilesCreated, CollectionTestCase): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class TestCollectionNeverCopyStorage(CollectionTestCase): | ||||
|  | ||||
|     @override_settings(STATICFILES_STORAGE='staticfiles_tests.storage.NeverCopyRemoteStorage') | ||||
|     def test_skips_newer_files_in_remote_storage(self): | ||||
|         """ | ||||
|         collectstatic skips newer files in a remote storage. | ||||
|         run_collectstatic() in setUp() copies the static files, then files are | ||||
|         always skipped after NeverCopyRemoteStorage is activated since | ||||
|         NeverCopyRemoteStorage.get_modified_time() returns a datetime in the | ||||
|         future to simulate an unmodified file. | ||||
|         """ | ||||
|         stdout = six.StringIO() | ||||
|         self.run_collectstatic(stdout=stdout, verbosity=2) | ||||
|         output = force_text(stdout.getvalue()) | ||||
|         self.assertIn("Skipping 'test.txt' (not modified)", output) | ||||
|  | ||||
|  | ||||
| @unittest.skipUnless(symlinks_supported(), "Must be able to symlink to run this test.") | ||||
| class TestCollectionLinks(TestDefaults, CollectionTestCase): | ||||
|     """ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user