Fixed #15190 -- Refactored the collectstatic command to improve the symlink mode and generally straighten out its behavior.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@15388 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2011-02-01 19:19:52 +00:00
parent 67a2bb6341
commit 5cd5612808
1 changed files with 106 additions and 85 deletions

View File

@ -34,16 +34,16 @@ class Command(NoArgsCommand):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NoArgsCommand, self).__init__(*args, **kwargs) super(NoArgsCommand, self).__init__(*args, **kwargs)
self.copied_files = set() self.copied_files = []
self.symlinked_files = set() self.symlinked_files = []
self.unmodified_files = set() self.unmodified_files = []
self.destination_storage = get_storage_class(settings.STATICFILES_STORAGE)() self.storage = get_storage_class(settings.STATICFILES_STORAGE)()
try: try:
self.destination_storage.path('') self.storage.path('')
except NotImplementedError: except NotImplementedError:
self.destination_local = False self.local = False
else: else:
self.destination_local = True self.local = True
# Use ints for file times (ticket #14665) # Use ints for file times (ticket #14665)
os.stat_float_times(False) os.stat_float_times(False)
@ -59,25 +59,33 @@ class Command(NoArgsCommand):
if sys.platform == 'win32': if sys.platform == 'win32':
raise CommandError("Symlinking is not supported by this " raise CommandError("Symlinking is not supported by this "
"platform (%s)." % sys.platform) "platform (%s)." % sys.platform)
if not self.destination_local: if not self.local:
raise CommandError("Can't symlink to a remote destination.") raise CommandError("Can't symlink to a remote destination.")
# Warn before doing anything more. # Warn before doing anything more.
if options.get('interactive'): if options.get('interactive'):
confirm = raw_input(""" confirm = raw_input("""
You have requested to collate static files and collect them at the destination You have requested to collect static files at the destination
location as specified in your settings file. location as specified in your settings file ('%s').
This will overwrite existing files. This will overwrite existing files.
Are you sure you want to do this? 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': if confirm != 'yes':
raise CommandError("Collecting static files cancelled.") raise CommandError("Collecting static files cancelled.")
for finder in finders.get_finders(): for finder in finders.get_finders():
for source, storage in finder.list(ignore_patterns): for path, storage in finder.list(ignore_patterns):
self.copy_file(source, storage, **options) # 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) actual_count = len(self.copied_files) + len(self.symlinked_files)
unmodified_count = len(self.unmodified_files) unmodified_count = len(self.unmodified_files)
@ -98,84 +106,97 @@ Type 'yes' to continue, or 'no' to cancel: """)
if self.verbosity >= level: if self.verbosity >= level:
self.stdout.write(msg) self.stdout.write(msg)
def copy_file(self, source, source_storage, **options): def delete_file(self, path, prefixed_path, source_storage, **options):
""" # Whether we are in symlink mode
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
symlink = options['link'] symlink = options['link']
dry_run = options['dry_run'] # Checks if the target file should be deleted if it already exists
if self.storage.exists(prefixed_path):
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):
try: try:
destination_last_modified = \ # When was the target file modified last time?
self.destination_storage.modified_time(destination) target_last_modified = self.storage.modified_time(prefixed_path)
except (OSError, NotImplementedError): except (OSError, NotImplementedError):
# storage doesn't support ``modified_time`` or failed. # The storage doesn't support ``modified_time`` or failed
pass pass
else: else:
destination_is_link = (self.destination_local and try:
os.path.islink(self.destination_storage.path(destination))) # When was the source file modified last time?
if destination_last_modified >= source_last_modified: source_last_modified = source_storage.modified_time(path)
if (not symlink and not destination_is_link): except (OSError, NotImplementedError):
self.log("Skipping '%s' (not modified)" % destination) pass
self.unmodified_files.add(destination) 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 return False
if dry_run: # Then delete the existing file if really needed
self.log("Pretending to delete '%s'" % destination) if options['dry_run']:
self.log("Pretending to delete '%s'" % path)
else: else:
self.log("Deleting '%s'" % destination) self.log("Deleting '%s'" % path)
self.destination_storage.delete(destination) self.storage.delete(prefixed_path)
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))
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)
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 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
shutil.copy2(source_path, full_path)
else:
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)