/django/contrib/staticfiles/management/commands/collectstatic.py
Python | 204 lines | 196 code | 4 blank | 4 comment | 0 complexity | 7db365e80540498598786ee75230a7ba MD5 | raw file
Possible License(s): BSD-3-Clause
1import os 2import sys 3import shutil 4from optparse import make_option 5 6from django.conf import settings 7from django.core.files.storage import get_storage_class 8from django.core.management.base import CommandError, NoArgsCommand 9from django.utils.encoding import smart_str 10 11from django.contrib.staticfiles import finders 12 13class Command(NoArgsCommand): 14 """ 15 Command that allows to copy or symlink media files from different 16 locations to the settings.STATIC_ROOT. 17 """ 18 option_list = NoArgsCommand.option_list + ( 19 make_option('--noinput', action='store_false', dest='interactive', 20 default=True, help="Do NOT prompt the user for input of any kind."), 21 make_option('-i', '--ignore', action='append', default=[], 22 dest='ignore_patterns', metavar='PATTERN', 23 help="Ignore files or directories matching this glob-style " 24 "pattern. Use multiple times to ignore more."), 25 make_option('-n', '--dry-run', action='store_true', dest='dry_run', 26 default=False, help="Do everything except modify the filesystem."), 27 make_option('-l', '--link', action='store_true', dest='link', 28 default=False, help="Create a symbolic link to each file instead of copying."), 29 make_option('--no-default-ignore', action='store_false', 30 dest='use_default_ignore_patterns', default=True, 31 help="Don't ignore the common private glob-style patterns 'CVS', " 32 "'.*' and '*~'."), 33 ) 34 help = "Collect static files from apps and other locations in a single location." 35 36 def __init__(self, *args, **kwargs): 37 super(NoArgsCommand, self).__init__(*args, **kwargs) 38 self.copied_files = [] 39 self.symlinked_files = [] 40 self.unmodified_files = [] 41 self.storage = get_storage_class(settings.STATICFILES_STORAGE)() 42 try: 43 self.storage.path('') 44 except NotImplementedError: 45 self.local = False 46 else: 47 self.local = True 48 # Use ints for file times (ticket #14665) 49 os.stat_float_times(False) 50 51 def handle_noargs(self, **options): 52 symlink = options['link'] 53 ignore_patterns = options['ignore_patterns'] 54 if options['use_default_ignore_patterns']: 55 ignore_patterns += ['CVS', '.*', '*~'] 56 ignore_patterns = list(set(ignore_patterns)) 57 self.verbosity = int(options.get('verbosity', 1)) 58 59 if symlink: 60 if sys.platform == 'win32': 61 raise CommandError("Symlinking is not supported by this " 62 "platform (%s)." % sys.platform) 63 if not self.local: 64 raise CommandError("Can't symlink to a remote destination.") 65 66 # Warn before doing anything more. 67 if options.get('interactive'): 68 confirm = raw_input(u""" 69You have requested to collect static files at the destination 70location as specified in your settings file. 71 72This will overwrite existing files. 73Are you sure you want to do this? 74 75Type 'yes' to continue, or 'no' to cancel: """) 76 if confirm != 'yes': 77 raise CommandError("Collecting static files cancelled.") 78 79 for finder in finders.get_finders(): 80 for path, storage in finder.list(ignore_patterns): 81 # Prefix the relative path if the source storage contains it 82 if getattr(storage, 'prefix', None): 83 prefixed_path = os.path.join(storage.prefix, path) 84 else: 85 prefixed_path = path 86 if symlink: 87 self.link_file(path, prefixed_path, storage, **options) 88 else: 89 self.copy_file(path, prefixed_path, storage, **options) 90 91 actual_count = len(self.copied_files) + len(self.symlinked_files) 92 unmodified_count = len(self.unmodified_files) 93 if self.verbosity >= 1: 94 self.stdout.write(smart_str(u"\n%s static file%s %s to '%s'%s.\n" 95 % (actual_count, actual_count != 1 and 's' or '', 96 symlink and 'symlinked' or 'copied', 97 settings.STATIC_ROOT, 98 unmodified_count and ' (%s unmodified)' 99 % unmodified_count or ''))) 100 101 def log(self, msg, level=2): 102 """ 103 Small log helper 104 """ 105 msg = smart_str(msg) 106 if not msg.endswith("\n"): 107 msg += "\n" 108 if self.verbosity >= level: 109 self.stdout.write(msg) 110 111 def delete_file(self, path, prefixed_path, source_storage, **options): 112 # Whether we are in symlink mode 113 symlink = options['link'] 114 # Checks if the target file should be deleted if it already exists 115 if self.storage.exists(prefixed_path): 116 try: 117 # When was the target file modified last time? 118 target_last_modified = self.storage.modified_time(prefixed_path) 119 except (OSError, NotImplementedError): 120 # The storage doesn't support ``modified_time`` or failed 121 pass 122 else: 123 try: 124 # When was the source file modified last time? 125 source_last_modified = source_storage.modified_time(path) 126 except (OSError, NotImplementedError): 127 pass 128 else: 129 # The full path of the target file 130 if self.local: 131 full_path = self.storage.path(prefixed_path) 132 else: 133 full_path = None 134 # Skip the file if the source file is younger 135 if target_last_modified >= source_last_modified: 136 if not ((symlink and full_path and not os.path.islink(full_path)) or 137 (not symlink and full_path and os.path.islink(full_path))): 138 if prefixed_path not in self.unmodified_files: 139 self.unmodified_files.append(prefixed_path) 140 self.log(u"Skipping '%s' (not modified)" % path) 141 return False 142 # Then delete the existing file if really needed 143 if options['dry_run']: 144 self.log(u"Pretending to delete '%s'" % path) 145 else: 146 self.log(u"Deleting '%s'" % path) 147 self.storage.delete(prefixed_path) 148 return True 149 150 def link_file(self, path, prefixed_path, source_storage, **options): 151 """ 152 Attempt to link ``path`` 153 """ 154 # Skip this file if it was already copied earlier 155 if prefixed_path in self.symlinked_files: 156 return self.log(u"Skipping '%s' (already linked earlier)" % path) 157 # Delete the target file if needed or break 158 if not self.delete_file(path, prefixed_path, source_storage, **options): 159 return 160 # The full path of the source file 161 source_path = source_storage.path(path) 162 # Finally link the file 163 if options['dry_run']: 164 self.log(u"Pretending to link '%s'" % source_path, level=1) 165 else: 166 self.log(u"Linking '%s'" % source_path, level=1) 167 full_path = self.storage.path(prefixed_path) 168 try: 169 os.makedirs(os.path.dirname(full_path)) 170 except OSError: 171 pass 172 os.symlink(source_path, full_path) 173 if prefixed_path not in self.symlinked_files: 174 self.symlinked_files.append(prefixed_path) 175 176 def copy_file(self, path, prefixed_path, source_storage, **options): 177 """ 178 Attempt to copy ``path`` with storage 179 """ 180 # Skip this file if it was already copied earlier 181 if prefixed_path in self.copied_files: 182 return self.log(u"Skipping '%s' (already copied earlier)" % path) 183 # Delete the target file if needed or break 184 if not self.delete_file(path, prefixed_path, source_storage, **options): 185 return 186 # The full path of the source file 187 source_path = source_storage.path(path) 188 # Finally start copying 189 if options['dry_run']: 190 self.log(u"Pretending to copy '%s'" % source_path, level=1) 191 else: 192 self.log(u"Copying '%s'" % source_path, level=1) 193 if self.local: 194 full_path = self.storage.path(prefixed_path) 195 try: 196 os.makedirs(os.path.dirname(full_path)) 197 except OSError: 198 pass 199 shutil.copy2(source_path, full_path) 200 else: 201 source_file = source_storage.open(path) 202 self.storage.save(prefixed_path, source_file) 203 if not prefixed_path in self.copied_files: 204 self.copied_files.append(prefixed_path)