diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index 25f683f770..54eaab0169 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -34,16 +34,16 @@ class Command(NoArgsCommand): def __init__(self, *args, **kwargs): super(NoArgsCommand, self).__init__(*args, **kwargs) - self.copied_files = set() - self.symlinked_files = set() - self.unmodified_files = set() - self.destination_storage = get_storage_class(settings.STATICFILES_STORAGE)() + self.copied_files = [] + self.symlinked_files = [] + self.unmodified_files = [] + self.storage = get_storage_class(settings.STATICFILES_STORAGE)() try: - self.destination_storage.path('') + self.storage.path('') except NotImplementedError: - self.destination_local = False + self.local = False else: - self.destination_local = True + self.local = True # Use ints for file times (ticket #14665) os.stat_float_times(False) @@ -59,25 +59,33 @@ class Command(NoArgsCommand): if sys.platform == 'win32': raise CommandError("Symlinking is not supported by this " "platform (%s)." % sys.platform) - if not self.destination_local: + if not self.local: raise CommandError("Can't symlink to a remote destination.") # Warn before doing anything more. if options.get('interactive'): confirm = raw_input(""" -You have requested to collate static files and collect them at the destination -location as specified in your settings file. +You have requested to collect static files at the destination +location as specified in your settings file ('%s'). This will overwrite existing files. Are you sure you want to do this? -Type 'yes' to continue, or 'no' to cancel: """) +Type 'yes' to continue, or 'no' to cancel: """ % settings.STATIC_ROOT) if confirm != 'yes': raise CommandError("Collecting static files cancelled.") for finder in finders.get_finders(): - for source, storage in finder.list(ignore_patterns): - self.copy_file(source, storage, **options) + for path, storage in finder.list(ignore_patterns): + # Prefix the relative path if the source storage contains it + if getattr(storage, 'prefix', None): + prefixed_path = os.path.join(storage.prefix, path) + else: + prefixed_path = path + if symlink: + self.link_file(path, prefixed_path, storage, **options) + else: + self.copy_file(path, prefixed_path, storage, **options) actual_count = len(self.copied_files) + len(self.symlinked_files) unmodified_count = len(self.unmodified_files) @@ -98,84 +106,97 @@ Type 'yes' to continue, or 'no' to cancel: """) if self.verbosity >= level: self.stdout.write(msg) - def copy_file(self, source, source_storage, **options): - """ - Attempt to copy (or symlink) ``source`` to ``destination``, - returning True if successful. - """ - source_path = source_storage.path(source) - try: - source_last_modified = source_storage.modified_time(source) - except (OSError, NotImplementedError): - source_last_modified = None - if getattr(source_storage, 'prefix', None): - destination = os.path.join(source_storage.prefix, source) - else: - destination = source + def delete_file(self, path, prefixed_path, source_storage, **options): + # Whether we are in symlink mode symlink = options['link'] - dry_run = options['dry_run'] - - if destination in self.copied_files: - self.log("Skipping '%s' (already copied earlier)" % destination) - return False - - if destination in self.symlinked_files: - self.log("Skipping '%s' (already linked earlier)" % destination) - return False - - if self.destination_storage.exists(destination): + # Checks if the target file should be deleted if it already exists + if self.storage.exists(prefixed_path): try: - destination_last_modified = \ - self.destination_storage.modified_time(destination) + # When was the target file modified last time? + target_last_modified = self.storage.modified_time(prefixed_path) except (OSError, NotImplementedError): - # storage doesn't support ``modified_time`` or failed. + # The storage doesn't support ``modified_time`` or failed pass else: - destination_is_link = (self.destination_local and - os.path.islink(self.destination_storage.path(destination))) - if destination_last_modified >= source_last_modified: - if (not symlink and not destination_is_link): - self.log("Skipping '%s' (not modified)" % destination) - self.unmodified_files.add(destination) - return False - if dry_run: - self.log("Pretending to delete '%s'" % destination) - else: - self.log("Deleting '%s'" % destination) - self.destination_storage.delete(destination) - - if symlink: - destination_path = self.destination_storage.path(destination) - if dry_run: - self.log("Pretending to link '%s' to '%s'" % - (source_path, destination_path), level=1) - else: - self.log("Linking '%s' to '%s'" % - (source_path, destination_path), level=1) try: - os.makedirs(os.path.dirname(destination_path)) + # When was the source file modified last time? + source_last_modified = source_storage.modified_time(path) + except (OSError, NotImplementedError): + pass + else: + # The full path of the target file + if self.local: + full_path = self.storage.path(prefixed_path) + else: + full_path = None + # Skip the file if the source file is younger + if target_last_modified >= source_last_modified: + if not ((symlink and full_path and not os.path.islink(full_path)) or + (not symlink and full_path and os.path.islink(full_path))): + if prefixed_path not in self.unmodified_files: + self.unmodified_files.append(prefixed_path) + self.log("Skipping '%s' (not modified)" % path) + return False + # Then delete the existing file if really needed + if options['dry_run']: + self.log("Pretending to delete '%s'" % path) + else: + self.log("Deleting '%s'" % path) + self.storage.delete(prefixed_path) + return True + + def link_file(self, path, prefixed_path, source_storage, **options): + """ + Attempt to link ``path`` + """ + # Skip this file if it was already copied earlier + if prefixed_path in self.symlinked_files: + return self.log("Skipping '%s' (already linked earlier)" % path) + # Delete the target file if needed or break + if not self.delete_file(path, prefixed_path, source_storage, **options): + return + # The full path of the source file + source_path = source_storage.path(path) + # Finally link the file + if options['dry_run']: + self.log("Pretending to link '%s'" % source_path, level=1) + else: + self.log("Linking '%s'" % source_path, level=1) + full_path = self.storage.path(prefixed_path) + try: + os.makedirs(os.path.dirname(full_path)) + except OSError: + pass + os.symlink(source_path, full_path) + if prefixed_path not in self.symlinked_files: + self.symlinked_files.append(prefixed_path) + + def copy_file(self, path, prefixed_path, source_storage, **options): + """ + Attempt to copy ``path`` with storage + """ + # Skip this file if it was already copied earlier + if prefixed_path in self.copied_files: + return self.log("Skipping '%s' (already copied earlier)" % path) + # Delete the target file if needed or break + if not self.delete_file(path, prefixed_path, source_storage, **options): + return + # The full path of the source file + source_path = source_storage.path(path) + # Finally start copying + if options['dry_run']: + self.log("Pretending to copy '%s'" % source_path, level=1) + else: + self.log("Copying '%s'" % source_path, level=1) + if self.local: + full_path = self.storage.path(prefixed_path) + try: + os.makedirs(os.path.dirname(full_path)) except OSError: pass - os.symlink(source_path, destination_path) - self.symlinked_files.add(destination) - else: - if dry_run: - self.log("Pretending to copy '%s' to '%s'" % - (source_path, destination), level=1) + shutil.copy2(source_path, full_path) else: - if self.destination_local: - destination_path = self.destination_storage.path(destination) - try: - os.makedirs(os.path.dirname(destination_path)) - except OSError: - pass - shutil.copy2(source_path, destination_path) - self.log("Copying '%s' to '%s'" % - (source_path, destination_path), level=1) - else: - source_file = source_storage.open(source) - self.destination_storage.save(destination, source_file) - self.log("Copying %s to %s" % - (source_path, destination), level=1) - self.copied_files.add(destination) - return True + source_file = source_storage.open(path) + self.storage.save(prefixed_path, source_file) + if not prefixed_path in self.copied_files: + self.copied_files.append(prefixed_path)