Erstellen Sie einen perfekten Blog mit FastAPI: Bild-Upload
Daniel Hayes
Full-Stack Engineer · Leapcell

Im vorherigen Artikel haben wir eine Kommentar-Antwort-Funktion für unseren FastAPI-Blog implementiert und damit die Interaktivität des Kommentarbereichs erheblich verbessert.
Nun, da die Funktionalität für Beiträge und Kommentare recht vollständig ist, unterstützen die Beiträge selbst nur einfachen Text, was ein wenig eintönig ist.
In diesem Artikel fügen wir den Beiträgen eine Bild-Upload-Funktion hinzu, damit Ihre Blog-Inhalte reich an Text und Bildern sind und ausdrucksstärker wirken.
Das Prinzip hinter der Implementierung von Bild-Uploads ist wie folgt:
- Ein Benutzer wählt ein Bild auf der Frontend-Seite aus und lädt es hoch.
- Das Backend empfängt das Bild und speichert es in einem Objektspeicher-Service.
- Das Backend gibt eine öffentlich zugängliche URL für das Bild zurück.
- Das Frontend fügt diese URL in das Inhalts-Textfeld des Beitrags im Markdown-Format (

) ein. - Wenn der Beitragsinhalt schließlich als Webseite gerendert wird, ruft der Browser das Bild über diese URL ab und zeigt es an.
Schritt 1: S3-kompatiblen Objektspeicher vorbereiten
Zuerst benötigen wir einenOrt, an dem die von Benutzern hochgeladenen Bilder gespeichert werden können. Obwohl Sie sie direkt auf der Festplatte des Servers speichern könnten, wird es bei modernen Webanwendungen eher empfohlen, einen Objektspeicher-Service (wie AWS S3) zu verwenden, da dieser leichter zu warten, skalierbarer und kostengünstiger ist.
Zur Bequemlichkeit werden wir weiterhin Leapcell verwenden, das nicht nur eine Datenbank und Backend-Hosting bietet, sondern auch einen S3-kompatiblen Objektspeicher-Service.
Melden Sie sich in der Leapcell-Hauptoberfläche an und klicken Sie auf "Objektspeicher erstellen".
Geben Sie einen Namen ein, um den Objektspeicher zu erstellen.
Auf der Detailseite des Objektspeichers sehen Sie die Verbindungsparameter wie Endpoint, Access Key ID und Secret Access Key. Diese werden wir später in unserer Backend-Konfiguration verwenden.
Die Oberfläche bietet auch eine sehr praktische Benutzeroberfläche zum direkten Hochladen und Verwalten von Dateien im Browser.
Schritt 2: Die Upload-API im Backend implementieren
Als Nächstes bauen wir das FastAPI-Backend, um Datei-Uploads zu verarbeiten.
1. Abhängigkeiten installieren
Wir benötigen boto3
(das AWS SDK für Python), um Dateien in einen S3-kompatiblen Objektspeicher-Service hochzuladen. Zusätzlich benötigen wir eine Markdown-Parsing-Bibliothek, um Markdown-Format in HTML zu konvertieren, wenn Beiträge angezeigt werden. Hier wählen wir markdown2
.
Fügen Sie sie Ihrer requirements.txt
-Datei hinzu:
# requirements.txt # ... andere Pakete boto3 markdown2
Führen Sie dann den Installationsbefehl aus:
pip install -r requirements.txt
2. Einen Upload-Service erstellen
Um unseren Code übersichtlich zu halten, erstellen wir eine neue Service-Datei für die Datei-Upload-Funktionalität.
Erstellen Sie eine neue Datei uploads_service.py
im Stammverzeichnis des Projekts. Dieser Service ist für die Kernlogik der Kommunikation mit S3 zuständig.
# uploads_service.py import boto3 import uuid from fastapi import UploadFile # --- S3-Konfiguration --- # Es wird empfohlen, diese Werte aus Umgebungsvariablen zu lesen S3_ENDPOINT_URL = "https://objstorage.leapcell.io" S3_ACCESS_KEY_ID = "DEINE_ACCESS_KEY_ID" S3_SECRET_ACCESS_KEY = "DEIN_SECRET_ACCESS_KEY" S3_BUCKET_NAME = "mein-fastapi-blog-bilder" # Dein Bucket-Name S3_PUBLIC_URL = f"https://{S3_BUCKET_NAME}.leapcellobj.com" # Die öffentliche URL deines Buckets # S3-Client initialisieren s3_client = boto3.client( "s3", endpoint_url=S3_ENDPOINT_URL, aws_access_key_id=S3_ACCESS_KEY_ID, aws_secret_access_key=S3_SECRET_ACCESS_KEY, region_name="us-east-1", # Bei S3-kompatiblem Speicher ist die Region oft nominell ) def upload_file_to_s3(file: UploadFile) -> str: """ Lädt eine Datei nach S3 hoch und gibt deren öffentliche URL zurück. """ try: # Einen eindeutigen Dateinamen generieren, um Konflikte zu vermeiden file_extension = file.filename.split(".")[-1] unique_filename = f"{uuid.uuid4()}.{file_extension}" s3_client.upload_fileobj( file.file, # ein dateiähnliches Objekt S3_BUCKET_NAME, unique_filename, ExtraArgs={ "ContentType": file.content_type, "ACL": "public-read", # Die Datei als öffentlich lesbar festlegen }, ) # Die öffentliche URL der Datei zurückgeben return f"{S3_PUBLIC_URL}/{unique_filename}" except Exception as e: print(f"Fehler beim Hochladen nach S3: {e}") raise
Hinweis: Um die Implementierung zu vereinfachen, sind die S3-Verbindungsparameter hartcodiert. In einem realen Projekt wird dringend empfohlen, diese sensiblen Informationen in Umgebungsvariablen zu speichern und sie mithilfe von os.getenv()
zu lesen.
3. Eine Upload-Route erstellen
Erstellen wir nun eine neue Datei uploads.py
im Ordner routers
, um die API-Route zu definieren.
# routers/uploads.py from fastapi import APIRouter, Depends, UploadFile, File from auth_dependencies import login_required import uploads_service router = APIRouter() @router.post("/uploads/image") def upload_image( user: dict = Depends(login_required), # Nur eingeloggte Benutzer können hochladen file: UploadFile = File(...) ): """ Empfängt eine hochgeladene Bilddatei, lädt sie auf S3 hoch und gibt die URL zurück. """ url = uploads_service.upload_file_to_s3(file) return {"url": url}
Schließlich binden Sie dieses neue Router-Modul in der Hauptanwendung main.py
ein.
# main.py # ... andere Importe from routers import posts, users, auth, comments, uploads # importiere den uploads router # ... app = FastAPI(lifespan=lifespan) # ... # Router einbinden app.include_router(posts.router) app.include_router(users.router) app.include_router(auth.router) app.include_router(comments.router) app.include_router(uploads.router) # binde den uploads router ein
Schritt 3: Die FilePicker-API im Frontend integrieren
Nachdem das Backend bereit ist, modifizieren wir die Frontend-Seite new-post.html
, um die Upload-Funktionalität hinzuzufügen.
Es gibt zwei Möglichkeiten, Uploads zu handhaben: die moderne FilePicker-API und das traditionelle <input type="file">
.
Traditionelle Methode: <input type="file">
hat eine hervorragende Kompatibilität und wird von allen Browsern unterstützt. Seine API ist jedoch etwas veraltet, nicht sehr intuitiv und bietet eine schlechte Benutzererfahrung.
Moderne Methode: Die File System Access API ist einfacher zu bedienen, leistungsfähiger und kann zu einer besseren Benutzererfahrung führen. Ihre Kompatibilität ist jedoch nicht so gut wie die der traditionellen Methode, und sie muss in einem sicheren Kontext (HTTPS) ausgeführt werden.
Da unser Blog ein modernes Projekt ist, werden wir die FilePicker-API zur Implementierung von Datei-Uploads verwenden.
Öffnen Sie templates/new-post.html
und fügen Sie eine Symbolleiste und eine Schaltfläche "Bild hochladen" über der textarea
hinzu.
{% include "_header.html" %} <form action="/posts" method="POST" class="post-form"> <div class="form-group"> <label for="title">Titel</label> <input type="text" id="title" name="title" required /> </div> <div class="form-group"> <label for="content">Inhalt</label> <div class="toolbar"> <button type="button" id="upload-image-btn">Bild hochladen</button> </div> <textarea id="content" name="content" rows="10" required></textarea> </div> <button type="submit">Absenden</button> </form> <script> document.addEventListener("DOMContentLoaded", () => { const uploadBtn = document.getElementById("upload-image-btn"); const contentTextarea = document.getElementById("content"); uploadBtn.addEventListener("click", async () => { try { const [fileHandle] = await window.showOpenFilePicker({ types: [ { description: "Bilder", accept: { "image/*": [".png", ".jpeg", ".jpg", ".gif", ".webp"] }, }, ], }); const file = await fileHandle.getFile(); uploadFile(file); } catch (error) { // Ein AbortError wird ausgelöst, wenn der Benutzer die Dateiauswahl abbricht, wir ignorieren ihn if (error.name !== "AbortError") { console.error("FilePicker-Fehler:", error); } } }); function uploadFile(file) { if (!file) return; const formData = new FormData(); formData.append("file", file); // Eine einfache Ladeanzeige anzeigen uploadBtn.disabled = true; uploadBtn.innerText = "Hochladen..."; fetch("/uploads/image", { method: "POST", body: formData, // Hinweis: Sie mssen den Content-Type Header nicht manuell setzen, wenn Sie FormData verwenden }) .then((response) => response.json()) .then((data) => { if (data.url) { // Die zurückgegebene Bild-URL in die Textbox im Markdown-Format einfügen const markdownImage = ``; insertAtCursor(contentTextarea, markdownImage); } else { alert("Upload fehlgeschlagen. Bitte versuchen Sie es erneut."); } }) .catch((error) => { console.error("Upload-Fehler:", error); alert("Ein Fehler ist während des Uploads aufgetreten."); }) .finally(() => { uploadBtn.disabled = false; uploadBtn.innerText = "Bild hochladen"; }); } // Hilfsfunktion zum Einfügen von Text an der Cursorposition function insertAtCursor(myField, myValue) { if (myField.selectionStart || myField.selectionStart === 0) { var startPos = myField.selectionStart; var endPos = myField.selectionEnd; myField.value = myField.value.substring(0, startPos) + myValue + myField.value.substring(endPos, myField.value.length); myField.selectionStart = startPos + myValue.length; myField.selectionEnd = startPos + myValue.length; } else { myField.value += myValue; } } }); </script> {% include "_footer.html" %}
Schritt 4: Beiträge mit Bildern rendern
Wir haben erfolgreich den Markdown-Link für das Bild in den Inhalt des Beitrags eingefügt, aber er wird immer noch nur als Textzeichenkette gerendert. Wir müssen das Markdown-Format auf der Detailseite des Beitrags (post.html
) in tatsächliches HTML umwandeln.
1. Markdown in der Route parsen
Modifizieren Sie die Funktion get_post_by_id
in routers/posts.py
. Bevor Sie die Beitragsdaten an die Vorlage übergeben, parsen Sie deren Inhalt mit markdown2
.
# routers/posts.py # ... andere Importe import markdown2 # importiere markdown2 # ... @router.get("/posts/{post_id}", response_class=HTMLResponse) def get_post_by_id( request: Request, post_id: uuid.UUID, session: Session = Depends(get_session), user: dict | None = Depends(get_user_from_session), ): post = session.get(Post, post_id) comments = comments_service.get_comments_by_post_id(post_id, session) # Markdown-Inhalt parsen if post: post.content = markdown2.markdown(post.content) return templates.TemplateResponse( "post.html", { "request": request, "post": post, "title": post.title, "user": user, "comments": comments, }, )
2. Die Ansicht der Beitragsdetailseite aktualisieren
Schließlich modifizieren wir templates/post.html
, um sicherzustellen, dass es das geparste HTML korrekt rendert. Zuvor haben wir {{ post.content | replace(' ', '<br>') | safe }}
verwendet, um Zeilenumbrüche zu behandeln. Jetzt, da der Inhalt bereits HTML ist, müssen wir nur noch den safe
-Filter verwenden.
{# templates/post.html #} {# ... #} <article class="post-detail"> <h1>{{ post.title }}</h1> <small>{{ post.createdAt.strftime('%Y-%m-%d') }}</small> <div class="post-content">{{ post.content | safe }}</div> </article> {# ... #}
Der safe
-Filter weist Jinja2 an, dass der Inhalt dieser Variablen sicher ist und keine HTML-Entschärfung benötigt, was die korrekte Wiedergabe des Bild-<img>
-Tags und anderer Markdown-Formatierungen ermöglicht.
Ausführen und Testen
Starten Sie nun Ihre Anwendung neu:
uvicorn main:app --reload
Nach der Anmeldung gehen Sie zur Seite "Neuer Beitrag" und sehen die neue Schaltfläche "Bild hochladen". Klicken Sie darauf, um eine Datei zur, zum Hochladen auszuwählen.
Wählen Sie ein Bild aus. Nach Abschluss des Uploads wird der Markdown-Link für das Bild automatisch in die Textbox eingefügt.
Veröffentlichen Sie den Beitrag und gehen Sie zur Beitragsdetailseite. Sie sehen, dass das Bild erfolgreich gerendert wird. Und als zusätzlichen Bonus unterstützt der Beitragsinhalt jetzt die Markdown-Syntax!
Herzlichen Glückwunsch, Ihr Blog unterstützt jetzt Bild-Uploads (und Markdown)! Von nun an wird Ihr Blog sicherlich viel spannender sein.