diff --git a/django/conf/urls/static.py b/django/conf/urls/static.py new file mode 100644 index 0000000..07a67cc --- /dev/null +++ b/django/conf/urls/static.py @@ -0,0 +1,21 @@ +import re +from django.conf import settings +from django.conf.urls.defaults import patterns, url +from django.core.exceptions import ImproperlyConfigured + +def static(prefix, view='django.views.static.serve', **kwargs): + """ + Helper function to return a URL pattern for serving files in debug mode. + + urlpatterns += static( + prefix=settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + """ + if not settings.DEBUG: + return [] + elif not prefix: + raise ImproperlyConfigured("Empty static prefix not permitted") + elif '://' in prefix: + raise ImproperlyConfigured("URL '%s' not allowed as static prefix" % prefix) + return patterns('', + url(r'^%s(?P.*)$' % re.escape(prefix.lstrip('/')), view, **kwargs), + ) diff --git a/django/contrib/staticfiles/urls.py b/django/contrib/staticfiles/urls.py index aa4ab45..04062c1 100644 --- a/django/contrib/staticfiles/urls.py +++ b/django/contrib/staticfiles/urls.py @@ -1,28 +1,16 @@ -import re from django.conf import settings -from django.conf.urls.defaults import patterns, url, include -from django.core.exceptions import ImproperlyConfigured +from django.conf.urls.static import static urlpatterns = [] -# only serve non-fqdn URLs -if settings.DEBUG: - urlpatterns += patterns('', - url(r'^(?P.*)$', 'django.contrib.staticfiles.views.serve'), - ) - def staticfiles_urlpatterns(prefix=None): """ Helper function to return a URL pattern for serving static files. """ - if not settings.DEBUG: - return [] if prefix is None: prefix = settings.STATIC_URL - if not prefix or '://' in prefix: - raise ImproperlyConfigured( - "The prefix for the 'staticfiles_urlpatterns' helper is invalid.") - if prefix.startswith("/"): - prefix = prefix[1:] - return patterns('', - url(r'^%s' % re.escape(prefix), include(urlpatterns)),) + return static(prefix, view='django.contrib.staticfiles.views.serve') + +# Only append if urlpatterns are empty +if settings.DEBUG and not urlpatterns: + urlpatterns += staticfiles_urlpatterns() diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py index f5a6ec3..4d2a50d 100644 --- a/django/contrib/staticfiles/views.py +++ b/django/contrib/staticfiles/views.py @@ -3,24 +3,16 @@ 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.http import Http404 +from django.views.static import serve as static_serve -from django.contrib.staticfiles import finders, utils +from django.contrib.staticfiles import finders - -def serve(request, path, document_root=None, show_indexes=False, insecure=False): +def serve(request, path, document_root=None, insecure=False, **kwargs): """ Serve static files below a given point in the directory structure or from locations inferred from the static files finders. @@ -42,124 +34,13 @@ def serve(request, path, document_root=None, show_indexes=False, insecure=False) a template called ``static/directory_index.html``. """ if not settings.DEBUG and not insecure: - raise ImproperlyConfigured("The view to serve static files can only " - "be used if the DEBUG setting is True or " - "the --insecure option of 'runserver' is " - "used") + raise ImproperlyConfigured("The staticfiles view can only be used in " + "debug mode or if the the --insecure " + "option of 'runserver' is used") if not document_root: path = os.path.normpath(path) absolute_path = finders.find(path) if not absolute_path: raise Http404('"%s" could not be found' % 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 = """ - - - - - - - Index of {{ directory }} - - -

Index of {{ directory }}

- - - -""" - -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_date = parsedate_tz(matches.group(1)) - if header_date is None: - raise ValueError - header_mtime = mktime_tz(header_date) - 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 + return static_serve(request, path, document_root=document_root, **kwargs) diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 7772a0b..34c76f4 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -18,8 +18,9 @@ import warnings from django.core.management.color import color_style from django.utils.http import http_date from django.utils._os import safe_join +from django.views.static import serve -from django.contrib.staticfiles import handlers, views as static +from django.contrib.staticfiles import handlers __version__ = "0.1" __all__ = ['WSGIServer','WSGIRequestHandler'] @@ -677,8 +678,7 @@ class AdminMediaHandler(handlers.StaticFilesHandler): def serve(self, request): document_root, path = os.path.split(self.file_path(request.path)) - return static.serve(request, path, - document_root=document_root, insecure=True) + return serve(request, path, document_root=document_root) def _should_handle(self, path): """ diff --git a/django/views/static.py b/django/views/static.py index 2ce886f..9b34312 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -9,7 +9,6 @@ import posixpath import re import stat import urllib -import warnings from email.Utils import parsedate_tz, mktime_tz from django.template import loader @@ -17,11 +16,8 @@ from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpRespons from django.template import Template, Context, TemplateDoesNotExist from django.utils.http import http_date -from django.contrib.staticfiles.views import (directory_index, - was_modified_since, serve as staticfiles_serve) - -def serve(request, path, document_root=None, show_indexes=False, insecure=False): +def serve(request, path, document_root=None, show_indexes=False): """ Serve static files below a given point in the directory structure. @@ -35,7 +31,114 @@ def serve(request, path, document_root=None, show_indexes=False, insecure=False) but if you'd like to override it, you can create a template called ``static/directory_index.html``. """ - warnings.warn("The view at `django.views.static.serve` is deprecated; " - "use the path `django.contrib.staticfiles.views.serve` " - "instead.", PendingDeprecationWarning) - return staticfiles_serve(request, path, document_root, show_indexes, insecure) + # 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 = """ + + + + + + + Index of {{ directory }} + + +

Index of {{ directory }}

+ + + +""" + +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_date = parsedate_tz(matches.group(1)) + if header_date is None: + raise ValueError + header_mtime = mktime_tz(header_date) + 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 diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt index 94e04bb..b68004d 100644 --- a/docs/howto/static-files.txt +++ b/docs/howto/static-files.txt @@ -109,10 +109,9 @@ the framework see :doc:`the staticfiles reference `. :setting:`MEDIA_URL` different from your :setting:`STATIC_ROOT` and :setting:`STATIC_URL`. You will need to arrange for serving of files in :setting:`MEDIA_ROOT` yourself; ``staticfiles`` does not deal with - user-uploaded files at all. You can, however, use ``staticfiles``' - :func:`~django.contrib.staticfiles.views.serve` view for serving - :setting:`MEDIA_ROOT` in development; see - :ref:`staticfiles-serve-other-directories`. + user-uploaded files at all. You can, however, use + :func:`~django.views.static.serve` view for serving :setting:`MEDIA_ROOT` + in development; see :ref:`staticfiles-serve-other-directories`. .. _staticfiles-in-templates: diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index dd26fe5..9a9ee0b 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -343,5 +343,13 @@ by appending something like this to your URLconf:: {'document_root': settings.MEDIA_ROOT}), ) +Luckily Django also ships with a small URL helper function that shrinks +that down a bit:: + + from django.conf import settings + from django.conf.urls.static import static + + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + This snippet assumes you've also set your :setting:`MEDIA_URL` (in development) to ``/media/``. diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py index d524254..0ceb11b 100644 --- a/tests/regressiontests/staticfiles_tests/tests.py +++ b/tests/regressiontests/staticfiles_tests/tests.py @@ -293,9 +293,8 @@ class TestServeDisabled(TestServeStatic): settings.DEBUG = False def test_disabled_serving(self): - self.assertRaisesRegexp(ImproperlyConfigured, 'The view to serve ' - 'static files can only be used if the DEBUG setting is True', - self._response, 'test.txt') + self.assertRaisesRegexp(ImproperlyConfigured, 'The staticfiles view ' + 'can only be used in debug mode ', self._response, 'test.txt') class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults):