evansd/whitenoise

Support index files

ionelmc opened this issue · 12 comments

Eg: if there's an foo/index.html it should be served at foo/. Also, foo should redirect to foo/.

For me, this is getting too far away from WhiteNoise's original goals to be supported as a core feature. WhiteNoise works best when the files it serves can be cached forever, which means giving each version a unique name like app.ea27bc4.js. It's not really designed as a generic file server.

However, if you really want this feature you could implement it as a subclass quite easily. Something like this (untested) would probably work:

class WhiteNoiseIndex(WhiteNoise):

    def add_files(self, *args, **kwargs):
        super(WhiteNoiseIndex, self).add_files(*args, **kwargs)
        index_files = {}
        for url, static_file in self.files.items():
            if url.endswith('/index.html'):
                index_files[url[:-len('index.html')]] = static_file
        self.files.update(index_files)

For WhiteNoise v2.0+, this needs to be modified slightly, so that it works with autorefresh when developing. (Feature was added in 25f5757).

eg:

from whitenoise.django import DjangoWhiteNoise


class IndexWhiteNoise(DjangoWhiteNoise):
    """Adds support for serving index pages for directory paths."""

    INDEX_NAME = 'index.html'

    def add_files(self, *args, **kwargs):
        super(IndexWhiteNoise, self).add_files(*args, **kwargs)
        index_page_suffix = "/" + self.INDEX_NAME
        index_name_length = len(self.INDEX_NAME)
        index_files = {}
        for url, static_file in self.files.items():
            # Add an additional fake filename to serve index pages for '/'.
            if url.endswith(index_page_suffix):
                index_files[url[:-index_name_length]] = static_file
        self.files.update(index_files)

    def find_file(self, url):
        # In debug mode, find_file() is used to serve files directly from the filesystem
        # instead of using the list in `self.files`, so we append the index filename so
        # that will be served if present.
        if url[-1] == '/':
            url += self.INDEX_NAME
        return super(IndexWhiteNoise, self).find_file(url)

And for WhiteNoise 3+, something like:

from whitenoise.middleware import WhiteNoiseMiddleware


class IndexWhiteNoise(WhiteNoiseMiddleware):
    """Adds support for serving index pages for directory paths."""

    INDEX_NAME = 'index.html'

    def update_files_dictionary(self, *args):
        super(IndexWhiteNoise, self).update_files_dictionary(*args)
        index_page_suffix = '/' + self.INDEX_NAME
        index_name_length = len(self.INDEX_NAME)
        directory_indexes = {}
        for url, static_file in self.files.items():
            if url.endswith(index_page_suffix):
                # For each index file found, add a corresponding URL->content mapping
                # for the file's parent directory, so that the index page is served for
                # the bare directory URL ending in '/'.
                parent_directory_url = url[:-index_name_length]
                directory_indexes[parent_directory_url] = static_file
        self.files.update(directory_indexes)

    def find_file(self, url):
        # In debug mode, find_file() is used to serve files directly from the filesystem
        # instead of using the list in `self.files`, so we append the index filename so
        # that will be served if present.
        if url.endswith('/'):
            url += self.INDEX_NAME
        return super(IndexWhiteNoise, self).find_file(url)

@edmorley thanks, works splendidly even when DEBUG=False. You made a small typo, though – replace CustomWhiteNoise with IndexWhiteNoise.

@evansd would it be possible to add this IndexWhiteNoise middleware to WhiteNoise itself and maintain it together with the rest of the package? Pretty please ☺️ 🙏. I'm really trying to find a canonical and easy way to use Django for frontend MVC frameworks (which need a static file server runing on /) without maintaining multiple servers. I got very close with the config explained in this SO answer, but of course I forgot that django.contrib.staticfiles.views.serve doesn't work in production when DEBUG=False. IndexWhiteNoise would work for that case.

@metakermit, thank you - typo fixed :-)

@metakermit I'll consider it. It's the "and maintain it" bit of your request that worries me ;) WhiteNoise has already got a lot more complicated than I first imagined in would and I'm wary of adding more and more features.

@evansd @edmorley I realised that my use case is a bit wider than just serving index.html on /. I also needed support for frontend routing, using solutions such as react-router. I ended up extending the middleware a bit and overriding a few more WhiteNoise methods.

Since I understand that making this a part of WhiteNoise doesn't make sense based on what @evansd said about being wary of adding new features, I started a new project I named django-spa. The idea would be to use WhiteNoise as a dependency (keep track of your development) and make sure that a typical use-case of serving single-page apps on / works (where all non-Django routing is delegated to the JS frontend, including things like 404s). To support this I made a 3-step decision process which I made sure works both on development (DEBUG=True) and production deployments (DEBUG=False, serving files from the self.files dictionary):

  1. try to find a static file either on / or /static/ (or indexl.html if / is requested)
  2. see if Django matches the url (so that Django admin and templates still work)
  3. everything else gets routed to / (so that fronted routing can be used)

It's still early stage, as I need to make sure everything works on CDNs too. I'm testing it for an app of my own and plan to extend features as I bump into problems. Feedback welcome :)

I've finally added built-in support for serving index files in the upcoming 4.0 release:
http://whitenoise.evans.io/en/latest/django.html#index-files-django

Amazing - thank you! :-)

I haven't had a chance to test it yet, but I see from the docs that it redirects /foo to /foo/, which is something we'd actually explicitly prevented Django from doing in our project, by setting APPEND_SLASH to false (this makes them 404 instead, which is desirable when using django-rest-framework to prevent client libraries accidentally using the non-canonical API URLs and causing thousands of redirects an hour).

Presumably the redirect will now happen regardless, since Django won't see the request?

Could WhiteNoise either not touch URLs of form /foo and let Django choose whether to redirect them as before, or else honour the value of APPEND_SLASH?

Oh wait sorry I see that the redirect only occurs for index pages that otherwise exist, and not for all URLs without a trailing slash. This is actually the best of both worlds (we want redirection for static pages, just not the REST API endpoints).

Great! (Was just writing something making that point but you've beaten me to it :)

The combination of autorefresh and index files and handling redirects hasn't made for the prettiest bit of code, but I'm fairly confident it all works correctly.