
Bun.js + S3 + Sharp: eine Pipeline für Upload und Bildoptimierung
So entsteht eine sichere Bild-Pipeline mit Bun.js, presigned S3-URLs, Sharp, WebP-Varianten und serverseitiger Validierung.
Bun validiert die angeforderten Dateidaten und liefert eine kurzlebige presigned PUT-URL. Der Browser lädt direkt in S3-kompatiblen Storage. Ein Worker liest das Original, erstellt mit Sharp WebP- und Thumbnail-Varianten und veröffentlicht nur geprüfte Objekte.
Architektur ohne Proxy für Dateibytes
Bun ist die Control Plane, der Object Storage die Data Plane.
01
Upload vorbereiten
Der Client sendet Dateiname, MIME-Type und Größe. Die API prüft die Allowlist und liefert Key und presigned URL.
02
Direkt hochladen
Der Browser führt PUT nach S3 aus. Der Bun-Prozess hält die Datei nicht im Arbeitsspeicher.
03
Verarbeiten
Ein Worker liest das Original und Sharp erstellt Thumbnail und WebP-Vorschau.
04
Veröffentlichen
In der Datenbank werden nur geprüfte Keys, Dimensionen, MIME-Type und Status gespeichert.
Die Datei geht direkt in den Storage; Bun verwaltet Zugriff und Metadaten statt große Bytes zu proxyen.
Screenshot des Abschnitts architectureSchritt 1: SDK und Sharp installieren
Abhängigkeiten hinzufügen
bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner sharpSharp enthält native Komponenten. Prüfe die optionalen Abhängigkeiten für die Docker-Zielplattform.
Bucket setzen
S3_REGION=eu-central-1
S3_BUCKET=media
S3_ENDPOINT=https://s3.example.com
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...S3_ENDPOINT funktioniert auch mit R2 und MinIO. Der Secret Key darf nie ins Frontend.
Schritt 2: presigned PUT-URL ausstellen
Halte die URL kurzlebig, verwende einen zufälligen Object Key und binde sie an den erwarteten Content-Type. Prüfe die tatsächliche Objektgröße nach dem Upload.
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: Bun.env.S3_REGION!, endpoint: Bun.env.S3_ENDPOINT || undefined, forcePathStyle: Boolean(Bun.env.S3_ENDPOINT) });
const allowed = new Set(["image/jpeg", "image/png", "image/webp"]);
export async function createUploadUrl(contentType: string) {
if (!allowed.has(contentType)) throw new Error("Unsupported image type");
const key = `originals/${crypto.randomUUID()}`;
const command = new PutObjectCommand({ Bucket: Bun.env.S3_BUCKET!, Key: key, ContentType: contentType, ServerSideEncryption: "AES256" });
return { key, url: await getSignedUrl(s3, command, { expiresIn: 300 }) };
}Upload vorbereiten und Status verfolgen
Setze eine Datei nicht schon bei der URL-Ausgabe auf ready. Lege zunächst einen Datensatz mit pending an und ändere ihn erst nach der Objektprüfung durch den Worker auf ready.
import { Elysia, t } from "elysia";
import { createUploadUrl } from "../storage";
export const uploadRoutes = new Elysia({ prefix: "/uploads" }).post("/prepare", async ({ body, set }) => {
const upload = await createUploadUrl(body.contentType);
// upload.key und userId mit status=pending speichern.
set.status = 201;
return { ...upload, status: "pending" };
}, { body: t.Object({ contentType: t.Union([t.Literal("image/jpeg"), t.Literal("image/png"), t.Literal("image/webp")]) }) });Sicherheit und Kostenkontrolle
Upload-Endpunkte brauchen Limits auf jeder Stufe.
Größe begrenzen
Payloads begrenzen und die tatsächliche Objektgröße nach PUT prüfen.
Keys serverseitig erzeugen
UUIDs und User-/Projekt-Namespace statt beliebiger Client-Keys verwenden.
Scannen oder moderieren
Bei öffentlichen Inhalten Antivirus oder Moderation vor ready einbauen.
Lifecycle-Regeln verwenden
Abgebrochene Uploads und temporäre Originals automatisch löschen.
Schwere Arbeit auslagern
Sharp in einem Worker ausführen, damit die Upload-API responsiv bleibt.
Die Bild-Pipeline testen
Teste das dekodierte Ergebnis, nicht nur den HTTP-Status. So werden Qualitätsregressionen und kaputte native Dependencies nach einem Docker-Update sichtbar.
import { expect, test } from "bun:test";
import sharp from "sharp";
import { makeVariants } from "./images";
test("erstellt ein begrenztes WebP-Thumbnail", async () => {
const input = await sharp({ create: { width: 1200, height: 800, channels: 3, background: "#f97316" } }).png().toBuffer();
const { thumbnail } = await makeVariants(input);
const meta = await sharp(thumbnail).metadata();
expect(meta.format).toBe("webp"); expect(meta.width).toBe(480); expect(meta.height).toBe(480);
});Starte bun test --coverage; verwende MinIO oder einen separaten Test-Bucket für Storage-Integrationstests.
FAQ
Ja, aber Sharp hat native Dependencies. Installiere sie für die Zielplattform und führe einen Smoke-Test im Production-Docker-Image aus.
Bei großen Dateien besser nicht. Verwende eine presigned URL und lade direkt in S3-kompatiblen Storage.
Fazit
Beschreiben Sie die Aufgabe — die ersten 15 Minuten der Beratung sind kostenlos.
Verwandte Artikel

Mau Saver zu einer Telegram-Gruppe hinzufügen und Medien im Chat speichern
Praktische Anleitung: Mau Saver zu einer Telegram-Gruppe hinzufügen und Videos, Audio und Posts direkt im Chat herunterladen.

Internationale Telegram-Media-Zielgruppen mit Werbung erreichen
Praktischer Leitfaden für Werbetreibende: Mau Saver, Telegram-Gruppen, angeheftete Beiträge und native Medienintegrationen.

AI Assistant Entwicklung Kosten 2026: RAG, Knowledge Base, Integrationen und Support
Praktischer Leitfaden zu Kosten fuer AI Assistants: RAG, Knowledge Base, Channels, Tool Use, Guardrails, Evaluations, Monitoring und Support.

KI kann mehr erzeugen. Nicht besser: Was die Forschung über Spieleentwicklung sagt
Generative KI kommt in die Spieleproduktion, doch die Fakten sind nuancierter als der Hype. Wir betrachten Adoption, Spielervertrauen, Qualitätsrisiken und einen sinnvollen Workflow.