Dateispeicher SDK
platform.files ist das virtuelle Dateisystem der Plattform: der zentrale Dateispeicher, den Nutzer:innen unter /files sehen. Dein Tool schreibt seine Ergebnisse dorthin, damit sie auffindbar, teilbar und durchsuchbar sind. Du fasst nie rohes R2 an und rechnest nie selbst mit Quota.
Ausgaben sichtbar ablegen
Tool-Ergebnisse gehören unter Tools/<Anzeigename deines Tools>/. Genau ein Aufruf erledigt Byte-Write, Quota-Prüfung und den sichtbaren Node:
import { saveToolFile } from "@/lib/files/tool-files";
const ref = await saveToolFile(ctx, {
moduleId: "homestaging", // der Anzeigename kommt aus der Modul-Registry
name: "Wohnzimmer · Skandinavisch.jpg",
bytes, // Uint8Array
contentType: "image/jpeg",
subfolder: "Entwürfe", // optional, max. eine Ebene
propertyId, // optional: zusätzlich mit einem Objekt verknüpfen
dedupeKey: `gen-${runId}`, // optional, aber empfohlen (siehe unten)
});
// ref: { nodeId, key, name, contentType, sizeBytes, url }- Quota:
saveToolFilebucht die Nutzung und erzwingt das Plan-Limit. Ist der Speicher voll, wirft esStorageQuotaExceededError. Fang das in Massen-Writern ab, stoppe sauber und benachrichtige die Inhaber:in, statt blind weiterzuladen. - Idempotenz: Gleicher
dedupeKeyliefert bei einem Retry denselben Node statt einer Dublette. Nutze eine stabile ID aus deinem Lauf. - Verboten: roher
r2PutoderstorageService().putin Modul-Code. Der Reuse-Ratchet (bun validate:reuse) bricht CI, wenn ein Modul daran vorbei schreibt.
Wenn deine Bytes aus Serving-Gründen ihren eigenen Key behalten müssen (z. B. eine öffentlich ausgelieferte Datei), registriere das bestehende Objekt als Node, ohne es zu kopieren:
import { registerToolFile } from "@/lib/files/tool-files";
await registerToolFile(ctx, { moduleId: "website", key, name, bytes, contentType, propertyId });Dateien vom Nutzer wählen lassen
Braucht dein Tool eine Datei aus dem Speicher als Eingabe, öffne den zentralen Datei-Picker statt eines eigenen Uploads:
import { useFilePicker } from "@/lib/files/picker";
const pick = useFilePicker();
const files = await pick({ accept: ["image/*"], multiple: false });
// files[0]: { id, name, contentType, sizeBytes, ... }Der Picker zeigt nur, was die fragende Person sehen darf (Ordner-ACLs und private Bereiche werden respektiert). Du bekommst Metadaten und eine Zugriffs-URL, nie rohe Keys.
Nach außen teilen
Einen zeitlich begrenzten, optional passwortgeschützten Link erzeugst du über platform.share. Der rohe Token erscheint genau einmal (gespeichert wird nur sein Hash):
const link = await platform.share.create(ctx, {
resourceType: "file_node",
resourceId: node.id,
expiresInDays: 30, // 1..90
passwordHash, // optional (scrypt), nie Klartext
allowDownload: true,
});
// link.token → nur hier verfügbar; Pfad: /share/<token>Widerrufen ist sofort wirksam (platform.share.revoke(ctx, link.id)). Die öffentliche Seite ist nicht von Suchmaschinen indexierbar und zeigt bei ungültigen Links immer dieselbe neutrale Meldung.
Regeln
- Nie roher Storage. Schreiben über
saveToolFile/registerToolFile, Quota nie selbst rechnen. dedupeKeysetzen. Retries dürfen keine Dubletten erzeugen.- Quota-Fehler behandeln.
StorageQuotaExceededErrorsauber abfangen, Owner informieren. - ACLs vertrauen. Picker und Suche filtern serverseitig; nie an ihnen vorbei auf Nodes zugreifen.
- Keine PII in Logs. Datei-Inhalte und Namen gehören nicht nach Sentry/PostHog.