1 | import os
|
---|
2 | import sys
|
---|
3 | from optparse import make_option
|
---|
4 |
|
---|
5 | from django.conf import settings
|
---|
6 | from django.core.files.storage import FileSystemStorage, get_storage_class
|
---|
7 | from django.core.management.base import CommandError, NoArgsCommand
|
---|
8 | from django.utils.encoding import smart_str, smart_unicode
|
---|
9 |
|
---|
10 | from django.contrib.staticfiles import finders
|
---|
11 |
|
---|
12 | class 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"""
|
---|
87 | You have requested to collect static files at the destination
|
---|
88 | location as specified in your settings%s
|
---|
89 |
|
---|
90 | %s
|
---|
91 | Are you sure you want to do this?
|
---|
92 |
|
---|
93 | Type '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)
|
---|