Writing a Simple Filesystem Browser with Django

Dieser Artikel ist mal wieder in Englisch, da er auch für die Leute auf #django interessant sein könnte.

This posting will show how to build a very simple filesystem browser with Django. This filesystem browser behaves mostly like a static webserver that allows directory traversal. The only speciality is that you can use the Django admin to define filesystems that are mounted into the namespace of the Django server. This is just to demonstrate how a Django application can make use of different data sources besides the database, it's not really meant to serve static content (although with added authentication it could come in quite handy for restricted static content!).

Even though the application makes very simple security checks on passed in filenames, you shouldn't run this on a public server - I didn't do any security tests and there might be buttloads of bad things in there that might expose your private data to the world. You have been warned.

We start as usual by creating the filesystems application with the django-admin.py startapp filesystems command. Just do it like you did with your polls application in the first tutorial. Just as an orientation, this is how the myproject directory does look like on my development machine:
.
|-- apps
|   |-- filesystems
|   |   |-- models
|   |   |-- urls
|   |   `-- views
|   `-- polls
|       |-- models
|       |-- urls
|       `-- views
|-- public_html
|   `-- admin_media
|       |-- css
|       |-- img
|       |   `-- admin
|       `-- js
|           `-- admin
|-- settings
|   `-- urls
`-- templates
    `-- filesystems

After creating the infrastructure, we start by building the model. The model for the filesystems is very simple - just a name for the filesystem and a path where the files are actually stored. So here it is, the model:

   from django.core import meta

class Filesystem(meta.Model):

fields = (
    meta.CharField('name', 'Name', maxlength=64),
meta.CharField('path', 'Path', maxlength=200),
)

def __repr__(self):
    return self.name

def get_absolute_url(self):
    return '/files/%s/' % self.name

def isdir(self, path):
    import os
    p = os.path.realpath(os.path.join(self.path, path))
    if not p.startswith(self.path): raise ValueError(path)
return os.path.isdir(p)

def files(self, path=''):
    import os
import mimetypes
p = os.path.realpath(os.path.join(self.path, path))
if not p.startswith(self.path): raise ValueError(path)
l = os.listdir(p)
if path: l.insert(0, '..')
    return [(f, os.path.isdir(os.path.join(p, f)),
                mimetypes.guess_type(f)[0] or 'application/octetstream')
           for f in l]

def file(self, path):
    import os
import mimetypes
p = os.path.realpath(os.path.join(self.path, path))
if p.startswith(self.path):
    (t, e) = mimetypes.guess_type(p)
    return (p, t or 'application/octetstream')
else: raise ValueError(path)

admin = meta.Admin(
    fields = (
        (None, {'fields': ('name', 'path')}),
),
    list_display = ('name', 'path'),
    search_fields = ('name', 'path'),
    ordering = ['name'],
)

As you can see, the model and the admin is rather boring. What is interesting, though, are the additional methods isdir, files and file. isdir just checks wether a given path below the filesystem is a directory or not. files returns the files of the given path below the filesystems base path and file returns the real pathname and the mimetype of a given file below the filesystems base path. All three methods check for validity of the passed in path - if the resulting path isn't below the filesystems base path, a ValueError is thrown. This is to make sure that nobody uses .. in the path name to break out of the defined filesystem area. So the model includes special methods you can use to access the filesystems content itself, without caring for how to do that in your views. It's job of the model to know about such stuff.

The next part of your little filesystem browser will be the URL configuration. It's rather simple, it consists of the line in settings/urls/main.py and the myproject.apps.filesystems.urls.filesystems module. Fist the line in the main urls module:

   from django.conf.urls.defaults import *

urlpatterns = patterns('', (r'^files/', include('myproject.apps.filesystems.urls.filesystems')), )

Next the filesystems own urls module:

   from django.conf.urls.defaults import *

urlpatterns = patterns('myproject.apps.filesystems.views.filesystems', (r'^$', 'index'), (r'^(?P<filesystem_name>.*?)/(?P<path>.*)$', 'directory'), )

You can now add the application to the main settings file so you don't forget to do that later on. Just look for the INSTALLED_APPS setting and add the filebrowser:

   INSTALLED_APPS = (
       'myproject.apps.polls',
       'myproject.apps.filesystems'
   )
   
One part is still missing: the views. This module defines the externally reachable methods we defined in the urlmapper. So we need two methods, index and directory. The second one actually doesn't work only with directories - if it get's passed a file, it just presents the contents of that file with the right mimetype. The view makes use of the methods defined in the model to access actual filesystem contents. Here is the source for the views module:
   from django.core import template_loader
   from django.core.extensions import DjangoContext as Context
   from django.core.exceptions import Http404
   from django.models.filesystems import filesystems
   from django.utils.httpwrappers import HttpResponse

def index(request): fslist = filesystems.getlist(orderby=['name']) t = templateloader.gettemplate('filesystems/index') c = Context(request, { 'fslist': fslist, }) return HttpResponse(t.render(c))

def directory(request, filesystem_name, path): import os try: fs = filesystems.getobject(name exact=filesystemname) if fs.isdir(path): files = fs.files(path) tpl = templateloader.gettemplate('filesystems/directory') c = Context(request, { 'dlist': [f for (f, d, t) in files if d], 'flist': [{'name':f, 'type':t} for (f, d, t) in files if not d], 'path': path, 'fs': fs, }) return HttpResponse(tpl.render(c)) else: (f, mimetype) = fs.file(path) return HttpResponse(open(f).read(), mimetype=mimetype) except ValueError: raise Http404 except filesystems.FilesystemDoesNotExist: raise Http404 except IOError: raise Http404

See how the elements of the directory pattern are passed in as parameters to the directory method - the filesystem name is used to find the right filesystem and the path is used to access content below that filesystems base path. Mimetypes are discovered using the mimetypes module from the python distribution, btw.

The last part of our little tutorial are the templates. We need two templates - one for the index of the defined filesystems and one for the content of some path below some filesystem. We don't need a template for the files content - file content is delivered raw. So first the main index template:

{% if fslist %}
<h1>defined filesystems</h1>
<ul>
{% for fs in fslist %}
<li><a href="{{ fs.get_absolute_url }}">{{ fs.name }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>Sorry, no filesystems have been defined.</p>
{% endif %}

The other template is the directory template that shows contents of a path below the filesystems base path:

   {% if dlist or flist %}
   <h1>Files in //{{ fs.name }}/{{ path }}</h1>
   <ul>
   {% for d in dlist %}
   <li> <a href="{{ fs.getabsoluteurl }}{{ path }}{{ d }}/" >{{ d }}</a> </li>
   {% endfor %}
   {% for f in flist %}
   <li> <a href="{{ fs.getabsoluteurl }}{{ path }}{{ f.name }}" >{{ f.name }}</a> ({{ f.type }})</li>
   {% endfor %}
   </ul>
   {% endif %}
   
Both templates need to be stored somewhere in your TEMPLATEPATH. I have set up a path in the TEMPLATEPATH with the name of the application: filesystems. In there I stored the files as index.html and directory.html. Of course you normally would build a base template for the site and extend that in your normal templates. And you would add a 404.html to handle 404 errors. But that's left as an exercise to the reader.After you start up your development server for your admin (don't forget to set DJANGOSETTINGSMODULE accordingly!) you can add a filesystem to your database (you did do django-admin.py install filesystems sometime in between? No? Do it now, before you start your server). Now stop the admin server, change your DJANGOSETTINGSMODULE and start the main settings server. Now you can surf to http://localhost:8000/files/ (at least if you did set up your URLs and server like I do) and browse the files in your filesystem. That's it. Wasn't very complicated, right? Django is really simple to use lachendes Gesicht

tags: Django, Programmierung, Python, Texte

Rainer Klein Aug. 7, 2005, 2:47 p.m.

Georg,

Vielen Dank fuer den grossartiken Artikel.

- Bei meinem Versuch 'das Filesystem' auf meinem Rechner nachzuvollziehen scheitere ich allerdings mit vollgender Fehlermeldung im Browser: "Sorry, no filesystems have been defined." - Ich konnte nicht herausfinden, wo der "base path" gesetzt wird?

Dabei muss ich auch gestehen, das die 'poll' Anwendung von dem Django Tutorial auch noch nicht laueft. - Hier lauf ich in den Fehler:
..
File "/usr/lib/python2.4/site-packages/django/core/urlresolvers.py", line 85, in resolve
raise Http404, "Tried all URL patterns but didn't find a match for %r" % app_path

Http404: Tried all URL patterns but didn't find a match for 'polls/'

Dem Text im Tutorial konnte ich nicht erkennen, wo die richtige URL eingestellt wird?

Was mach ich falsch?

Mit freundlichen Gruessen,

Rainer




hugo Aug. 7, 2005, 3:10 p.m.

Dein Problem bei den Polls klingt danach das du wohl das falsche Settings-File benutzt. Django braucht halt immer _ein_ Settings-File das für eine Applikation aktiv sein muss. Die Benutzerseite der Anwendungen gibts über das settings.main, die Administration über settings.admin. Ansonsten steht in den Tutorials drin wo die URLs und deren Patterns eingestellt werden.

Die Filebrowser-Geschichte macht erst Sinn wenn du die normalen Tutorials einmal durchgearbeitet hast, denn ich hab natürlich nicht all den ganzen Kleinkram nochmal hingeschrieben der schon in den Tutorials steht.

Beim Filesystem gibt es übrigens sowohl Admin als auch Userseite - also erstmal im Admin (dafür dann das admin Settings File aktivieren) ein Filesystemobjekt anlegen, danach funktioniert dann auch erst der Filebrowser auf der User-Seite (dafür dann natürlich wieder das main Settings File aktivieren).

Das Umschalten der Settings-Files umgeht man dadurch das man einfach in zwei Fenstern unterschiedliche Settings-Files per Environment aktiviert hat und dann den Entwicklungsserver auf unterschiedlichen Ports laufen lässt - dann kann man je nach Port auf die beiden Sichten der Django-Anwendung gucken.