Samstag, 5. November 2011

Bottle, WTForms und Datei-Uploads

Wer Webanwendung schreibt kommt wahrscheinlich auch irgendwann mal an den Punkt, wo dem Anwender die Möglichkeit gegeben wird, Dateien auf den Server hoch zuladen. Natürlich bietet mein derzeit favorisiertes WSGI-Framework Bottle auch Funktionen an, um dies zu realisieren. Ebenso bietet mein derzeit favorisiertes HTML Form Framework WTForms eine entsprechende Klasse an. Um die "Hochzeit" der beiden geht es in diesem Blogeintrag.

Letztendlich ist das alles kein großes Ding - die jeweiligen Dokumentationen (Link und Link) sind zwar kurz, aber aussagekräftig. Aber ich kann ja trotzdem ein paar Stolpersteine aus dem Weg räumen.

Dazu im folgenden eine kleine Beispielapplikation. Diese besteht aus drei Dateien:
  • myapp.py, welche die eigentliche Applikation ist
  • forms.py, welche die Definition der Formularklasse enthält
  • formtemplate.tpl, welche das Template für die Formulardarstellung enthält
Die Datei "forms.py" sieht dabei so aus:

# -*- coding: utf-8 -*-

import re
from wtforms import Form, FileField, TextField, SubmitField,\     validators

class AttachmentForm(Form):
    attachment = FileField(u'Datei',
        [validators.regexp(r'.+\.(jpg|pdf|png|docx|doc|txt)$',
        flags=2,
        message=u'Es sind nur Dateien mit der Endung jpg, pdf,\
        png, docx, doc und txt erlaubt!')])
    textfield = TextField(u'Bemerkung',
            [validators.Required(
             message=u'Es muss ein Text eingegeben werden!'),
             validators.Length(max=50,
             message=u'Der Text darf nicht länger als 50 Zeichen\
             sein.')])
    send = SubmitField(u'Senden')

    def validate_attachment(form, field):
        if field.data:
            field.data = re.sub(
                r'[^a-zA-Z0-9_.-]', '_', field.data)

Der Regex-Validator kann natürlich nach belieben geändert (oder weg gelassen) werden. Gleiches gilt für das Textfeld namens "textfield", welches hier jedoch verwendet wird, um später den Unterschied Formulardaten und Upload-Daten deutlich zu machen (siehe unten). Der Validator "validate_attachment" ist auch optional - aber hier durchaus sinnvoll. Es validiert nämlich nicht, sondern ersetzt einfach alle (Sonder-) Zeichen im Dateinamen, welche später Probleme machen könnten, durch einen Unterstrich.

Das Template "formtemplate.tpl" sieht so aus:

<html>
<head>
<title>Datei-Upload</title>
</head>
<body>
<h2>Datei-Upload</h2>
%if errors:
<p class="fehler">Die Eingabe enthält <a href="#fehler">Fehler</a></p>
%end
<form action="/upload" method="post" enctype="multipart/form-data">
<ul>
<li>{{!form.attachment.label()}}<br/> {{!form.attachment(size=75)}}</li>
<br/>
<li>{{!form.textfield.label()}}<br/> {{!form.textfield(size=50)}}</li>
</ul>
<p>{{!form.send()}}</p>
</form>
<p>Hinweis: Alle Zeichen im Dateinamen, welche nicht im Bereich a-z, A-Z, 0-9, _, - und . enthalten sind, werden durch einen Unterstrich _ ausgetauscht. Dies betrifft also auch die Zeichen ä, ö, ü und ß sowie das Leerzeichen.</p>
%if errors:
<p style="color: #ff0000;">Fehler:</p>
%for k,v in errors.iteritems():
<p>{{!form[k].label}}: {{!v[0]}}</p>
%end
%end
</body>
</html>

Auch hier gibt es nicht außergewöhnliches. Es gibt ein rudimentäres HTML-Gerüst, das Formular an sich inklusive Hinweistext und die Anzeige von Fehlern im Formular, sofern dieses Fehler hat.

Die Hauptdatei "myapp.py" sieht so aus:

#!/usr/bin/env python

import os
from bottle import route, template, request, run, debug
import forms

PATH = '/home/noisefloor/attachment'

@route('/upload')
@route('/upload', method='POST')
def upload():
    req = request.forms
    data = request.files.get('attachment')
    form = forms.AttachmentForm(req)
    try:
        form.attachment.data = data.filename
    except:
        form.attachment.data = None
    if not req or not form.validate():
        return template('formtemplate.tpl',
            form=form,errors=form.errors)
    else:
        raw = data.file.read()
        with open(os.path.join(PATH, data.filename),'wb') as f:
            f.write(raw)
        return u'Datei {0} gespeichert, Bemerkung: {1}'.format(
            data.filename, form.textfield.data)

if __name__ == '__main__':
    debug(True)
    run(reloader=True)

Ein paar Anmerkungen sind hier zu machen:

Bottle hält die Daten des Dateiuploads unter request.files vor, während die Formulardaten über request.forms abrufbar sind. Dabei ist zu beachten, dass auch der Dateiname der herauf geladenen Datei nicht in request.forms liegt, sondern in ebenfalls in request.files. Es ist in der Tat nur der eigentliche Name der Datei dort hinterlegt, ohne den (lokalen) Dateipfad, von der die Datei hoch geladen wurde.

Von daher ist auch die Zeile

form.attachment.data = data.filename

wichtig, da so der Dateiname in die Formulardaten geschrieben wird. Ohne diesen Schritt würde das Formular nie validieren, da "attachment" ein Pflichtfeld ist.

Werden die Formular- und Uploaddaten übermittelt und Validiert das Formular, dann wird die Datei lokal gespeichert. Wichtig ist hier, dass der Benutzer, unter dem die Anwendung läuft, auch Schreibrechte in dem entsprechenden Verzeichnis hat.

In einer realen Anwendung würde man die Zeile

raw = data.file.read()

vielleicht nicht alleine so verwenden, da der Upload von sehr großen Dateien zu Problemen wie z.B. einem komplett vollem RAM führen kann. Von daher wird in der Bottle-Dokumentation ebenfalls vor dieser Zeile gewarnt.

In der Beispielanwendung wird das Feld "textfield" nicht weiter verwendet. In einer realen Applikation würde man diese z.B. zusammen mit dem Dateinamen in einer Datenbank speichern.

Zusammenfassend kann gesagt werden, dass die Kombination von Bottle und WTForms auch für Dateiuploads nicht weiter schwierig, sofern man einige wenige Punkte beachtet.

Kommentare:

  1. "NameError: name 'validators' is not defined"

    Bitte korrigieren!

    AntwortenLöschen
    Antworten
    1. Ups, da fehlt wirklich der Import für "validators". Korrigiert, Danke für den Hinweis! :-)

      Löschen
  2. Hallo

    noch eine Frage/Anregung:

    forms.py", line 22, in validate_attachment
    field.data = re.sub(
    NameError: global name 're' is not defined

    AntwortenLöschen
  3. Hallo

    Ich nochmals. Erst mal vielen Dank für das kleine aber tolle Tut. Es funktioniert soweit auch alles prima, wenn man die Funktion validate_attachment(form, field): rausnimmt. Könntest du diese Funktion noch erklären?

    mfg

    AntwortenLöschen
    Antworten
    1. 1) da fehlt der Import des RegEx Moduls re aus der Standardbibliothek... ist drin
      2) re.sub(...) ersetzt einfach alle Zeichen, die _nicht_ im Bereich a-z, A-Z und 0-9 liegen durch einen Unterstrich _ .

      Löschen