Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial checkin from Pinax' repository

  • Loading branch information...
commit 12c39731c18f6eef26084ad609bdc7569022a281 0 parents
@jezdez authored
3  AUTHORS
@@ -0,0 +1,3 @@
+Jannis Leidel <jannis@leidel.info>
+Brian Beck <exogen@gmail.com>
+Brian Rosner <brosner@gmail.com>
28 LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2009, Jannis Leidel
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of the author nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2  MANIFEST.in
@@ -0,0 +1,2 @@
+include AUTHORS
+include LICENSE
2  setup.cfg
@@ -0,0 +1,2 @@
+[egg_info]
+tag_build = dev
23 setup.py
@@ -0,0 +1,23 @@
+from setuptools import setup, find_packages
+
+setup(
+ name='django-staticfiles',
+ version='0.1.0',
+ description="A Django app that provides helpers for serving static files.",
+ author='Jannis Leidel',
+ author_email='jannis@leidel.info',
+ license='BSD',
+ url='http://bitbucket.org/jezdez/django-staticfiles/',
+ download_url='http://bitbucket.org/jezdez/django-staticfiles/downloads/',
+ packages=find_packages(),
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Framework :: Django',
+ ],
+ zip_safe=False,
+)
0  staticfiles/__init__.py
No changes.
0  staticfiles/management/__init__.py
No changes.
0  staticfiles/management/commands/__init__.py
No changes.
286 staticfiles/management/commands/build_media.py
@@ -0,0 +1,286 @@
+import os
+import sys
+import glob
+import shutil
+from optparse import make_option
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.db.models import get_app
+from django.utils.text import get_text_list
+from django.core.management.base import CommandError, AppCommand
+
+from staticfiles.utils import import_module
+from staticfiles.settings import (ROOT, DIRS, APPS,
+ MEDIA_DIRNAMES, PREPEND_LABEL_APPS)
+
+prepend_label_apps = [a.rsplit('.', 1)[-1] for a in PREPEND_LABEL_APPS]
+
+try:
+ set
+except NameError:
+ from sets import Set as set # Python 2.3 fallback
+
+class Command(AppCommand):
+ """
+ Command that allows to copy or symlink media files from different
+ locations to the settings.STATIC_ROOT.
+
+ Based on the collectmedia management command by Brian Beck:
+ http://blog.brianbeck.com/post/50940622/collectmedia
+ """
+ media_files = {}
+ media_dirs = MEDIA_DIRNAMES
+ media_root = ROOT
+ exclude = ['CVS', '.*', '*~']
+ option_list = AppCommand.option_list + (
+ make_option('-i', '--interactive', action='store_true', dest='interactive',
+ help="Run in interactive mode, asking before modifying files and selecting from multiple sources."),
+ make_option('-a', '--all', action='store_true', dest='all',
+ help="Traverse all installed apps."),
+ make_option('--media-root', default=media_root, dest='media_root', metavar='DIR',
+ help="Specifies the root directory in which to collect media files."),
+ make_option('-m', '--media-dir', action='append', default=media_dirs, dest='media_dirs', metavar='DIR',
+ help="Specifies the name of the media directory to look for in each app."),
+ make_option('-e', '--exclude', action='append', default=exclude, dest='exclude', metavar='PATTERNS',
+ help="A space-delimited list of glob-style patterns to ignore. Use multiple times to add more."),
+ make_option('-n', '--dry-run', action='store_true', dest='dry_run',
+ help="Do everything except modify the filesystem."),
+ make_option('-l', '--link', action='store_true', dest='link',
+ help="Create a symbolic link to each file instead of copying."),
+ )
+ help = 'Collect media files from apps and other locations in a single media directory.'
+ args = '[appname appname ...]'
+
+ def handle(self, *app_labels, **options):
+ media_root = os.path.normpath(options.get('media_root'))
+
+ if not os.path.isdir(media_root):
+ raise CommandError(
+ 'Designated media location %s could not be found.' % media_root)
+
+ if options.get('dry_run', False):
+ print "\n DRY RUN! NO FILES WILL BE MODIFIED."
+ print "\nCollecting media in %s" % media_root
+
+ if app_labels:
+ try:
+ app_list = self.load_apps(
+ [get_app(label).__name__.rsplit('.', 1)[0] for label in app_labels]
+ )
+ except (ImproperlyConfigured, ImportError), e:
+ raise CommandError(
+ "%s. Is your INSTALLED_APPS setting correct?" % e)
+ else:
+ if not options.get('all', False):
+ raise CommandError('Enter at least one appname or use the --all option')
+ app_list = self.load_apps(APPS)
+
+ traversed_apps = [app.__name__.rsplit('.', 1)[-1] for app in app_list]
+ print "Traversing apps: %s" % get_text_list(traversed_apps, 'and')
+ for app_mod in app_list:
+ self.handle_app(app_mod, **options)
+
+ if not app_labels:
+ # Look in additional locations for media, only if --all is used.
+ extra_media = []
+ for label, path in DIRS:
+ if os.path.isdir(path):
+ extra_media.append((label, path))
+ extra_labels = [label for label, path in extra_media]
+ print "Looking additionally in: %s" % get_text_list(extra_labels, 'and')
+ exclude = options.get('exclude')
+ for extra_label, extra_path in extra_media:
+ self.add_media_files(extra_label, extra_path, exclude)
+
+ # This mapping collects files that may be copied. Keys are what the
+ # file's path relative to `media_root` will be when copied. Values
+ # are a list of 2-tuples containing the the name of the app providing
+ # the file and the file's absolute path. The list will have a length
+ # greater than 1 if multiple apps provide a media file with the same
+ # relative path.
+
+ # Forget the unused versions of a media file
+ for f in self.media_files:
+ self.media_files[f] = dict(self.media_files[f]).items()
+
+ # Stop if no media files were found
+ if not self.media_files:
+ print "\nNo media found."
+ return
+
+ interactive = options.get('interactive', False)
+ # Try to copy in some predictable order.
+ destinations = list(self.media_files)
+ destinations.sort()
+ for destination in destinations:
+ sources = self.media_files[destination]
+ first_source, other_sources = sources[0], sources[1:]
+ if interactive and other_sources:
+ first_app = first_source[0]
+ app_sources = dict(sources)
+ for (app, source) in sources:
+ if destination.startswith(app):
+ first_app = app
+ first_source = (app, source)
+ break
+ print "\nThe file %r is provided by multiple apps:" % destination
+ print "\n".join([" %s" % app for (app, source) in sources])
+ message = "Enter the app that should provide this file [%s]: " % first_app
+ while True:
+ app = raw_input(message)
+ if not app:
+ app, source = first_source
+ break
+ elif app in app_sources:
+ source = app_sources[app]
+ break
+ else:
+ print "The app %r does not provide this file." % app
+ else:
+ app, source = first_source
+
+ # Special case apps that have media in <app>/media, not in
+ # <app>/media/<app>, e.g. django.contrib.admin
+ if app in prepend_label_apps:
+ destination = os.path.join(app, destination)
+ print "\nSelected %r provided by %r." % (destination, app)
+ self.process_file(source, destination, media_root, **options)
+
+ def handle_app(self, app, **options):
+ exclude = options.get('exclude')
+ media_dirs = options.get('media_dirs')
+ app_label = app.__name__.rsplit('.', 1)[-1]
+ app_root = os.path.dirname(app.__file__)
+ for media_dir in media_dirs:
+ app_media = os.path.join(app_root, media_dir)
+ if os.path.isdir(app_media):
+ self.add_media_files(app_label, app_media, exclude)
+
+ def load_apps(self, apps):
+ app_list = []
+ for app_entry in apps:
+ try:
+ app_mod = import_module(app_entry)
+ except ImportError, e:
+ raise CommandError('ImportError %s: %s' % (app_entry, e.args[0]))
+ app_media_dir = os.path.join(
+ os.path.dirname(app_mod.__file__), 'media')
+ if os.path.isdir(app_media_dir):
+ app_list.append(app_mod)
+ return app_list
+
+ def add_media_files(self, app, location, exclude):
+ prefix_length = len(location) + len(os.sep)
+ for root, dirs, files in os.walk(location):
+ # Filter files based on the exclusion pattern.
+ for filename in self.filter_names(files, exclude=exclude):
+ absolute_path = os.path.join(root, filename)
+ relative_path = absolute_path[prefix_length:]
+ self.media_files.setdefault(
+ relative_path, []).append((app, absolute_path))
+
+ def process_file(self, source, destination, root, link=False, **options):
+ dry_run = options.get('dry_run', False)
+ interactive = options.get('interactive', False)
+ destination = os.path.abspath(os.path.join(root, destination))
+ if not dry_run:
+ # Get permission bits and ownership of `root`.
+ try:
+ root_stat = os.stat(root)
+ except os.error, e:
+ mode = 0777 # Default for `os.makedirs` anyway.
+ uid = gid = None
+ else:
+ mode = root_stat.st_mode
+ uid, gid = root_stat.st_uid, root_stat.st_gid
+ destination_dir = os.path.dirname(destination)
+ try:
+ # Recursively create all the required directories, attempting
+ # to use the same mode as `root`.
+ os.makedirs(destination_dir, mode)
+ except os.error, e:
+ # This probably just means the leaf directory already exists,
+ # but if not, we'll find out when copying or linking anyway.
+ pass
+ else:
+ if None not in (uid, gid):
+ os.lchown(destination_dir, uid, gid)
+ if link:
+ success = self.link_file(source, destination, interactive, dry_run)
+ else:
+ success = self.copy_file(source, destination, interactive, dry_run)
+ if success and None not in (uid, gid):
+ # Try to use the same ownership as `root`.
+ os.lchown(destination, uid, gid)
+
+ def copy_file(self, source, destination, interactive=False, dry_run=False):
+ "Attempt to copy `source` to `destination` and return True if successful."
+ if interactive:
+ exists = os.path.exists(destination) or os.path.islink(destination)
+ if exists:
+ print "The file %r already exists." % destination
+ if not self.prompt_overwrite(destination):
+ return False
+ print "Copying %r to %r." % (source, destination)
+ if not dry_run:
+ try:
+ os.remove(destination)
+ except os.error, e:
+ pass
+ shutil.copy2(source, destination)
+ return True
+ return False
+
+ def link_file(self, source, destination, interactive=False, dry_run=False):
+ "Attempt to link to `source` from `destination` and return True if successful."
+ if sys.platform == 'win32':
+ message = "Linking is not supported by this platform (%s)."
+ raise os.error(message % sys.platform)
+
+ if interactive:
+ exists = os.path.exists(destination) or os.path.islink(destination)
+ if exists:
+ print "The file %r already exists." % destination
+ if not self.prompt_overwrite(destination):
+ return False
+ if not dry_run:
+ try:
+ os.remove(destination)
+ except os.error, e:
+ pass
+ print "Linking to %r from %r." % (source, destination)
+ if not dry_run:
+ os.symlink(source, destination)
+ return True
+ return False
+
+ def prompt_overwrite(self, filename, default=True):
+ "Prompt the user to overwrite and return their selection as True or False."
+ yes_values = ['Y']
+ no_values = ['N']
+ if default:
+ prompt = "Overwrite? [Y/n]: "
+ yes_values.append('')
+ else:
+ prompt = "Overwrite? [y/N]: "
+ no_values.append('')
+ while True:
+ overwrite = raw_input(prompt).strip().upper()
+ if overwrite in yes_values:
+ return True
+ elif overwrite in no_values:
+ return False
+ else:
+ print "Select 'Y' or 'N'."
+
+ def filter_names(self, names, exclude=None, func=glob.fnmatch.filter):
+ if exclude is None:
+ exclude = []
+ elif isinstance(exclude, basestring):
+ exclude = exclude.split()
+ else:
+ exclude = [pattern for patterns in exclude for pattern in patterns.split()]
+ excluded_names = set(
+ [name for pattern in exclude for name in func(names, pattern)])
+ return set(names) - excluded_names
17 staticfiles/management/commands/resolve_media.py
@@ -0,0 +1,17 @@
+import os
+from django.core.management.base import LabelCommand
+from staticfiles.utils import get_media_path
+
+class Command(LabelCommand):
+ help = "Finds the location of the given media by resolving its path."
+ args = "[media_path]"
+ label = 'media path'
+
+ def handle_label(self, media_path, **options):
+ print "Resolving %s:" % media_path
+ paths = get_media_path(media_path, all=True)
+ if paths is None:
+ print " No media found."
+ else:
+ for path in paths:
+ print u" %s" % os.path.realpath(path)
0  staticfiles/models.py
No changes.
26 staticfiles/settings.py
@@ -0,0 +1,26 @@
+import os.path
+from django.conf import settings
+from django.core.exceptios import ImproperlyConfigured
+
+# The directory in which the static files are collected in
+ROOT = getattr(settings, 'STATIC_ROOT', None)
+if ROOT is None:
+ raise ImproperlyConfigured('Please set your STATIC_ROOT setting to an '
+ 'existing directory in which the staticfiles can be collected.')
+
+# A tuple of two-tuples with a name and the path of additional directories
+# which hold static files and should be taken into account
+DIRS = getattr(settings, 'STATICFILES_DIRS', ())
+
+# Names of sub directories in apps that should be automatically scanned
+MEDIA_DIRNAMES = getattr(settings, 'STATICFILES_MEDIA_DIRNAMES', ['media'])
+
+# Apps that have media in <app>/media, not in <app>/media/<app>,
+# e.g. django.contrib.admin
+PREPEND_LABEL_APPS = getattr(settings, 'STATICFILES_PREPEND_LABEL_APPS',
+ ('django.contrib.admin',))
+
+# Apps that shouldn't be taken into account when collecting app media
+EXCLUDED_APPS = getattr(settings, 'STATICFILES_EXCLUDED_APPS', ())
+
+APPS = [app for app in settings.INSTALLED_APPS if app not in EXCLUDED_APPS]
9 staticfiles/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+from django.conf import settings
+
+urlpatterns = patterns('',
+ (r'^static/(?P<path>.*)$', 'staticfiles.views.serve'),
+ (r'^media/(?P<path>.*)$', 'django.views.static.serve', {
+ 'document_root': settings.MEDIA_ROOT
+ }),
+)
81 staticfiles/utils.py
@@ -0,0 +1,81 @@
+import os
+import sys
+from django.conf import settings
+
+from staticfiles.settings import ROOT, DIRS, MEDIA_DIRNAMES, APPS
+
+def get_media_path(path, all=False):
+ """
+ Traverses the following locations to find a requested media file in the
+ given order and return the absolute file path:
+
+ 1. The site media path, e.g. for user-contributed files, e.g.:
+ <project>/site_media/static/<path>
+ 2. Any extra media locations given in the settings
+ 4. Installed apps:
+ a) <app>/media/<app>/<path>
+ b) <app>/media/<path>
+ """
+ collection = []
+ locations = [ROOT] + [root for label, root in DIRS]
+ for location in locations:
+ media = os.path.join(location, path)
+ if os.path.exists(media):
+ if not all:
+ return media
+ collection.append(media)
+
+ installed_apps = APPS
+ app_labels = [label.split('.')[-1] for label in installed_apps]
+ for app in installed_apps:
+ app_mod = import_module(app)
+ app_root = os.path.dirname(app_mod.__file__)
+ for media_dir in MEDIA_DIRNAMES:
+ media = os.path.join(app_root, media_dir, path)
+ if os.path.exists(media):
+ if not all:
+ return media
+ collection.append(media)
+ splitted_path = path.split('/', 1)
+ if len(splitted_path) > 1:
+ app_name, newpath = splitted_path
+ if app_name in app_labels:
+ media = os.path.join(app_root, media_dir, newpath)
+ if os.path.exists(media):
+ if not all:
+ return media
+ collection.append(media)
+ return collection or None
+
+def _resolve_name(name, package, level):
+ """Return the absolute name of the module to be imported."""
+ if not hasattr(package, 'rindex'):
+ raise ValueError("'package' not set to a string")
+ dot = len(package)
+ for x in xrange(level, 1, -1):
+ try:
+ dot = package.rindex('.', 0, dot)
+ except ValueError:
+ raise ValueError("attempted relative import beyond top-level "
+ "package")
+ return "%s.%s" % (package[:dot], name)
+
+def import_module(name, package=None):
+ """Import a module.
+
+ The 'package' argument is required when performing a relative import. It
+ specifies the package to use as the anchor point from which to resolve the
+ relative import to an absolute import.
+
+ """
+ if name.startswith('.'):
+ if not package:
+ raise TypeError("relative imports require the 'package' argument")
+ level = 0
+ for character in name:
+ if character != '.':
+ break
+ level += 1
+ name = _resolve_name(name[level:], package, level)
+ __import__(name)
+ return sys.modules[name]
67 staticfiles/views.py
@@ -0,0 +1,67 @@
+"""
+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 os
+import stat
+import urllib
+import mimetypes
+import posixpath
+
+from django.utils.http import http_date
+from django.views.static import was_modified_since, directory_index
+from django.http import Http404, HttpResponse, HttpResponseRedirect, \
+ HttpResponseNotModified
+from staticfiles.utils import get_media_path
+
+def serve(request, path, show_indexes=False):
+ """
+ Serve static files below a given point in the directory structure.
+
+ To use, put a URL pattern such as::
+
+ (r'^(?P<path>.*)$', 'staticfiles.views.serve')
+
+ in your URLconf. 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``.
+ """
+
+ # 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 = get_media_path(newpath)
+ if fullpath is None:
+ raise Http404, '"%s" does not exist' % newpath
+ if not os.path.exists(fullpath):
+ raise Http404, '"%s" does not exist' % fullpath
+ if os.path.isdir(fullpath):
+ if show_indexes:
+ return directory_index(newpath, fullpath)
+ raise Http404, "Directory indexes are not allowed here."
+ # Respect the If-Modified-Since header.
+ statobj = os.stat(fullpath)
+ mimetype = mimetypes.guess_type(fullpath)[0] 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)
+ return response
Please sign in to comment.
Something went wrong with that request. Please try again.