diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index 38f1f5822e..801113e3c4 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -274,12 +274,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) diff --git a/docs/releases/1.10.5.txt b/docs/releases/1.10.5.txt index 5c9d40914b..3e75235c42 100644 --- a/docs/releases/1.10.5.txt +++ b/docs/releases/1.10.5.txt @@ -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`). diff --git a/tests/staticfiles_tests/cases.py b/tests/staticfiles_tests/cases.py index ea16101eb6..009672ee12 100644 --- a/tests/staticfiles_tests/cases.py +++ b/tests/staticfiles_tests/cases.py @@ -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): diff --git a/tests/staticfiles_tests/storage.py b/tests/staticfiles_tests/storage.py index 12eb032d8e..79e6245f59 100644 --- a/tests/staticfiles_tests/storage.py +++ b/tests/staticfiles_tests/storage.py @@ -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 QueryStringStorage(storage.Storage): def url(self, path): return path + '?a=b&c=d' diff --git a/tests/staticfiles_tests/test_management.py b/tests/staticfiles_tests/test_management.py index eb0b83bca9..bbb8567c2a 100644 --- a/tests/staticfiles_tests/test_management.py +++ b/tests/staticfiles_tests/test_management.py @@ -399,6 +399,23 @@ class TestCollectionNonLocalStorage(TestNoFilesCreated, CollectionTestCase): storage.path('name') +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): """