Erstelle dein eigenes Forum mit FastAPI: Schritt 9 – Bilder hochladen
Lukas Schneider
DevOps Engineer · Leapcell

Im vorherigen Artikel haben wir die integrierte Volltextsuchfunktion von PostgreSQL verwendet, um die Suche nach Beiträgen in unserem Forum zu ermöglichen.
Als Nächstes werden wir die Funktionalität unserer Website weiter verbessern, indem wir die Unterstützung für das Hochladen von Bildern in Beiträge hinzufügen.
Der Prozess für das Hochladen von Bildern ist wie folgt:
- Der Benutzer wählt eine Bilddatei aus und sendet sie ab.
- Nachdem FastAPI die Daten empfangen hat, leitet es die Bilddatei an einen dedizierten Objektspeicherdienst wie S3 weiter.
Warum nicht direkt auf dem Server speichern?
Dies ist zwar praktisch, birgt jedoch mehrere Probleme:
- Sie müssen persistenten Speicher für die Bilder pflegen. Das bedeutet, dass Sie sicherstellen müssen, dass Bilddateien nicht jedes Mal verloren gehen, wenn Sie die Anwendung bereitstellen.
- Das Ausliefern von Bildern vom Server verbraucht die teuren Bandbreiten- und Rechenressourcen des Servers. Die Verwaltung dies mit einem Objektspeicherdienst verbraucht keine Serverrechenressourcen, und die Bandbreite ist viel günstiger.
Deshalb müssen wir einen externen Objektspeicherdienst wie S3 verwenden.
Schritt 1: Einen S3-Speicher-Bucket vorbereiten
Sie benötigen einen S3-kompatiblen Speicherdienst. Sie können wählen:
- Leapcell. Leapcell ist eine Plattform, die All-in-One-Backend-Dienste anbietet, mit denen Sie Websites erstellen, Datenbanken bereitstellen können, und die auch S3-kompatiblen Objektspeicher enthält.
- Amazon S3 (offizieller AWS-Dienst)
- Andere Cloud-Anbieter, solange sie als S3-kompatibel gekennzeichnet sind.
Als Nächstes verwenden wir Leapcell als Beispiel.
Melden Sie sich bei der Leapcell-Hauptoberfläche an und klicken Sie auf „Object Storage erstellen“.

Geben Sie einen Namen ein, um den Objektspeicher zu erstellen.

Auf der Detailseite des Objektspeichers sehen Sie den Endpunkt, die Access Key ID und den Secret Access Key, welche die Parameter für die Verbindung sind. Wir werden sie später in unserer Backend-Konfiguration verwenden.

Die Benutzeroberfläche bietet auch eine sehr praktische Benutzeroberfläche, mit der Sie Dateien direkt im Browser hochladen und verwalten können.

Schritt 2: Abhängigkeiten installieren
Wir werden boto3 verwenden, das das offizielle Python SDK von AWS ist und auch vollständig mit allen S3-Protokolldiensten kompatibel ist.
pip install boto3
Schritt 3: Konfiguration hinzufügen
Erstellen Sie eine config.py-Datei im Stammverzeichnis des Projekts, um alle Konfigurationen des Objektspeichers zentral zu verwalten:
config.py
# S3 Speicher Konfiguration # Bitte ersetzen Sie dies durch Ihre eigenen S3-Informationen S3_ENDPOINT_URL = "https.objstorage.leapcell.io" S3_ACCESS_KEY = "YOUR_ACCESS_KEY" S3_SECRET_KEY = "YOUR_SECRET_KEY" S3_BUCKET_NAME = "your-bucket-name" S3_PUBLIC_URL = "https://your-bucket-name.leapcellobj.com"
Schritt 4: S3-Upload-Dienstprogramm erstellen
Um die Logik übersichtlicher zu gestalten, erstellen wir eine neue Datei s3_utils.py, die speziell für die Verarbeitung von Datei-Uploads zuständig ist.
s3_utils.py
import boto3 import uuid import config # Initialisiere den S3-Client s3_client = boto3.client( 's3', endpoint_url=config.S3_ENDPOINT_URL, aws_access_key_id=config.S3_ACCESS_KEY, aws_secret_access_key=config.S3_SECRET_KEY ) def upload_file_to_s3(file_bytes: bytes, bucket_name: str, content_type: str, extension: str) -> str: """ Lädt einen binären Dateistream nach S3 hoch :param file_bytes: Der binäre Inhalt der Datei :param bucket_name: Der Bucket-Name :param content_type: Der MimeType der Datei :param extension: Die Dateierweiterung :return: Die öffentliche URL der Datei """ # Generiere einen eindeutigen Dateinamen file_name = f"uploads/{uuid.uuid4()}.{extension}" try: s3_client.put_object( Body=file_bytes, Bucket=bucket_name, Key=file_name, ContentType=content_type, ) # Konstruiere und gib die öffentliche URL zurück public_url = f"{config.S3_PUBLIC_URL}/{file_name}" return public_url except Exception as e: print(f"S3-Upload fehlgeschlagen: {e}") return None
Schritt 5: Datenbankmodell aktualisieren
Wir müssen der Tabelle posts ein Feld hinzufügen, um die URL des hochgeladenen Bildes zu speichern.
Öffnen Sie models.py und ändern Sie das Post-Modell:
models.py (Aktualisiertes Post-Modell)
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean # ... (Weitere Importe) class Post(Base): __tablename__ = "posts" id = Column(Integer, primary_key=True, index=True) title = Column(String, index=True) content = Column(String) owner_id = Column(Integer, ForeignKey("users.id")) # --- Neues Feld --- image_url = Column(String, nullable=True) # --------------- owner = relationship("User", back_populates="posts") comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan") search_vector = Column(TSVECTOR, nullable=True)
Wir müssen auch unsere Datenbanktabellenstruktur aktualisieren. Die entsprechende SQL-Anweisung lautet:
ALTER TABLE posts ADD COLUMN image_url VARCHAR(512);
Wenn Ihre Datenbank mit Leapcell erstellt wurde,
können Sie diese SQL-Anweisungen direkt im webbasierten Bedienfeld ausführen.

Schritt 6: Die Backend-Route refaktorieren
Als Nächstes ändern wir die Route create_post in main.py, um Datei-Uploads zu akzeptieren.
main.py (Aktualisierte create_post-Route)
# ... (Vorherige Importe) ... from fastapi import File, UploadFile import s3_utils # Importiere das gerade erstellte Dienstprogramm import config # Importiere die Konfiguration import asyncio # Importiere asyncio # ... (App, Templates, Abhängigkeiten usw. bleiben unverändert) @app.post("/api/posts") async def create_post( title: str = Form(...), content: str = Form(...), image: Optional[UploadFile] = File(None), # 1. Akzeptiere die Bilddatei db: AsyncSession = Depends(get_db), current_user: Optional[models.User] = Depends(get_current_user) ): if not current_user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) if current_user.is_banned: raise HTTPException(status_code=403, detail="Sie sind gesperrt und können keine Beiträge erstellen.") image_url = None # 2. Verarbeite den Bild-Upload if image and image.filename: # Überprüfe den Dateityp if image.content_type not in ["image/jpeg", "image/png", "image/gif"]: raise HTTPException(status_code=400, detail="Ungültiger Dateityp. Nur JPEG, PNG, GIF sind erlaubt.") # Lese den Dateiinhalt file_bytes = await image.read() # Hole die Dateierweiterung extension = image.filename.split('.')[-1] # 3. Verwende asyncio.to_thread, um den blockierenden S3-Upload in einem Hintergrundthread auszuführen # Boto3 (s3_client.put_object) ist eine blockierende I/O-Operation # in image_url = await asyncio.to_thread( s3_utils.upload_file_to_s3, file_bytes, config.S3_BUCKET_NAME, image.content_type, extension ) if not image_url: raise HTTPException(status_code=500, detail="Bild-Upload fehlgeschlagen.") # 4. Erstelle das Post-Objekt, einschließlich der image_url new_post = models.Post( title=title, content=content, owner_id=current_user.id, image_url=image_url # Speichere die URL ) db.add(new_post) await db.commit() await db.refresh(new_post) return RedirectResponse(url="/posts", status_code=status.HTTP_303_SEE_OTHER) # ... (Alle anderen Routen bleiben unverändert) ...
Die wichtigsten Änderungen sind:
- Die Parameter von
create_postenthalten jetztimage: Optional[UploadFile] = File(None). - Lese den Inhalt des
image-Parameters (await image.read()) und rufes3_utils.upload_file_to_s3auf, um das Bild in den Objektspeicher hochzuladen. - Speichere schließlich bei der Erstellung des
models.Post-Objekts die von einem der vorherigen Schritte zurückgegebeneimage_urlin der Datenbank.
Schritt 7: Frontend-Vorlagen aktualisieren
Zuletzt müssen wir unsere Frontend-Seiten aktualisieren, um das Feld für den Datei-Upload hinzuzufügen und die Bilder anzuzeigen.
templates/posts.html (Aktualisiert)
... (Kopf und Stil bleiben unverändert) ... <body> <header> ... (Header-Inhalt bleibt unverändert) ... </header> <form action="/search" method="GET" style="display: flex;"> ... (Suchformular bleibt unverändert) ... </form> {% if current_user and not current_user.is_banned %} <h2>Neuen Beitrag erstellen</h2> <form action="/api/posts" method="post" enctype="multipart/form-data"> <input type="text" name="title" placeholder="Beitragstitel" required /><br /> <textarea name="content" rows="4" placeholder="Beitragsinhalt" required></textarea><br /> <label for="image">Bild hochladen (Optional, JPE/PNG/GIF):</label> <input type="file" name="image" id="image" accept="image/jpeg,image/png,image/gif" /> <br /><br /> <button type="submit">Posten</button> </form> {% elif current_user and current_user.is_banned %} ... (Nachricht für gesperrte Benutzer) ... {% else %} ... (Aufforderung zum Anmelden) ... {% endif %} <hr /> <h2>Beitragsliste</h2> {% for post in posts %} <div class="post-item"> <a href="/posts/{{ post.id }}"><h3>{{ post.title }}</h3></a> {% if post.image_url %} <img src="{{ post.image_url }}" alt="{{ post.title }}" style="max-width: 400px; height: auto; margin-bottom: 10px;" /> {% endif %} <p>{{ post.content }}</p> <small>Autor: {{ post.owner.username if post.owner else 'Unbekannt' }}</small> {% if current_user and post.owner_id == current_user.id %} ... (Bearbeitungslink) ... {% endif %} </div> {% endfor %} </body> </html>
templates/post_detail.html (Aktualisiert)
... (Kopf und Stil bleiben unverändert) ... <body> <div class="post-container"> <h1>{{ post.title }}</h1> {% if post.image_url %} <img src="{{ post.image_url }}" alt="{{ post.title }}" style="max-width: 600px; height: auto; margin-bottom: 10px;" /> {% endif %} <p>{{ post.content }}</p> <small>Autor: {{ post.owner.username }}</small> </div> ... (Kommentarformular und Kommentarbereich bleiben unverändert) ... </body> </html>
Ausführen und Überprüfen
Starten Sie Ihren Uvicorn-Server neu:
uvicorn main:app --reload
Besuchen Sie http://127.0.0.1:8000.
Sie sehen einen Dateiauswahlknopf im Formular „Neuen Beitrag erstellen“.

Versuchen Sie, einen neuen Beitrag zu veröffentlichen und ein Bild anzuhängen. Nach dem Absenden sehen Sie das von Ihnen hochgeladene Bild im Beitrag.

Fazit
Mit Objektspeicher haben Sie erfolgreich die Bild-Upload-Funktionalität zu Ihrem Forum hinzugefügt.
Derzeit sind alle Beiträge auf derselben Seite untergebracht. Dies wird mit wachsendem Inhalt des Forums sehr chaotisch werden.
Im nächsten Artikel fügen wir Kategorien (Unterforen) zum Forum hinzu, die es ermöglichen, Beiträge nach verschiedenen Themen zu organisieren und das Forum strukturierter zu gestalten.
