Ticket #16424: collectstatic.py

File collectstatic.py, 10.1 KB (added by Kidwind, 13 years ago)

Fixed bug!

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