Zum Inhalt springen

Speicher-Konfiguration

Rebase unterstützt drei Speicher-Backends:

  • Lokales Dateisystem — Auf der Festplatte gespeicherte Dateien (ideal für die Entwicklung)
  • S3-kompatibel — AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces
  • Google Cloud Storage / Firebase Storage — Native GCS-Unterstützung über @google-cloud/storage

Der Speicher wird im storage-Block von initializeRebaseBackend konfiguriert:

const backend = await initializeRebaseBackend({
// ...
storage: {
type: "local",
basePath: "./uploads" // Directory for file storage
}
});

Der lokale Speicher legt die Metadaten jedes Objekts direkt daneben ab, als <key>.metadata.json. Ein Schlüssel, dessen letztes Segment auf .metadata.json endet, wird daher auf jeder Route mit 400 INVALID_STORAGE_KEY abgelehnt. S3 und GCS speichern Metadaten am Objekt selbst und können beliebige Schlüssel speichern. Auf einem Dateisystem ohne Berücksichtigung der Groß-/Kleinschreibung (standardmäßig unter macOS und Windows) sind Avatar.png und avatar.png dieselbe lokale Datei, aber zwei unterschiedliche Objekte auf S3, GCS oder einem Linux-Laufwerk. Daher warnt der Server beim Systemstart, wenn der Speicherordner auf einem solchen Dateisystem liegt.

const backend = await initializeRebaseBackend({
// ...
storage: {
type: "s3",
bucket: env.S3_BUCKET!,
region: env.S3_REGION || "auto",
accessKeyId: env.S3_ACCESS_KEY_ID || "",
secretAccessKey: env.S3_SECRET_ACCESS_KEY || "",
endpoint: env.S3_ENDPOINT, // For MinIO, R2, etc.
forcePathStyle: env.S3_FORCE_PATH_STYLE // Required for MinIO
}
});
const backend = await initializeRebaseBackend({
// ...
storage: {
type: "gcs",
bucket: env.GCS_BUCKET!,
projectId: env.GCS_PROJECT_ID,
}
});

Auf der GCP (Cloud Run, GCE, GKE) werden die Standard-Anmeldedaten des Dienstkontos automatisch verwendet. Außerhalb der GCP setzen Sie die Umgebungsvariable GOOGLE_APPLICATION_CREDENTIALS auf den Pfad Ihrer Dienstkonto-Schlüsseldatei.

Sie können mehrere benannte Backends konfigurieren und verschiedene Felder an unterschiedliche Speicher weiterleiten:

storage: {
"(default)": { type: "local", basePath: "./uploads" },
"media": { type: "s3", bucket: "media-bucket", region: "us-east-1", ... }
}

Verweisen Sie anschließend in Ihren Collection-Eigenschaften auf ein bestimmtes Backend:

image: {
type: "string",
name: "Image",
storage: {
storagePath: "products",
storageSource: "media" // Routes to the "media" S3 backend
}
}
Methode Pfad Beschreibung
POST /api/storage/upload Direkter Datei-Upload, ein einziger multipart/form-data-Body: file (Pflichtfeld); key, der Objektschlüssel samt Ordnern (author_pictures/ada.jpg), sonst der Dateiname im Wurzelverzeichnis; bucket, sonst der Standard-Bucket; storageId; collection und property, die die Datei an maxSize und acceptedFiles dieser Property binden; und metadata_<name> für eigene Metadaten. Jedes andere Feld wird ignoriert: Ein Feld path gibt es nicht
POST /api/storage/upload?storageId=<key> Upload in ein bestimmtes benanntes Backend
GET /api/storage/file/* Eine Datei abrufen — alles nach /file/ ist der Objektschlüssel
GET /api/storage/file/*?storageId=<key> Eine Datei von einem bestimmten Backend abrufen
GET /api/storage/metadata/* Größe, Content-Type und benutzerdefinierte Metadaten eines Objekts ohne dessen Bytes, plus ein kurzlebiges Download-Token, falls es privat ist
POST /api/storage/metadata Dasselbe für bis zu 100 Objekte einer Quelle: Body { keys, storageId? }
DELETE /api/storage/file/* Eine Datei löschen
GET /api/storage/list Objekte unter einem Präfix auflisten (prefix, bucket, maxResults, pageToken, storageId). Ein maxResults kleiner als 1 oder ein pageToken, den die Quelle nie ausgestellt hat, ergibt 400 INVALID_LIST_OPTIONS
POST /api/storage/folder Eine leere Ordnermarkierung erstellen
GET /api/storage/sources Die Speicherquellen, die dieses Backend bedient, nach Schlüssel
OPTIONS /api/storage/tus Unterstützte TUS-Protokollfunktionen abfragen
POST /api/storage/tus Eine fortsetzbare TUS-Upload-Sitzung initiieren
HEAD /api/storage/tus/:id Upload-Fortschritt prüfen (Byte-Offset)
PATCH /api/storage/tus/:id Daten-Chunk an temporäre Datei anhängen
DELETE /api/storage/tus/:id TUS-Upload-Sitzung beenden/abbrechen

Was sie antworten. Ein einheitlicher Umschlag (Envelope), derselbe, den auch /api/data verwendet: Die Nutzlast liegt unter data, und ein Fehler hat das Format { "error": { message, code, requestId } } mit den Codes aus der Fehlerreferenz. /api/storage/file/* ist die Ausnahme, da dessen Nutzlast die Datei selbst ist — es antwortet mit den reinen Bytes sowie Content-Type, Content-Length und den Caching-Headern.

// GET /api/storage/list?prefix=products/images/
{ "data": { "items": [ { "bucket": "default", "fullPath": "products/images/a.jpg", "name": "a.jpg",
"size": 48213, "updated": "2026-10-01T12:00:00.000Z", "contentType": "image/jpeg" } ], "prefixes": [] } }

Eine Auflistung beschreibt jede Datei, die sie zurückgibt: ihre size in Bytes und updated, wann sie zuletzt geschrieben wurde (im lokalen Speicher, auf S3 und GCS), sowie ihren contentType im lokalen Speicher und auf GCS. Ein S3-Listing enthält den Typ nicht, daher fehlt er bei S3 und /api/storage/metadata/* liefert ihn stattdessen. Ein Ordner in prefixes enthält keines dieser Felder, ebenso wie ein benutzerdefinierter StorageController, der sie nicht setzt.

POST /api/storage/upload antwortet mit 201 und { key, bucket, storageUrl } des gespeicherten Objekts unter data; GET /api/storage/metadata/* mit den Metadaten des Objekts und für private Objekte mit dem kurzlebigen token; POST /api/storage/metadata mit einem Objekt, dessen Schlüssel die übergebenen Pfade sind und die jeweils das enthalten, was der GET-Endpunkt dafür geantwortet hätte (siehe unten). Ein Ordner ist kein Objekt: Ein /metadata-Aufruf auf einen Schlüssel, der auf / endet, liefert auf jedem Speicher 404, selbst wenn S3 oder GCS eine folder/-Markierung vorhalten; es wird daher kein Download-Token für ein Präfix erzeugt. GET /api/storage/sources liefert das Array der konfigurierten Quellen mit configured: true. In einem Deployment ganz ohne Speicher antwortet es mit 200 {"data": [], "configured": false}, während jede andere Route mit 501 STORAGE_NOT_CONFIGURED antwortet. GET /api/auth/config meldet dieselbe Tatsache als storage: false, sodass ein Client die Anfrage überspringen kann. GET /api/storage/sources ist öffentlich: Es antwortet ohne Token, selbst wenn requireAuth aktiv ist. Es gibt lediglich die Quellen an (Schlüssel, Engine, Transport und Label) und niemals eine Datei; das Client-SDK liest diesen Endpunkt aus, um seine Quellen zu verdrahten, was geschehen kann, bevor sich jemand angemeldet hat. Eine Anwendung, die eine direct-Quelle anonym ausliest, benötigt dies ebenfalls. Jede Route, die eine Datei auflistet, liest oder schreibt, behält ihre Authentifizierung bei. DELETE /api/storage/file/* und POST /api/storage/folder enthalten lediglich eine message, da es nichts zurückzugeben gibt.

Auf S3 und GCS muss ein bucket eines sein, das die Quelle bedient, sowohl bei Schreib- als auch bei Leseoperationen: Ein Upload, ein POST /api/storage/folder oder ein TUS-Upload mit Angabe eines anderen Buckets antwortet mit 404 UNKNOWN_STORAGE_SOURCE, genau wie das Listing. Der lokale Speicher erstellt ein Bucket weiterhin beim ersten Schreibvorgang automatisch.

Viele Objekte auf einmal. POST /api/storage/metadata akzeptiert { "keys": ["photos/a.png", "photos/b.png"], "storageId": "media" } — bis zu 100 Objektpfade einer einzelnen Quelle, jeweils so formatiert, wie GET /api/storage/metadata/<key> sie erwartet — und liefert einen Eintrag pro Schlüssel zurück:

{ "data": {
"photos/a.png": { "fullPath": "photos/a.png", "size": 48213, "contentType": "image/png", "token": "…", "tokenExpiresIn": 300 },
"photos/b.png": { "error": { "code": "FORBIDDEN", "message": "Not authorized for this object", "status": 403 } }
} }

Jeder Schlüssel wird exakt so beurteilt wie beim GET-Endpunkt — derselbe storageAuthorize-Hook, derselbe Speicher-Scope, dieselbe Public-Regel, ein eigenes Download-Token. Ein Schlüssel, der abgelehnt wird, fehlt oder fehlerhaft ist, liefert seinen eigenen error zurück, ohne die anderen abzubrechen oder preiszugeben. Ein Aufrufer ohne Sitzung in einem Deployment, das eine solche erfordert, erhält den 401, den auch der GET-Endpunkt liefern würde, es sei denn, jeder Schlüssel ist öffentlich. Mehr als 100 Schlüssel oder ein Body ohne keys führen zu 400 BAD_REQUEST. Der Rate Limiter zählt die Anfrage als ⌈N/10⌉ Anfragen für N Schlüssel; eine Seite mit 100 Thumbnails kostet also 10 Einheiten. client.storage.getSignedUrl() und das Admin-Panel bündeln die Abfragen einer Seite auf diese Weise und fallen auf einzelne GET-Aufrufe pro Datei zurück, falls der Server die Route nicht unterstützt.

Wie ein Lesezugriff auf eine Datei autorisiert wird. Die Lese-Routen — /api/storage/file/* und /api/storage/metadata/* — erwarten das kurzlebige, signierte Token, das getSignedUrl() generiert, übergeben als ?token=<token> oder als Bearer. Ein reguläres Zugriffs-JWT wird auf /file/* mit 401 Unauthorized: Access JWT not allowed on file routes abgelehnt: Das Token, das auf jeder anderen Route funktioniert, funktioniert dort bewusst nicht, da eine Datei-URL an einen Browser, ein CDN oder ein <img>-Tag weitergegeben wird. Alle anderen oben aufgeführten Routen akzeptieren wie gewohnt das Zugriffs-JWT.

Rebase enthält eine integrierte Bildverarbeitungspipeline auf Basis von Sharp. Beim Ausliefern von Bild-Assets aus dem Speicher können Sie dynamische Operationen über Abfrageparameter anwenden:

# Serve image scaled to 300px width in webp format
GET /api/storage/file/products/laptop.jpg?width=300&format=webp
  • width, height: Abmessungen für die Größenänderung, 1–4096 (das Bild wird niemals vergrößert).
  • quality: 1–100.
  • format: Konvertiert das Bildformat. Unterstützte Formate: webp, jpeg, png, avif.
  • fit: cover, contain, fill, inside oder outside.

Ein Parameter außerhalb dieser Grenzen führt zu einem 400-Fehler und wird nicht stillschweigend begrenzt — ?width=99999 lieferte früher ein Bild mit 4096px und ?format=tiff ein WebP-Bild zurück, ohne Rückmeldung darüber zu geben.

Die Thumbnails gespeicherter Bilder im Admin-Panel nutzen diesen Endpunkt: Eine Tabellen- oder Karten-Vorschau fordert eine WebP-Version an, die etwa doppelt so groß wie ihre dargestellte Größe ist (96–800px), anstatt des Originals. SVG- und GIF-Dateien werden immer unverändert angezeigt. sharp ist optional: ohne es erhält eine Transformationsanfrage das Original, und das Backend warnt einmal, pnpm add sharp in backend/ auszuführen (unter 0.24 antwortet es mit 500, und das Panel zeigt die Originale).

Das Transformieren ist CPU- und speicherintensiv, und bei einem öffentlichen Objekt ist der Endpunkt anonym erreichbar. Daher ist die Arbeitslast begrenzt und nicht nur zwischengespeichert:

  • Kapazität: Ein LRU-Cache, der global auf 500 Einträge limitiert ist, aufgeschlüsselt nach Speicherquelle, Bucket und kanonischem Schlüssel.
  • TTL (Time to Live): Zwischengespeicherte Varianten laufen nach 1 Stunde ab.
  • Gleichzeitige Anfragen für dieselbe nicht zwischengespeicherte Variante erzeugen eine Transformation, nicht jeweils eine eigene.
  • Eine kleine Anzahl von Transformationen läuft gleichzeitig; jenseits eines begrenzten Backlogs antwortet der Server mit 503 TRANSFORM_OVERLOADED, anstatt Arbeit anzunehmen, die er nicht bewältigen kann.

Dieser Cache lebt im Prozess, was bedeutet, dass er nicht zwischen Instanzen geteilt wird und einen Neustart nicht übersteht. Zwei Replikate berechnen jede Variante separat, und ein Deployment verwirft den gesamten Cache.

storageRenditionCache schreibt jedes abgeleitete Bild zurück in dasselbe Bucket wie seine Quelle, sodass die Arbeit einmal für das gesamte Deployment statt einmal pro Instanz und Release ausgeführt wird:

storageRenditionCache: { enabled: true }

oder STORAGE_RENDITION_CACHE=true für ein Bundle-Deployment. Renderings werden unter dem reservierten Präfix _rebase/renditions/ gespeichert, verschlüsselt nach der Version des Quellobjekts — das Ersetzen eines Bildes liefert also sofort das neue aus.

Drei Dinge, die Sie vor dem Aktivieren beachten sollten:

  • Ein Lesevorgang führt nun zu einem Schreibvorgang. Jede neue Variante kostet einen PUT-Aufruf in Ihrem Bucket. Aus diesem Grund ist die Funktion standardmäßig deaktiviert.
  • Ein fehlgeschlagener Schreibvorgang ist keine fehlgeschlagene Anfrage. Schreibgeschützte Anmeldedaten oder eine Bucket-Richtlinie, die das Präfix ablehnt, fallen auf den In-Process-Cache zurück; das Bild wird dennoch ausgeliefert und der Grund wird einmalig protokolliert.
  • Ersetzte Renderings werden nicht bereinigt. Das Ersetzen eines Quellobjekts lässt alte Renderings verwaist zurück. Richten Sie eine Lifecycle-Regel für _rebase/renditions/ ein — dieses Präfix ist fest vorgegeben und nicht konfigurierbar, damit eine Regel exakt darauf verweisen kann.

Das Präfix kann nicht über die API adressiert werden. Direktes Lesen oder Schreiben darauf wird mit 400 INVALID_STORAGE_KEY beantwortet: Jede Zugriffsregel im Produkt — sowohl storageAuthorize als auch die deklarativen Richtlinien — bezieht sich auf den Quell-Schlüssel, und ein Rendering, das unter seinem eigenen Pfad ausgeliefert wird, würde eine Anfrage beantworten, die niemand gestellt hat.

_rebase/maintenance/ ist auf dieselbe Weise reserviert. rebase storage orphans legt dort den Datensatz jedes Scans ab, in der Quelle, die er gescannt hat, damit der Prozess, der den Scan ausführt, und der, der seinen Status beantwortet, denselben Datensatz lesen. Ein Aufrufer, der einen Datensatz schreiben könnte, könnte bestimmen, was ein späteres Löschen entfernt, daher beantwortet auch das Lesen, Schreiben oder Auflisten des Präfixes 400. Datensätze werden 7 Tage nach ihrem letzten Schreibvorgang gelöscht, wenn der nächste Scan dieser Quelle beginnt.

Der gespeicherte Content-Type entspricht dem, was der Uploader angegeben hat — die Bytes werden nicht analysiert. Daher rendert /api/storage/file/* inline nur eine enge Allowlist: Bilder, Video, Audio, application/pdf und text/plain. Alles andere, einschließlich text/html, wird als application/octet-stream mit Content-Disposition: attachment ausgeliefert, und jede Antwort enthält X-Content-Type-Options: nosniff. Der Speicher ist kein Webserver: Eine hochgeladene HTML-Seite, die auf dem API-Origin gerendert wird, könnte Cookies dieses Origins auslesen und dessen Endpunkte aufrufen.

Ein SVG ist ein Bild, das gleichzeitig ein Dokument ist und <script> enthalten kann. Es wird als image/svg+xml ausgeliefert — sodass es in einem <img> und in den Thumbnails des Admin-Panels gerendert wird —, jedoch mit Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox bei jeder Antwort, einschließlich transformierter URLs und URLs mit Download-Token. Wird es direkt geöffnet, rendert es mit deaktivierten Skripten und einem opaken Origin, sodass es weder auf die Cookies noch auf die Endpunkte der API zugreifen kann.

Jede Speicherquelle akzeptiert Dateien bis zu ihrem eigenen Limit: 50 MB, sofern nicht STORAGE_MAX_FILE_SIZE (in Bytes, mit dem Suffix __<KEY> für eine benannte Quelle) oder das maxFileSize-Feld der Quelle etwas anderes festlegt. Jeder Zugangsweg bindet eine Datei an denselben Wert. POST /api/storage/upload antwortet bei Überschreitung mit 413 PAYLOAD_TOO_LARGE; ein fortsetzbarer Upload wird bereits bei der Erstellung abgelehnt, noch vor seinem ersten Chunk; und OPTIONS /api/storage/tus meldet ihn als Tus-Max-Size — für die durch ?storageId= benannte Quelle bzw. andernfalls für die Standardquelle.

In Version 0.23 legt ausschließlich das Feld maxFileSize der Quelle das Limit fest; STORAGE_MAX_FILE_SIZE wird nicht ausgewertet.

STORAGE_MAX_FILE_SIZE=209715200 # (default): 200 MB
STORAGE_MAX_FILE_SIZE__MEDIA=1073741824 # media: 1 GB

Wie viel einer Datei der Server gleichzeitig im Arbeitsspeicher hält, hängt vom Eingangsweg ab. POST /api/storage/upload ist ein einzelner Multipart-Body, den der Server vollständig parst: das Drei- bis Fünffache der Dateigröße im Arbeitsspeicher. Ein fortsetzbarer Upload (TUS) verarbeitet jeweils nur einen Chunk. client.storage.putObject() und das Admin-Panel senden Dateien über 8 MiB über das fortsetzbare Verfahren. Unter Fortsetzbare Uploads (TUS) finden Sie Informationen zum Protokoll, zu den Antworten und zum Verhalten über mehrere Instanzen hinweg.

Variable Beschreibung
STORAGE_TYPE "local", "s3" oder "gcs"
STORAGE_PATH Verzeichnis für lokalen Speicher (Standard: <project>/uploads, unter rebase dev und in einem von rebase eject geschriebenen Server gleichermaßen). Ein relativer Pfad wird von dem Verzeichnis aus gelesen, in dem der Server läuft; nach rebase eject ist das backend/. Ein benanntes lokales Bucket speichert seine Dateien daneben in <STORAGE_PATH>__<key>, es sei denn, STORAGE_PATH__<KEY> definiert einen eigenen Pfad
S3_BUCKET Name des S3-Buckets
S3_REGION AWS-Region (Standard: "auto")
S3_ACCESS_KEY_ID AWS-Access-Key
S3_SECRET_ACCESS_KEY AWS-Secret-Key
S3_ENDPOINT Benutzerdefinierter S3-Endpunkt (für MinIO, R2)
S3_FORCE_PATH_STYLE Path-Style-URLs erzwingen (erforderlich für MinIO)
GCS_BUCKET Name des Google Cloud Storage Buckets
GCS_PROJECT_ID GCP-Projekt-ID für GCS
GCS_KEY_FILENAME Pfad zu einer GCP-Dienstkonto-Schlüsseldatei (auf GKE weglassen — Workload Identity/ADC stellt Zugangsdaten bereit)
GOOGLE_APPLICATION_CREDENTIALS Standard-ADC-Variable, die vom Google-SDK selbst gelesen wird (auf GCP mit Standardanmeldedaten nicht erforderlich)
STORAGE_DOWNLOAD_TOKEN_TTL Wie lange die Download-URL einer privaten Datei in Sekunden gültig ist — die Lebensdauer des Tokens, das /api/storage/metadata/* generiert (Standard 300, maximal 604800, d. h. eine Woche). Erhöhen Sie diesen Wert für private Videos und Audios, die nach dem Rendern der Seite weiterhin Byte-Ranges anfragen. Entspricht der Option storageDownloadTokenTtl
STORAGE_MAX_FILE_SIZE Größte Datei, die die Quelle akzeptiert, in Bytes (Standard 52428800, 50 MB). Suffix __<KEY> für eine benannte Quelle. Ein Wert, der keine ganze Byte-Zahl ist, verhindert den Systemstart. Siehe Maximale Dateigröße
FORCE_LOCAL_STORAGE Erlaubt STORAGE_TYPE=local in Produktionsumgebungen — siehe unten
STORAGE_PUBLIC_READ Gespeicherte Objekte für unauthentifizierte Leser bereitstellen. Entspricht storagePublicRead und ist eine der drei Möglichkeiten, den Boot-Guard für die Produktion zu erfüllen.
STORAGE_ALLOW_ANY_AUTHENTICATED Deaktiviert den Boot-Guard und stellt das Verhalten wieder her, bei dem jeder angemeldete Benutzer jeden Schlüssel lesen, überschreiben, löschen oder auflisten darf. Entspricht storageInsecureAllowAnyAuthenticated. Nur vertretbar, wenn jedem angemeldeten Benutzer jede Datei anvertraut werden kann.

Ein Projekt kann mehr als ein Bucket haben. Deklarieren Sie jedes davon in config/resources.ts — dem zentralen Ort, den die Plattform, die Runtime und die Konsole gemeinsam lesen:

import { bucket } from "@rebasepro/types";
export const uploads = bucket({ engine: "s3" }); // the default one
export const media = bucket("media", { engine: "s3", label: "Media" });

Führen Sie anschließend rebase resources --write aus. Dadurch wird rebase.resources.json neu generiert, sodass ein Host Ihre Topologie ohne Build-Vorgang auslesen kann. Siehe Mehrere Quellen für Datenbanken, Buckets und Topics im Zusammenspiel.

Jede Quelle wird über dieselben Variablennamen mit ihrem eigenen Suffix konfiguriert. Die Standardquelle verwendet kein Suffix, sodass ein Projekt mit nur einem Bucket weiterhin die einfachen Namen von oben nutzen kann und gar nichts deklarieren muss, und benannte Buckets daneben zu deklarieren ändert sie nicht (Details):

S3_BUCKET=app-uploads # (default)
S3_BUCKET__MEDIA=app-media # media
S3_ACCESS_KEY_ID__MEDIA=…
S3_SECRET_ACCESS_KEY__MEDIA=…

Das Suffix wird aus dem Schlüssel abgeleitet: Großbuchstaben, nicht-alphanumerische Zeichen durch Unterstriche ersetzt, hinter einem doppelten Unterstrich (media-cdn → __MEDIA_CDN). Ein einfacher Unterstrich würde mit echten Variablennamen kollidieren — S3_BUCKET_NAME würde sonst fälschlicherweise als Bucket name interpretiert werden.

Leiten Sie eine Eigenschaft über storageSource an eine Quelle weiter:

{
name: "Cover",
type: "string",
storage: { storagePath: "covers", storageSource: "media", acceptedFiles: ["image/*"] }
}

Eine Quelle, die Sie deklarieren, aber nie konfigurieren, wird übersprungen und führt nicht zu einem Abbruch: Uploads, die an sie weitergeleitet werden, antworten mit 501 STORAGE_SOURCE_NOT_CONFIGURED. Das Deklarieren eines Buckets erfolgt meist, bevor jemand Speicher dafür anhängt; ein Startfehler an dieser Stelle würde das Backend in einer Crash-Schleife halten. Eine Quelle, die die Umgebung falsch konfiguriert — ein Typ ohne Bucket oder ein Bucket ohne Anmeldedaten — führt hingegen zum Abbruch beim Start, da es sich hierbei um einen Konfigurationsfehler und nicht um ein bewusstes Fehlen handelt.

Anmeldedaten beschreiben üblicherweise den Provider, nicht das Bucket. Fünfzehn Buckets auf einer MinIO-Installation würden andernfalls fünfzehn Kopien desselben Zugriffsschlüssels bedeuten, und eine Rotation erforderte fünfzehn synchrone Änderungen. Benennen Sie stattdessen ein Konto:

export const media = bucket("media", { engine: "s3", account: "minio" });
export const avatars = bucket("avatars", { engine: "s3", account: "minio" });
S3_BUCKET__MEDIA=b-media # per bucket, always
S3_BUCKET__AVATARS=b-avatars
S3_ACCESS_KEY_ID__MINIO=… # shared by both
S3_SECRET_ACCESS_KEY__MINIO=…
S3_ENDPOINT__MINIO=https://minio.internal

Nur Variablen mit Konto-Geltungsbereich fallen auf den Fallback zurück — S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, S3_REGION, S3_FORCE_PATH_STYLE sowie das GCS-Paar GCS_PROJECT_ID / GCS_KEY_FILENAME. Der Bucket-Name tut dies nie: Er unterscheidet eine Quelle von der anderen. Ein Wert auf Bucket-Ebene hat immer Vorrang, sodass eine Quelle den Provider wechseln kann, ohne die anderen zu beeinträchtigen.

Das Frontend erfährt die Quellen des Servers über GET /api/storage/sources, eine server-Quelle braucht also keinen Eintrag. Übergeben Sie storageSources an den <Rebase>-Provider für eine direct-Quelle, die direkt zu ihrem Provider hochlädt, oder um ein Label oder den Transport selbst festzulegen:

import { Rebase } from "@rebasepro/app";
<Rebase
apiUrl="https://api.example.com"
storageSources={[
// `engine` names the provider, `transport` says who talks to it:
// "server" proxies through the Rebase backend, "direct" goes
// client-to-provider (and needs a `source` implementation).
{ key: "media", engine: "s3", transport: "server", label: "Media CDN" },
{ key: "firebase", engine: "firebase", transport: "server", label: "Firebase Storage" },
]}
>
{() => <MyApp />}
</Rebase>

Der key einer server-Quelle muss mit einem Backend-Schlüssel übereinstimmen, der in der storage-Map des Servers registriert ist. Der React-Kontext StorageSourcesContext löst die aktive Quelle für jedes Upload-Feld auf.

Jedes Objekt wird über den Server weitergeleitet — mit einem ETag für kostengünstige Neuvalidierung, einem Cache-Control, das davon abhängt, wer die Datei lesen darf, und Byte-Ranges für das Vor- und Zurückspulen in Audio- und Videodateien. Unter Speicher-Caching und CDNs finden Sie die Header und Hinweise zur Konfiguration eines vorgelagerten CDNs.

  • Binden Sie ein persistentes Volume über das übergeordnete Verzeichnis von STORAGE_PATH ein, wenn Sie lokalen Speicher unter Docker/Kubernetes verwenden, und setzen Sie FORCE_LOCAL_STORAGE=true — siehe Self-Hosting
  • Verwenden Sie S3 oder kompatible Dienste (R2, MinIO) bzw. GCS für Produktions-Deployments
  • Schalten Sie aus Performancegründen ein CDN (CloudFront, Cloudflare) vor — siehe Ein CDN vorschalten
  • Jede Anwendung mit Speicher in der Produktion muss ein Zugriffsmodell deklarieren — siehe Speicher-Autorisierung. Nicht nur mandantenfähige Anwendungen: Der Server verweigert den Start ohne ein solches Modell.

Wer jeden Schlüssel lesen, schreiben, löschen oder auflisten darf, entscheiden storagePolicies und der Hook storageAuthorize, und ein API-Schlüssel braucht zusätzlich den Speicher-Scope für die Operation. Die Produktion verweigert den Start mit Speicher und ohne Zugriffsmodell. Siehe Speicher-Autorisierung.

Auf jeden anderen Schreibvorgang in Rebase kann reagiert werden — eine Datenzeile bietet beforeSave und afterSave, ein Zeitplan einen Cron-Job — für Uploads gab es bisher nichts. Alles, was ein Upload nach sich zog, musste vom Client in einem zweiten Aufruf erledigt werden. Ging die Verbindung des Clients dazwischen verloren, unterblieb die Aktion vollständig.

storageTriggers: [
{
path: "uploads/:uid/**",
events: ["finalize"],
handler: async ({ key, params, size, user }) => {
await indexUpload.enqueue({ key, uid: params.uid, size }); // a `queue()` in config/resources.ts
}
}
]

Die Mustersyntax entspricht der von storagePolicies — literale Segmente, * für ein einzelnes Segment, :name zur Erfassung eines Segments, ** für den Rest. Ein fehlerhaftes Muster verhindert den Systemstart, anstatt stillschweigend auf nichts zuzutreffen.

Ereignis Wann
finalize Nachdem das Objekt dauerhaft geschrieben wurde; niemals bei einem fehlgeschlagenen Schreibvorgang
delete Nachdem das Objekt gelöscht wurde

finalize wird sowohl für Multipart- als auch für fortsetzbare (TUS) Upload-Pfade ausgelöst — einmal pro Upload, nicht einmal pro Chunk. Ein fortsetzbarer Upload meldet den Benutzer, der ihn erstellt hat, da dies der Principal ist, den die Autorisierung geprüft hat.

Was ein Handler nicht annehmen darf:

  • Ein Handler, der einen Fehler wirft, lässt die Anfrage nicht fehlschlagen. Das Objekt ist zu dem Zeitpunkt, an dem der Handler läuft, bereits gespeichert. Eine Fehlermeldung an den Client würde fälschlicherweise signalisieren, dass der Upload gescheitert sei, woraufhin Clients Uploads wiederholen. Fehler werden protokolliert und die Antwort bleibt unberührt. Wenn die Aufgabe zwingend ausgeführt werden muss, reihen Sie einen Job in eine Warteschlange (Queue) ein.
  • Handler werden mit await blockierend ausgeführt, in der Reihenfolge ihrer Deklaration, bevor die Antwort gesendet wird — Fire-and-Forget würde ein Promise hinterlassen, das eine Serverless-Laufzeitumgebung mitten in der Ausführung einfrieren könnte. Ein langsamer Handler führt daher zu einem langsamen Upload; dies ist ein weiterer Grund, Arbeiten in eine Queue auszulagern.
  • Interne Schreibvorgänge lösen keine Trigger aus. Der Cache für Bildtransformationen schreibt abgeleitete Objekte direkt an den Speicher-Controller; ein **-Trigger, der darauf anspringen würde, würde auf seine eigene Ausgabe reagieren.