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):
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)