Flask-Datei-Uploads mit Validierung und privatem Speicher
Um eine Datei mit Flask hochzuladen, senden Sie ein Multipart-Formular ab, validieren die Anfrage und speichern den hochgeladenen Stream. Diese Anleitung zeigt ein lokales Browserformular, das JPEG-, PNG- und PDF-Dateien akzeptiert und außerhalb des öffentlichen Verzeichnisses für statische Dateien speichert. Außerdem prüfen Sie die gespeicherten Bytes und sehen, was passiert, wenn ein Upload abgelehnt wird.
Flask-Umgebung einrichten
Verwenden Sie Linux mit Bash, Python 3.14, Unterstützung für virtuelle Umgebungen und installiertem libmagic. Die Installationsanleitung für python-magic behandelt die Systembibliothek; die Installation des Python-Pakets allein installiert diese nicht mit. Das Beispiel verwendet Flask 3.1.3, Flask-WTF 1.3.0 und python-magic 0.4.27.
Führen Sie in einem Verzeichnis, in dem Sie Ihre Projekte ablegen, Folgendes aus:
mkdir flask-upload-demo &&
cd flask-upload-demo &&
python3 -m venv .venv &&
.venv/bin/python -m pip install Flask==3.1.3 Flask-WTF==1.3.0 python-magic==0.4.27 &&
mkdir templates
Die Befehle brechen ab, wenn das Projekt bereits existiert oder ein Einrichtungsschritt fehlschlägt.
Fahren Sie erst nach erfolgreichem Abschluss fort, während sich Ihr Terminal weiterhin in
flask-upload-demo befindet. Erstellen Sie dort die folgenden zwei Dateien.
Datei-Uploadformular erstellen
Speichern Sie dies als templates/upload.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Upload a file</title>
</head>
<body>
<h1>Upload a file</h1>
<p>Choose a JPEG, PNG, or PDF. The whole request must fit within 16 MiB.</p>
{% for message in get_flashed_messages() %}
<p role="status">{{ message }}</p>
{% endfor %}
{% if error %}<p role="alert">{{ error }}</p>{% endif %}
<form method="post" enctype="multipart/form-data" novalidate>
{{ form.hidden_tag() }}
{{ form.file.label }} {{ form.file }}
{% for errors in form.errors.values() %}
{% for message in errors %}
<p role="alert">{{ message }}</p>
{% endfor %}
{% endfor %}
<button type="submit">Upload</button>
</form>
</body>
</html>
Die Multipart-Codierung überträgt die Datei-Bytes. hidden_tag() gibt das CSRF-Token
aus, das Flask-WTF anhand der Browsersitzung prüft. novalidate ermöglicht es Ihnen,
die Serverantwort bei fehlender Datei zu testen, statt das Absenden bereits durch den Browser
verhindern zu lassen. Die Formularklasse steht weiter unten in app.py.
Verarbeitung von Datei-Uploads implementieren
Speichern Sie diese vollständige Anwendung als app.py:
import os
import tempfile
from pathlib import Path
import magic
from flask import Flask, flash, redirect, render_template, request, url_for
from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed, FileField, FileRequired
from werkzeug.exceptions import RequestEntityTooLarge
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
upload_directory = Path(app.instance_path) / 'uploads'
upload_directory.mkdir(mode=0o700, parents=True, exist_ok=True)
ALLOWED_TYPES = {
'image/jpeg': ('jpg', 'jpeg'),
'image/png': ('png',),
'application/pdf': ('pdf',),
}
class UploadForm(FlaskForm):
file = FileField('File', validators=[
FileRequired('Choose a file.'),
FileAllowed(['jpg', 'jpeg', 'png', 'pdf'], 'Choose a JPEG, PNG, or PDF.'),
])
def save_upload(file):
extension = file.filename.rsplit('.', 1)[-1].lower()
detected_type = magic.from_buffer(file.read(2048), mime=True)
file.seek(0)
extensions = ALLOWED_TYPES.get(detected_type)
if extensions is None or extension not in extensions:
raise ValueError('The file contents do not match an allowed file type and extension.')
# The client filename never becomes a storage path.
target = tempfile.NamedTemporaryFile(
dir=upload_directory, prefix='upload-', suffix='.' + extensions[0], delete=False,
)
try:
with target:
file.save(target)
except OSError:
Path(target.name).unlink(missing_ok=True)
raise
return Path(target.name).name
@app.errorhandler(RequestEntityTooLarge)
def too_large(error):
# Do not parse the rejected request again while rendering its error page.
form = UploadForm(formdata=None)
return render_template(
'upload.html', form=form,
error='Upload request is too large. Choose a smaller file.',
), 413
@app.route('/', methods=['GET', 'POST'])
def upload():
form = UploadForm()
if request.method == 'GET':
return render_template('upload.html', form=form)
if not form.validate_on_submit():
return render_template('upload.html', form=form), 400
try:
filename = save_upload(form.file.data)
except ValueError:
return render_template(
'upload.html', form=form,
error='The file contents do not match an allowed file type and extension.',
), 400
except (OSError, magic.MagicException) as error:
app.logger.error('Upload failed (%s)', type(error).__name__)
return render_template(
'upload.html', form=form,
error='The file could not be saved. Please try again.',
), 500
flash(f'Saved as {filename}.')
return redirect(url_for('upload'), code=303)
Die Dateivalidatoren von Flask-WTF prüfen, ob eine Datei ausgewählt wurde und ihre Erweiterung zulässig ist. Die Hilfsfunktion untersucht anschließend mit libmagic die ersten 2.048 Bytes gemäß den Hinweisen von python-magic und setzt den Stream vor dem Speichern an den Anfang zurück. Ohne dieses Zurücksetzen würden der gespeicherten Datei die bereits gelesenen Bytes fehlen. Der vom Browser angegebene MIME-Typ wird für diese Entscheidung nicht verwendet.
Diese Prüfungen lehnen offensichtliche Abweichungen ab, etwa eine Textdatei, die in
.png umbenannt wurde. Sie decodieren die Datei nicht vollständig, erkennen
nicht alle Beschädigungen und belegen nicht, dass ihr Inhalt harmlos ist. Halten Sie Uploads privat,
bis alle erforderlichen Sicherheitsscans oder formatspezifischen Verarbeitungsschritte Ihrer
Anwendung erfolgreich abgeschlossen sind.
NamedTemporaryFile erstellt bei jedem akzeptierten Upload einen neuen Namen. Unter Linux
sind die Dateiberechtigungen auf den erstellenden Benutzer beschränkt. Selbst zwei Uploads namens
photo.png erhalten separate Dateien. Trotz des Funktionsnamens bewahrt
delete=False erfolgreiche Uploads in instance/uploads auf, bis Sie sie
entfernen. Ein abgefangener Fehler beim Schreiben oder Schließen löst das Entfernen der Teildatei
dieses Versuchs aus. Ein beendeter Prozess oder ein Dateisystem, das die Löschung verweigert, kann
jedoch eine Datei zurücklassen.
Formular starten und einen Upload prüfen
Erzeugen Sie im selben Projektverzeichnis einen lokalen geheimen Sitzungsschlüssel und starten Sie Flask:
FLASK_SECRET_KEY="$(.venv/bin/python -c 'import secrets; print(secrets.token_hex(32))')" &&
export FLASK_SECRET_KEY &&
.venv/bin/python -m flask --app app run --host 127.0.0.1 --port 5000
Öffnen Sie http://127.0.0.1:5000/, wählen Sie mit
File eine kleine JPEG-, PNG- oder PDF-Datei aus und klicken Sie auf Upload.
Die zurückgegebene Seite zeigt Saved as,
gefolgt vom erzeugten Dateinamen. Ein erfolgreicher POST leitet auf einen GET weiter, sodass ein
Neuladen der Ergebnisseite die Datei nicht erneut sendet. Wenn Sie das Formular bewusst erneut
absenden, wird eine weitere Kopie erstellt.
Wechseln Sie in einem anderen Terminal ins Projektverzeichnis und vergleichen Sie Ihre Originaldatei mit der gespeicherten Datei. Ersetzen Sie beide Beispielpfade und verwenden Sie dabei den erzeugten Namen von der Seite:
cmp -- '/path/to/photo.png' 'instance/uploads/upload-example.png'
Keine Ausgabe und der Exit-Status null bedeuten, dass die Bytes übereinstimmen. Die gespeicherte
Datei liegt außerhalb des Flask-Verzeichnisses static, und diese Anwendung hat
keine Downloadroute. Schließen Sie das Instanzverzeichnis von allen öffentlichen
Webserver-Zuordnungen aus.
Stoppen Sie Flask mit Strg+C. Zum Neustart führen Sie den Startbefehl erneut im Projektverzeichnis aus. Er erzeugt einen neuen geheimen Schlüssel. Laden Sie daher jedes offene Formular vor dem Absenden neu. Wenn Port 5000 belegt ist, wählen Sie im Befehl und in der Browser-URL einen ungenutzten Port. Dieser Entwicklungsserver bleibt auf Loopback beschränkt; die Anwendung hat weder eine Anmeldung noch ein Speicherkontingent oder eine automatische Löschrichtlinie.
Fehlerbehandlung und Validierung
Testen Sie diese Fälle und beobachten Sie dabei den POST-Status im Netzwerkbereich des Browsers:
| Eingabe oder Bedingung | Erwartete Antwort | Gespeichertes Ergebnis |
|---|---|---|
| Ohne Dateiauswahl absenden | 400 und Choose a file. | Keine neue Datei |
Eine Datei mit der Erweiterung .txt auswählen | 400 und Choose a JPEG, PNG, or PDF. | Keine neue Datei |
Reinen Text in .png umbenennen oder eine leere Datei mit der Erweiterung .png absenden | 400 und eine Meldung über eine Abweichung zwischen Inhalt und Typ | Keine neue Datei |
| Eine Datei mit 16 MiB oder mehr auswählen | 413 und Upload request is too large. Choose a smaller file. | Keine neue Datei |
Das versteckte Eingabeelement csrf_token vor dem Absenden in den Browser-Entwicklertools entfernen | 400 und The CSRF token is missing. | Keine neue Datei |
| Hochladen, wenn das Speicherverzeichnis nicht beschreibbar ist | 500 und The file could not be saved. Please try again. | Keine neue Datei, wenn das Erstellen fehlgeschlagen ist |
Das Anfragelimit umfasst Multipart-Felder und Trennmarkierungen, sodass die größte akzeptierte Datei etwas kleiner als 16 MiB ist. Flask begrenzt außerdem Größe und Anzahl der Multipart-Felder. Die Upload-Dokumentation weist darauf hin, dass manche Uploads auf dem Entwicklungsserver mit einem Verbindungsreset enden können, anstatt eine Antwort mit Status 413 anzuzeigen.
Laden Sie bei CSRF-Fehlern die Seite neu und wählen Sie die Datei erneut aus. Halten Sie den geheimen Schlüssel beim Betrieb mehrerer Anwendungsprozesse unverändert: Diese müssen bei Sitzungs- und Token-Signaturen übereinstimmen. Der CSRF-Schutz validiert das Sitzungstoken des Formulars; er authentifiziert nicht die hochladende Person. Bei Speicherfehlern wird nur der Ausnahmetyp protokolliert. Lokale Pfade und Diagnosedetails erscheinen nicht auf der Seite.
Wenn Sie eine Flask-API für Datei-Uploads benötigen
Dieses Beispiel gibt HTML zurück und benötigt das Sitzungscookie sowie das CSRF-Token des Browsers. Eine reine Multipart-Anfrage mit cURL scheitert daher an der Formularvalidierung. Legen Sie für eine separate API fest, wer hochladen darf, wie sich Clients authentifizieren und welche JSON-Antworten sie verarbeiten, bevor Sie die Validierungs- und Speicherlogik wiederverwenden. Eine per Cookie authentifizierte API benötigt weiterhin CSRF-Schutz; ein anderes Antwortformat hebt diese Anforderung nicht auf.
Große Datei-Uploads verarbeiten
Wenn Sie die Anwendung später hinter Nginx betreiben, kann dessen
client_max_body_size
die Anfrage ablehnen, bevor sie Flask erreicht. Stimmen Sie die Limits von Proxy und Anwendung
aufeinander ab und behandeln Sie Fehler auf beiden Ebenen. Eine Erhöhung des Proxy-Limits allein
kann das Flask-Limit von 16 MiB nicht außer Kraft setzen.
Verwenden Sie für fortsetzbare Uploads ein Protokoll wie tus mit einem kompatiblen Server und Client. Das Formular hier sendet eine einzige Multipart-Anfrage. Das Aufteilen einer Datei in Teilstücke im Browser würde eine andere Serverimplementierung erfordern, um diese zu verfolgen und zusammenzusetzen.
Die Hintergrundverarbeitung ist ein separater Schritt nach dem Empfang einer Datei. Ein Worker kann gespeicherte Bytes verarbeiten, macht die Uploads dieses Formulars aber weder fortsetzbar noch umgeht er das Anfragelimit. Dieses lokale Beispiel endet, sobald die Datei gespeichert ist. Falls die Verarbeitung später langsam wird, erklärt die Celery-Anleitung von Flask die zusätzliche Einrichtung von Worker und Broker. Außerdem benötigen Sie eine Richtlinie für fehlgeschlagene Jobs und aufbewahrte Uploads.
Bevor Sie Uploads öffentlich zugänglich machen
Legen Sie eine Aufbewahrungsrichtlinie für instance/uploads fest, bevor Sie echte
Nutzerdaten sammeln. Ergänzen Sie Authentifizierung, benutzerspezifische Autorisierung und
Kontingente sowie die für Ihre Dateiformate erforderlichen Inhaltsprüfungen, bevor Sie öffentliche
Zugriffe zulassen oder Downloads bereitstellen. Diese Entscheidungen liegen bei der Anwendung, die
die hochgeladenen Dateien verwendet. Das lokale Formular bietet Ihnen die Möglichkeit, sie zu
testen.
