diff options
Diffstat (limited to 'django/contrib')
| -rw-r--r-- | django/contrib/staticfiles/__init__.py | 0 | ||||
| -rw-r--r-- | django/contrib/staticfiles/context_processors.py | 7 | ||||
| -rw-r--r-- | django/contrib/staticfiles/finders.py | 254 | ||||
| -rw-r--r-- | django/contrib/staticfiles/handlers.py | 72 | ||||
| -rw-r--r-- | django/contrib/staticfiles/management/__init__.py | 0 | ||||
| -rw-r--r-- | django/contrib/staticfiles/management/commands/__init__.py | 0 | ||||
| -rw-r--r-- | django/contrib/staticfiles/management/commands/collectstatic.py | 184 | ||||
| -rw-r--r-- | django/contrib/staticfiles/management/commands/findstatic.py | 24 | ||||
| -rw-r--r-- | django/contrib/staticfiles/models.py | 0 | ||||
| -rw-r--r-- | django/contrib/staticfiles/storage.py | 84 | ||||
| -rw-r--r-- | django/contrib/staticfiles/templatetags/__init__.py | 0 | ||||
| -rw-r--r-- | django/contrib/staticfiles/templatetags/staticfiles.py | 43 | ||||
| -rw-r--r-- | django/contrib/staticfiles/urls.py | 29 | ||||
| -rw-r--r-- | django/contrib/staticfiles/utils.py | 30 | ||||
| -rw-r--r-- | django/contrib/staticfiles/views.py | 159 |
15 files changed, 886 insertions, 0 deletions
diff --git a/django/contrib/staticfiles/__init__.py b/django/contrib/staticfiles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/staticfiles/__init__.py diff --git a/django/contrib/staticfiles/context_processors.py b/django/contrib/staticfiles/context_processors.py new file mode 100644 index 0000000000..e1c8d8c8ed --- /dev/null +++ b/django/contrib/staticfiles/context_processors.py @@ -0,0 +1,7 @@ +from django.conf import settings + +def staticfiles(request): + return { + 'STATICFILES_URL': settings.STATICFILES_URL, + 'MEDIA_URL': settings.MEDIA_URL, + } diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py new file mode 100644 index 0000000000..7f0a497174 --- /dev/null +++ b/django/contrib/staticfiles/finders.py @@ -0,0 +1,254 @@ +import os +from django.conf import settings +from django.db import models +from django.core.exceptions import ImproperlyConfigured +from django.core.files.storage import default_storage, Storage, FileSystemStorage +from django.utils.datastructures import SortedDict +from django.utils.functional import memoize, LazyObject +from django.utils.importlib import import_module + +from django.contrib.staticfiles import utils +from django.contrib.staticfiles.storage import AppStaticStorage + +_finders = {} + + +class BaseFinder(object): + """ + A base file finder to be used for custom staticfiles finder classes. + + """ + def find(self, path, all=False): + """ + Given a relative file path this ought to find an + absolute file path. + + If the ``all`` parameter is ``False`` (default) only + the first found file path will be returned; if set + to ``True`` a list of all found files paths is returned. + """ + raise NotImplementedError() + + def list(self, ignore_patterns=[]): + """ + Given an optional list of paths to ignore, this should return + a three item iterable with path, prefix and a storage instance. + """ + raise NotImplementedError() + + +class FileSystemFinder(BaseFinder): + """ + A static files finder that uses the ``STATICFILES_DIRS`` setting + to locate files. + """ + storages = SortedDict() + locations = set() + + def __init__(self, apps=None, *args, **kwargs): + for root in settings.STATICFILES_DIRS: + if isinstance(root, (list, tuple)): + prefix, root = root + else: + prefix = '' + self.locations.add((prefix, root)) + # Don't initialize multiple storages for the same location + for prefix, root in self.locations: + self.storages[root] = FileSystemStorage(location=root) + super(FileSystemFinder, self).__init__(*args, **kwargs) + + def find(self, path, all=False): + """ + Looks for files in the extra media locations + as defined in ``STATICFILES_DIRS``. + """ + matches = [] + for prefix, root in self.locations: + matched_path = self.find_location(root, path, prefix) + if matched_path: + if not all: + return matched_path + matches.append(matched_path) + return matches + + def find_location(self, root, path, prefix=None): + """ + Find a requested static file in a location, returning the found + absolute path (or ``None`` if no match). + """ + if prefix: + prefix = '%s/' % prefix + if not path.startswith(prefix): + return None + path = path[len(prefix):] + path = os.path.join(root, path) + if os.path.exists(path): + return path + + def list(self, ignore_patterns): + """ + List all files in all locations. + """ + for prefix, root in self.locations: + storage = self.storages[root] + for path in utils.get_files(storage, ignore_patterns): + yield path, prefix, storage + + +class AppDirectoriesFinder(BaseFinder): + """ + A static files finder that looks in the ``media`` directory of each app. + """ + storages = {} + storage_class = AppStaticStorage + + def __init__(self, apps=None, *args, **kwargs): + if apps is not None: + self.apps = apps + else: + self.apps = models.get_apps() + for app in self.apps: + self.storages[app] = self.storage_class(app) + super(AppDirectoriesFinder, self).__init__(*args, **kwargs) + + def list(self, ignore_patterns): + """ + List all files in all app storages. + """ + for storage in self.storages.itervalues(): + if storage.exists(''): # check if storage location exists + prefix = storage.get_prefix() + for path in utils.get_files(storage, ignore_patterns): + yield path, prefix, storage + + def find(self, path, all=False): + """ + Looks for files in the app directories. + """ + matches = [] + for app in self.apps: + app_matches = self.find_in_app(app, path) + if app_matches: + if not all: + return app_matches + matches.append(app_matches) + return matches + + def find_in_app(self, app, path): + """ + Find a requested static file in an app's media locations. + """ + storage = self.storages[app] + prefix = storage.get_prefix() + if prefix: + prefix = '%s/' % prefix + if not path.startswith(prefix): + return None + path = path[len(prefix):] + # only try to find a file if the source dir actually exists + if storage.exists(path): + matched_path = storage.path(path) + if matched_path: + return matched_path + + +class BaseStorageFinder(BaseFinder): + """ + A base static files finder to be used to extended + with an own storage class. + """ + storage = None + + def __init__(self, storage=None, *args, **kwargs): + if storage is not None: + self.storage = storage + if self.storage is None: + raise ImproperlyConfigured("The staticfiles storage finder %r " + "doesn't have a storage class " + "assigned." % self.__class__) + # Make sure we have an storage instance here. + if not isinstance(self.storage, (Storage, LazyObject)): + self.storage = self.storage() + super(BaseStorageFinder, self).__init__(*args, **kwargs) + + def find(self, path, all=False): + """ + Looks for files in the default file storage, if it's local. + """ + try: + self.storage.path('') + except NotImplementedError: + pass + else: + if self.storage.exists(path): + match = self.storage.path(path) + if all: + match = [match] + return match + return [] + + def list(self, ignore_patterns): + """ + List all files of the storage. + """ + for path in utils.get_files(self.storage, ignore_patterns): + yield path, '', self.storage + +class DefaultStorageFinder(BaseStorageFinder): + """ + A static files finder that uses the default storage backend. + """ + storage = default_storage + + +def find(path, all=False): + """ + Find a requested static file, first looking in any defined extra media + locations and next in any (non-excluded) installed apps. + + If no matches are found and the static location is local, look for a match + there too. + + If ``all`` is ``False`` (default), return the first matching + absolute path (or ``None`` if no match). Otherwise return a list of + found absolute paths. + + """ + matches = [] + for finder in get_finders(): + result = finder.find(path, all=all) + if not all and result: + return result + if not isinstance(result, (list, tuple)): + result = [result] + matches.extend(result) + if matches: + return matches + # No match. + return all and [] or None + +def get_finders(): + for finder_path in settings.STATICFILES_FINDERS: + yield get_finder(finder_path) + +def _get_finder(import_path): + """ + Imports the message storage class described by import_path, where + import_path is the full Python path to the class. + """ + module, attr = import_path.rsplit('.', 1) + try: + mod = import_module(module) + except ImportError, e: + raise ImproperlyConfigured('Error importing module %s: "%s"' % + (module, e)) + try: + Finder = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured('Module "%s" does not define a "%s" ' + 'class.' % (module, attr)) + if not issubclass(Finder, BaseFinder): + raise ImproperlyConfigured('Finder "%s" is not a subclass of "%s"' % + (Finder, BaseFinder)) + return Finder() +get_finder = memoize(_get_finder, _finders, 1) diff --git a/django/contrib/staticfiles/handlers.py b/django/contrib/staticfiles/handlers.py new file mode 100644 index 0000000000..8681c55c16 --- /dev/null +++ b/django/contrib/staticfiles/handlers.py @@ -0,0 +1,72 @@ +import os +import urllib +from urlparse import urlparse + +from django.core.handlers.wsgi import WSGIHandler, STATUS_CODE_TEXT +from django.http import Http404 + +from django.contrib.staticfiles.views import serve + +class StaticFilesHandler(WSGIHandler): + """ + WSGI middleware that intercepts calls to the static files directory, as + defined by the STATICFILES_URL setting, and serves those files. + """ + def __init__(self, application, media_dir=None): + self.application = application + if media_dir: + self.media_dir = media_dir + else: + self.media_dir = self.get_media_dir() + self.media_url = self.get_media_url() + + def get_media_dir(self): + from django.conf import settings + return settings.STATICFILES_ROOT + + def get_media_url(self): + from django.conf import settings + return settings.STATICFILES_URL + + def file_path(self, url): + """ + Returns the relative path to the media file on disk for the given URL. + + The passed URL is assumed to begin with ``media_url``. If the + resultant file path is outside the media directory, then a ValueError + is raised. + """ + # Remove ``media_url``. + relative_url = url[len(self.media_url):] + return urllib.url2pathname(relative_url) + + def serve(self, request, path): + from django.contrib.staticfiles import finders + absolute_path = finders.find(path) + if not absolute_path: + raise Http404('%r could not be matched to a static file.' % path) + absolute_path, filename = os.path.split(absolute_path) + return serve(request, path=filename, document_root=absolute_path) + + def __call__(self, environ, start_response): + media_url_bits = urlparse(self.media_url) + # Ignore all requests if the host is provided as part of the media_url. + # Also ignore requests that aren't under the media path. + if (media_url_bits[1] or + not environ['PATH_INFO'].startswith(media_url_bits[2])): + return self.application(environ, start_response) + request = self.application.request_class(environ) + try: + response = self.serve(request, self.file_path(environ['PATH_INFO'])) + except Http404: + status = '404 NOT FOUND' + start_response(status, {'Content-type': 'text/plain'}.items()) + return [str('Page not found: %s' % environ['PATH_INFO'])] + status_text = STATUS_CODE_TEXT[response.status_code] + status = '%s %s' % (response.status_code, status_text) + response_headers = [(str(k), str(v)) for k, v in response.items()] + for c in response.cookies.values(): + response_headers.append(('Set-Cookie', str(c.output(header='')))) + start_response(status, response_headers) + return response + diff --git a/django/contrib/staticfiles/management/__init__.py b/django/contrib/staticfiles/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/staticfiles/management/__init__.py diff --git a/django/contrib/staticfiles/management/commands/__init__.py b/django/contrib/staticfiles/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/staticfiles/management/commands/__init__.py diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py new file mode 100644 index 0000000000..aae6bee10e --- /dev/null +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -0,0 +1,184 @@ +import os +import sys +import shutil +from optparse import make_option + +from django.conf import settings +from django.core.files.storage import get_storage_class +from django.core.management.base import CommandError, NoArgsCommand + +from django.contrib.staticfiles import finders + +class Command(NoArgsCommand): + """ + Command that allows to copy or symlink media files from different + locations to the settings.STATICFILES_ROOT. + """ + option_list = NoArgsCommand.option_list + ( + make_option('--noinput', action='store_false', dest='interactive', + default=True, help="Do NOT prompt the user for input of any " + "kind."), + make_option('-i', '--ignore', action='append', default=[], + dest='ignore_patterns', metavar='PATTERN', + help="Ignore files or directories matching this glob-style " + "pattern. Use multiple times to ignore more."), + make_option('-n', '--dry-run', action='store_true', dest='dry_run', + default=False, help="Do everything except modify the filesystem."), + make_option('-l', '--link', action='store_true', dest='link', + default=False, help="Create a symbolic link to each file instead of copying."), + make_option('--no-default-ignore', action='store_false', + dest='use_default_ignore_patterns', default=True, + help="Don't ignore the common private glob-style patterns 'CVS', " + "'.*' and '*~'."), + ) + help = "Collect static files from apps and other locations in a single location." + + def handle_noargs(self, **options): + symlink = options['link'] + ignore_patterns = options['ignore_patterns'] + if options['use_default_ignore_patterns']: + ignore_patterns += ['CVS', '.*', '*~'] + ignore_patterns = list(set(ignore_patterns)) + self.copied_files = [] + self.symlinked_files = [] + self.unmodified_files = [] + self.destination_storage = get_storage_class(settings.STATICFILES_STORAGE)() + + try: + self.destination_storage.path('') + except NotImplementedError: + self.destination_local = False + else: + self.destination_local = True + + if symlink: + if sys.platform == 'win32': + raise CommandError("Symlinking is not supported by this " + "platform (%s)." % sys.platform) + if not self.destination_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. + +This will overwrite existing files. +Are you sure you want to do this? + +Type 'yes' to continue, or 'no' to cancel: """) + if confirm != 'yes': + raise CommandError("Static files build cancelled.") + + for finder in finders.get_finders(): + for source, prefix, storage in finder.list(ignore_patterns): + self.copy_file(source, prefix, storage, **options) + + verbosity = int(options.get('verbosity', 1)) + actual_count = len(self.copied_files) + len(self.symlinked_files) + unmodified_count = len(self.unmodified_files) + if verbosity >= 1: + self.stdout.write("\n%s static file%s %s to '%s'%s.\n" + % (actual_count, actual_count != 1 and 's' or '', + symlink and 'symlinked' or 'copied', + settings.STATICFILES_ROOT, + unmodified_count and ' (%s unmodified)' + % unmodified_count or '')) + + def copy_file(self, source, prefix, 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 prefix: + destination = '/'.join([prefix, source]) + else: + destination = source + symlink = options['link'] + dry_run = options['dry_run'] + verbosity = int(options.get('verbosity', 1)) + + if destination in self.copied_files: + if verbosity >= 2: + self.stdout.write("Skipping '%s' (already copied earlier)\n" + % destination) + return False + + if destination in self.symlinked_files: + if verbosity >= 2: + self.stdout.write("Skipping '%s' (already linked earlier)\n" + % destination) + return False + + if self.destination_storage.exists(destination): + try: + destination_last_modified = \ + self.destination_storage.modified_time(destination) + except (OSError, NotImplementedError): + # storage doesn't support ``modified_time`` or failed. + pass + else: + destination_is_link= os.path.islink( + self.destination_storage.path(destination)) + if destination_last_modified == source_last_modified: + if (not symlink and not destination_is_link): + if verbosity >= 2: + self.stdout.write("Skipping '%s' (not modified)\n" + % destination) + self.unmodified_files.append(destination) + return False + if dry_run: + if verbosity >= 2: + self.stdout.write("Pretending to delete '%s'\n" + % destination) + else: + if verbosity >= 2: + self.stdout.write("Deleting '%s'\n" % destination) + self.destination_storage.delete(destination) + + if symlink: + destination_path = self.destination_storage.path(destination) + if dry_run: + if verbosity >= 1: + self.stdout.write("Pretending to symlink '%s' to '%s'\n" + % (source_path, destination_path)) + else: + if verbosity >= 1: + self.stdout.write("Symlinking '%s' to '%s'\n" + % (source_path, destination_path)) + try: + os.makedirs(os.path.dirname(destination_path)) + except OSError: + pass + os.symlink(source_path, destination_path) + self.symlinked_files.append(destination) + else: + if dry_run: + if verbosity >= 1: + self.stdout.write("Pretending to copy '%s' to '%s'\n" + % (source_path, destination)) + 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) + if verbosity >= 1: + self.stdout.write("Copying '%s' to '%s'\n" + % (source_path, destination_path)) + else: + source_file = source_storage.open(source) + self.destination_storage.save(destination, source_file) + if verbosity >= 1: + self.stdout.write("Copying %s to %s\n" + % (source_path, destination)) + self.copied_files.append(destination) + return True diff --git a/django/contrib/staticfiles/management/commands/findstatic.py b/django/contrib/staticfiles/management/commands/findstatic.py new file mode 100644 index 0000000000..0f13277c37 --- /dev/null +++ b/django/contrib/staticfiles/management/commands/findstatic.py @@ -0,0 +1,24 @@ +import os +from optparse import make_option +from django.core.management.base import LabelCommand + +from django.contrib.staticfiles import finders + +class Command(LabelCommand): + help = "Finds the absolute paths for the given static file(s)." + args = "[file ...]" + label = 'static file' + option_list = LabelCommand.option_list + ( + make_option('--first', action='store_false', dest='all', default=True, + help="Only return the first match for each static file."), + ) + + def handle_label(self, path, **options): + verbosity = int(options.get('verbosity', 1)) + result = finders.find(path, all=options['all']) + if result: + output = '\n '.join((os.path.realpath(path) for path in result)) + self.stdout.write("Found %r here:\n %s\n" % (path, output)) + else: + if verbosity >= 1: + self.stdout.write("No matching file found for %r.\n" % path) diff --git a/django/contrib/staticfiles/models.py b/django/contrib/staticfiles/models.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/staticfiles/models.py diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py new file mode 100644 index 0000000000..87b569af76 --- /dev/null +++ b/django/contrib/staticfiles/storage.py @@ -0,0 +1,84 @@ +import os +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.files.storage import FileSystemStorage +from django.utils.importlib import import_module + +from django.contrib.staticfiles import utils + + +class StaticFilesStorage(FileSystemStorage): + """ + Standard file system storage for site media files. + + The defaults for ``location`` and ``base_url`` are + ``STATICFILES_ROOT`` and ``STATICFILES_URL``. + """ + def __init__(self, location=None, base_url=None, *args, **kwargs): + if location is None: + location = settings.STATICFILES_ROOT + if base_url is None: + base_url = settings.STATICFILES_URL + if not location: + raise ImproperlyConfigured("You're using the staticfiles app " + "without having set the STATICFILES_ROOT setting. Set it to " + "the absolute path of the directory that holds static media.") + if not base_url: + raise ImproperlyConfigured("You're using the staticfiles app " + "without having set the STATICFILES_URL setting. Set it to " + "URL that handles the files served from STATICFILES_ROOT.") + super(StaticFilesStorage, self).__init__(location, base_url, *args, **kwargs) + + +class AppStaticStorage(FileSystemStorage): + """ + A file system storage backend that takes an app module and works + for the ``static`` directory of it. + """ + source_dir = 'static' + + def __init__(self, app, *args, **kwargs): + """ + Returns a static file storage if available in the given app. + """ + # app is actually the models module of the app. Remove the '.models'. + bits = app.__name__.split('.')[:-1] + self.app_name = bits[-1] + self.app_module = '.'.join(bits) + # The models module (app) may be a package in which case + # dirname(app.__file__) would be wrong. Import the actual app + # as opposed to the models module. + app = import_module(self.app_module) + location = self.get_location(os.path.dirname(app.__file__)) + super(AppStaticStorage, self).__init__(location, *args, **kwargs) + + def get_location(self, app_root): + """ + Given the app root, return the location of the static files of an app, + by default 'static'. We special case the admin app here since it has + its static files in 'media'. + """ + if self.app_module == 'django.contrib.admin': + return os.path.join(app_root, 'media') + return os.path.join(app_root, self.source_dir) + + def get_prefix(self): + """ + Return the path name that should be prepended to files for this app. + """ + if self.app_module == 'django.contrib.admin': + return self.app_name + return None + + def get_files(self, ignore_patterns=[]): + """ + Return a list containing the relative source paths for all files that + should be copied for an app. + """ + files = [] + prefix = self.get_prefix() + for path in utils.get_files(self, ignore_patterns): + if prefix: + path = '/'.join([prefix, path]) + files.append(path) + return files diff --git a/django/contrib/staticfiles/templatetags/__init__.py b/django/contrib/staticfiles/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/staticfiles/templatetags/__init__.py diff --git a/django/contrib/staticfiles/templatetags/staticfiles.py b/django/contrib/staticfiles/templatetags/staticfiles.py new file mode 100644 index 0000000000..6153f5a385 --- /dev/null +++ b/django/contrib/staticfiles/templatetags/staticfiles.py @@ -0,0 +1,43 @@ +from django import template +from django.utils.encoding import iri_to_uri + +register = template.Library() + +class StaticFilesPrefixNode(template.Node): + + def __init__(self, varname=None): + self.varname = varname + + def render(self, context): + try: + from django.conf import settings + except ImportError: + prefix = '' + else: + prefix = iri_to_uri(settings.STATICFILES_URL) + if self.varname is None: + return prefix + context[self.varname] = prefix + return '' + +@register.tag +def get_staticfiles_prefix(parser, token): + """ + Populates a template variable with the prefix (settings.STATICFILES_URL). + + Usage:: + + {% get_staticfiles_prefix [as varname] %} + + Examples:: + + {% get_staticfiles_prefix %} + {% get_staticfiles_prefix as staticfiles_prefix %} + + """ + tokens = token.contents.split() + if len(tokens) > 1 and tokens[1] != 'as': + raise template.TemplateSyntaxError( + "First argument in '%s' must be 'as'" % tokens[0]) + return StaticFilesPrefixNode(varname=(len(tokens) > 1 and tokens[2] or None)) + diff --git a/django/contrib/staticfiles/urls.py b/django/contrib/staticfiles/urls.py new file mode 100644 index 0000000000..131b10266b --- /dev/null +++ b/django/contrib/staticfiles/urls.py @@ -0,0 +1,29 @@ +import re +from django.conf import settings +from django.conf.urls.defaults import patterns, url, include +from django.core.exceptions import ImproperlyConfigured + +urlpatterns = [] + +# only serve non-fqdn URLs +if not settings.DEBUG: + urlpatterns += patterns('', + url(r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve'), + ) + +def staticfiles_urlpatterns(prefix=None): + """ + Helper function to return a URL pattern for serving static files. + """ + if settings.DEBUG: + return [] + if prefix is None: + prefix = settings.STATICFILES_URL + if '://' in prefix: + raise ImproperlyConfigured( + "The STATICFILES_URL setting is a full URL, not a path and " + "can't be used with the urls.staticfiles_urlpatterns() helper.") + if prefix.startswith("/"): + prefix = prefix[1:] + return patterns('', + url(r'^%s' % re.escape(prefix), include(urlpatterns)),) diff --git a/django/contrib/staticfiles/utils.py b/django/contrib/staticfiles/utils.py new file mode 100644 index 0000000000..f5a30befe9 --- /dev/null +++ b/django/contrib/staticfiles/utils.py @@ -0,0 +1,30 @@ +import fnmatch + +def get_files(storage, ignore_patterns=[], location=''): + """ + Recursively walk the storage directories gathering a complete list of files + that should be copied, returning this list. + + """ + def is_ignored(path): + """ + Return True or False depending on whether the ``path`` should be + ignored (if it matches any pattern in ``ignore_patterns``). + + """ + for pattern in ignore_patterns: + if fnmatch.fnmatchcase(path, pattern): + return True + return False + + directories, files = storage.listdir(location) + static_files = [location and '/'.join([location, fn]) or fn + for fn in files + if not is_ignored(fn)] + for dir in directories: + if is_ignored(dir): + continue + if location: + dir = '/'.join([location, dir]) + static_files.extend(get_files(storage, ignore_patterns, dir)) + return static_files diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py new file mode 100644 index 0000000000..27d499df87 --- /dev/null +++ b/django/contrib/staticfiles/views.py @@ -0,0 +1,159 @@ +""" +Views and functions for serving static files. These are only to be used during +development, and SHOULD NOT be used in a production setting. + +""" +import mimetypes +import os +import posixpath +import re +import stat +import urllib +from email.Utils import parsedate_tz, mktime_tz + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified +from django.template import loader, Template, Context, TemplateDoesNotExist +from django.utils.http import http_date + +from django.contrib.staticfiles import finders + + +def serve(request, path, document_root=None, show_indexes=False): + """ + Serve static files below a given point in the directory structure or + from locations inferred from the static files finders. + + To use, put a URL pattern such as:: + + (r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve') + + in your URLconf. + + If you provide the ``document_root`` parameter, the file won't be looked + up with the staticfiles finders, but in the given filesystem path, e.g.:: + + (r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve', {'document_root' : '/path/to/my/files/'}) + + You may also set ``show_indexes`` to ``True`` if you'd like to serve a + basic index of the directory. This index view will use the + template hardcoded below, but if you'd like to override it, you can create + a template called ``static/directory_index.html``. + """ + if settings.DEBUG: + raise ImproperlyConfigured("The view to serve static files can only " + "be used if the DEBUG setting is True") + if not document_root: + absolute_path = finders.find(path) + if not absolute_path: + raise Http404("%r could not be matched to a static file." % path) + document_root, path = os.path.split(absolute_path) + # Clean up given path to only allow serving files below document_root. + path = posixpath.normpath(urllib.unquote(path)) + path = path.lstrip('/') + newpath = '' + for part in path.split('/'): + if not part: + # Strip empty path components. + continue + drive, part = os.path.splitdrive(part) + head, part = os.path.split(part) + if part in (os.curdir, os.pardir): + # Strip '.' and '..' in path. + continue + newpath = os.path.join(newpath, part).replace('\\', '/') + if newpath and path != newpath: + return HttpResponseRedirect(newpath) + fullpath = os.path.join(document_root, newpath) + if os.path.isdir(fullpath): + if show_indexes: + return directory_index(newpath, fullpath) + raise Http404("Directory indexes are not allowed here.") + if not os.path.exists(fullpath): + raise Http404('"%s" does not exist' % fullpath) + # Respect the If-Modified-Since header. + statobj = os.stat(fullpath) + mimetype, encoding = mimetypes.guess_type(fullpath) + mimetype = mimetype or 'application/octet-stream' + if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), + statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): + return HttpResponseNotModified(mimetype=mimetype) + contents = open(fullpath, 'rb').read() + response = HttpResponse(contents, mimetype=mimetype) + response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) + response["Content-Length"] = len(contents) + if encoding: + response["Content-Encoding"] = encoding + return response + + +DEFAULT_DIRECTORY_INDEX_TEMPLATE = """ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="Content-Language" content="en-us" /> + <meta name="robots" content="NONE,NOARCHIVE" /> + <title>Index of {{ directory }}</title> + </head> + <body> + <h1>Index of {{ directory }}</h1> + <ul> + {% ifnotequal directory "/" %} + <li><a href="../">../</a></li> + {% endifnotequal %} + {% for f in file_list %} + <li><a href="{{ f|urlencode }}">{{ f }}</a></li> + {% endfor %} + </ul> + </body> +</html> +""" + +def directory_index(path, fullpath): + try: + t = loader.select_template(['static/directory_index.html', + 'static/directory_index']) + except TemplateDoesNotExist: + t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template') + files = [] + for f in os.listdir(fullpath): + if not f.startswith('.'): + if os.path.isdir(os.path.join(fullpath, f)): + f += '/' + files.append(f) + c = Context({ + 'directory' : path + '/', + 'file_list' : files, + }) + return HttpResponse(t.render(c)) + +def was_modified_since(header=None, mtime=0, size=0): + """ + Was something modified since the user last downloaded it? + + header + This is the value of the If-Modified-Since header. If this is None, + I'll just return True. + + mtime + This is the modification time of the item we're talking about. + + size + This is the size of the item we're talking about. + """ + try: + if header is None: + raise ValueError + matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, + re.IGNORECASE) + header_mtime = mktime_tz(parsedate_tz(matches.group(1))) + header_len = matches.group(3) + if header_len and int(header_len) != size: + raise ValueError + if mtime > header_mtime: + raise ValueError + except (AttributeError, ValueError, OverflowError): + return True + return False |
