# Files SDK > A unified storage SDK for object and blob backends. One small, honest API. Web-standards I/O. An escape hatch when you need the native client. # files-sdk@1.0.0 Source: https://files-sdk.dev/changelog/files-sdk-1-0-0 ### Major Changes - 30900e6: Initial release --- # files-sdk@1.1.0 Source: https://files-sdk.dev/changelog/files-sdk-1-1-0 ### Minor Changes - 510cde5: Add Akamai Cloud Object Storage adapter (`files-sdk/akamai`), formerly Linode Object Storage. Thin wrapper over the S3 adapter with Akamai defaults: endpoint derived from the `region` cluster code (`us-iad-1`, `nl-ams-1`, `fr-par-1`, the older `us-east-1`/`eu-central-1`/`ap-south-1` clusters, etc.) as `https://.linodeobjects.com` and overridable, virtual-hosted-style addressing, `"Akamai error"` provider label, and `AKAMAI_ACCESS_KEY_ID` / `AKAMAI_SECRET_ACCESS_KEY` env-var fallbacks. `publicBaseUrl` accepts a public-bucket origin (`https://..linodeobjects.com`) or a custom CNAME for unsigned URLs; otherwise `url()` returns a presigned GetObject (1-hour default). - f40e0d3: Add Box adapter (`files-sdk/box`) for personal Box and Box Enterprise via the official `box-typescript-sdk-gen` SDK. Box files live by ID rather than by path, so the adapter walks `rootFolderId` and translates virtual keys (`docs/a.txt`) into nested Box subfolders, auto-creating intermediate folders on `upload()` and racing-recovering on `item_name_in_use`. Five auth shapes (pre-built `client`, `developerToken`, `oauth` with refresh-token seeding, `ccg` with `enterpriseId` or `userId`, and `jwt` with `configJsonString` or `configFilePath`) cover scripts, user apps, and enterprise installs; env-var fallback via `BOX_DEVELOPER_TOKEN`. Token lifecycle is handled by the SDK's built-in `Authentication` classes — no manual refresh bookkeeping. Direct `upload()` uses single-call `uploads.uploadFile` up to 50 MB and switches to `chunkedUploads.uploadBigFile` automatically; existing leaf names route through `uploadFileVersion` (overwrite). `url()` mints a signed download URL via `getDownloadFileUrl` by default; with `publicByDefault: true`, `upload()` also calls `addShareLinkToFile` (open access) and `url()` returns the link's `download_url`; `responseContentDisposition` always throws (no override on Box URLs). `signedUploadUrl()` throws — Box uploads require a multipart POST with both an `attributes` JSON part and the file bytes part, which fits neither the SDK's PUT-with-headers nor POST-with-form-fields shape; use `upload()` server-side or Box's UI Elements / Content Uploader for browser flows. `list()` returns immediate-children files only at `rootFolderId` (no recursion, subfolders filtered out, prefix matched client-side, offset encoded as a numeric cursor). User `metadata` and `cacheControl` throw (Box exposes file metadata via classifications and metadata templates — drop to `raw.fileMetadata.*` if you need it). - 54edb1b: Add DigitalOcean Spaces adapter (`files-sdk/digitalocean-spaces`). Thin wrapper over the S3 adapter with Spaces defaults: endpoint derived from `region` (`https://${region}.digitaloceanspaces.com`), virtual-hosted addressing, `"Spaces error"` provider label, and `DO_SPACES_KEY` / `DO_SPACES_SECRET` env-var fallbacks. `publicBaseUrl` accepts a Spaces CDN host (`https://${bucket}.${region}.cdn.digitaloceanspaces.com`) or a custom CNAME. - c841bbb: Add Dropbox adapter (`files-sdk/dropbox`) for personal Dropbox and Dropbox Business via the official `dropbox` SDK. Path-addressable like OneDrive, so virtual keys map directly to Dropbox paths — no virtual-key cache. Four auth shapes (pre-built `client`, static or callable `accessToken`, OAuth refresh-token flow with `refreshToken` + `appKey` (+ optional `appSecret`), and env-var fallback via `DROPBOX_ACCESS_TOKEN` or `DROPBOX_REFRESH_TOKEN` + `DROPBOX_APP_KEY` (+ `DROPBOX_APP_SECRET`)). Refresh tokens are exchanged at `api.dropboxapi.com/oauth2/token` and cached until ~60s before expiry. `url()` mints a 4-hour temporary link via `filesGetTemporaryLink` by default; with `publicByDefault: true`, `upload()` also creates a public shared link and `url()` returns it (rewritten to `?dl=1` for direct download); `expiresIn` is capped at Dropbox's 14400s (4h) maximum and `responseContentDisposition` always throws (no override on Dropbox links). `signedUploadUrl()` throws — Dropbox's temporary upload link expects POST with a raw body, which fits neither the SDK's PUT-with-headers nor POST-with-form-fields shape; use `upload()` or drop to `raw.filesGetTemporaryUploadLink(...)`. Direct `upload()` uses single-call `filesUpload` up to 150 MB and switches to `filesUploadSession*` (chunked, up to 350 GB) automatically; user `metadata` and `cacheControl` throw (Dropbox files have no native arbitrary-metadata field — use `raw` with `property_groups` if you need it). - 5ff9d79: Add Google Drive adapter (`files-sdk/google-drive`) via the official `@googleapis/drive` v3 client. Drive has no native key field, so the adapter maps virtual keys onto `appProperties.fsdkKey` and amortizes lookups with a per-instance LRU cache (configurable via `fileIdCacheSize`, defaults to 1024). Three auth shapes: inline service-account `credentials`, a `keyFilename` JSON path, or 3-legged `oauth` refresh tokens — plus a pre-built `client` escape hatch (note: `signedUploadUrl()` requires an auth handle and throws when constructed via `client`). `signedUploadUrl()` initiates a Drive resumable session and returns the session URL as a one-shot PUT (`maxSize` is forwarded as `X-Upload-Content-Length` advisory only; `minSize` is ignored). `url()` requires `publicByDefault: true` (grants `anyone, reader` on upload and returns the permanent Drive download URL); `expiresIn` ignored, `responseContentDisposition` always throws. Service-account workloads should target a Shared Drive via `driveId` to avoid the 15 GB personal quota. Caller `metadata` keys starting with `fsdk` are reserved. - 2a84ef2: Add Hetzner Object Storage adapter (`files-sdk/hetzner`). Thin wrapper over the S3 adapter with Hetzner defaults: endpoint derived from the `region` location code (`fsn1`, `nbg1`, `hel1`) as `https://.your-objectstorage.com` and overridable, virtual-hosted-style addressing, `"Hetzner error"` provider label, and `HCLOUD_ACCESS_KEY_ID` / `HCLOUD_SECRET_ACCESS_KEY` env-var fallbacks. `publicBaseUrl` accepts a custom CNAME or proxy host for unsigned URLs; otherwise `url()` returns a presigned GetObject (1-hour default). - b4fd387: Add Netlify Blobs adapter (`files-sdk/netlify-blobs`). Wraps the `@netlify/blobs` SDK with site-scoped or deploy-scoped stores, configurable consistency, and a metadata round-trip that packs `contentType`/`size`/`lastModified`/`cacheControl` plus user metadata into Netlify's metadata map so `head()`/`download()` return rich fields. Auto-detects credentials from Netlify's runtime context (`NETLIFY_BLOBS_CONTEXT`) when available, with explicit `siteID`/`token` overrides falling back to `NETLIFY_SITE_ID` / `NETLIFY_API_TOKEN` / `NETLIFY_BLOBS_TOKEN`. `copy()` is read-then-write since Netlify has no native copy primitive; `list()` returns key + etag (rich metadata requires a per-item `head()`); `url()` and `signedUploadUrl()` throw because Netlify Blobs has no public URL or presigned-upload primitive. - 0d5af66: Add OneDrive adapter (`files-sdk/onedrive`) for OneDrive personal, OneDrive for Business, and SharePoint document libraries via Microsoft Graph (`@microsoft/microsoft-graph-client` + `@azure/identity`). Path-addressable like the underlying API, so virtual keys map onto real OneDrive paths — no virtual-key cache, no reserved-metadata namespace. Four auth shapes (`clientCredentials` for app-only, `oauth` for delegated refresh-token flow, `accessToken` for caller-managed tokens, and a pre-built `client` escape hatch) and four drive targets (`/me/drive`, `driveId`, `siteId`, `userId`). `signedUploadUrl()` returns a Graph upload-session URL (one-shot PUT, advisory `maxSize`/`minSize`); `url()` requires `publicByDefault: true` and creates an anonymous-view share link (Graph has no signed URL primitive, `expiresIn` ignored). `copy()` polls Graph's async copy monitor with a configurable `copyTimeoutMs`. Direct `upload()` is capped at OneDrive's 250 MB simple-upload limit; user `metadata` and `cacheControl` throw (Graph drive items have no native arbitrary-metadata field — use `raw` for Open Extensions). - 7251d42: Add Storj adapter (`files-sdk/storj`). Thin wrapper over the S3 adapter with Storj defaults: `endpoint` defaults to `https://gateway.storjshare.io` (Gateway MT, the hosted multi-tenant gateway) and is overridable for self-hosted Gateway ST, path-style addressing on, region defaulted to `us-east-1` (the gateway ignores it for routing), `"Storj error"` provider label, and `STORJ_ACCESS_KEY_ID` / `STORJ_SECRET_ACCESS_KEY` env-var fallbacks. `publicBaseUrl` accepts a linksharing prefix like `https://link.storjshare.io/raw//` for unsigned URLs. - 37be6fc: Add UploadThing adapter (`files-sdk/uploadthing`). Maps the user-supplied key onto UploadThing's `customId`, supports public-read and private ACLs, signs UFS presigned PUT URLs via Web Crypto HMAC-SHA256, and falls back to HEAD-on-URL for `head()` and read-then-write for `copy()` since UploadThing has no native primitives for those. ### Patch Changes - 0ec97d0: Extract shared adapter helpers into `src/internal/core.ts` so authoring a new adapter is less boilerplate. The new module exports `DEFAULT_URL_EXPIRES_IN`, `joinPublicUrl`, `resolveUrlStrategy` (the two-state public-vs-sign decision, with `responseContentDisposition` always forcing signing), `normalizeBody` (Body → `Uint8Array | ReadableStream` + content-type/length), and `makeErrorMapper` (factory for the per-provider `mapXError` scaffold — code-set lookup, HTTP-status fallback, `FilesError` pass-through). The s3, azure, gcs, supabase, r2, fs, and uploadthing adapters now consume these helpers; supabase keeps its own `normalizeBody` because Blob pass-through is required for multipart uploads, and r2's `url()` keeps its three-state hybrid logic. `mapS3Error` retains its 2-arg legacy signature for the S3-compatible wrappers (R2 HTTP, MinIO, DigitalOcean Spaces, Storj, Hetzner, Akamai). No public-API changes. - 30d3634: Improve test coverage and remove dead code in the fs adapter. Adds tests for r2's HTTP-path delegation (copy/delete/download/head/list/signedUploadUrl proxies to the lazy-loaded inner s3 adapter, plus the `raw` getter's pre/post-init behavior) and for fs uploads with `ArrayBuffer` and `ArrayBufferView` bodies plus rejection of keys that resolve to the adapter root. Drops the unreachable `ReadableStream` branch in `fs/bodyToBytes` — stream uploads route through `writeStreamToTempThenRename`, so the parameter type is narrowed to `Exclude>` to enforce that at the type level. Further hardens coverage of edge paths across the fs, azure, supabase, r2, and stored-file modules: corrupt/partial sidecar JSON handling, lazy-body errors when an underlying file is removed, atomic upload cleanup when rename fails (buffer + stream paths), non-ENOENT delete errors, Azure stream downloads with missing `readableStreamBody`, anonymous Azure copy source URLs, Supabase numeric `statusCode` fallback and Date/number `lastModified` parsing, R2 binding copy with put failure, and concurrent reads on a lazy `StoredFile` sharing the in-flight cache promise. --- # files-sdk@1.1.1 Source: https://files-sdk.dev/changelog/files-sdk-1-1-1 ### Patch Changes - bd31113: Fix release workflow referencing a non-existent `VERCEL_PROJECT_ID_WEB` secret; now reads `VERCEL_PROJECT_ID` to match the configured repository secret so the post-publish Vercel deploy succeeds. --- # files-sdk@1.1.2 Source: https://files-sdk.dev/changelog/files-sdk-1-1-2 ### Patch Changes - 6edb433: `googleDrive` and `onedrive` adapters now auto-load credentials from `process.env` when not passed explicitly, matching the convention already in place for the other adapters. `googleDrive()` reads `GOOGLE_DRIVE_CLIENT_EMAIL` + `GOOGLE_DRIVE_PRIVATE_KEY` (service-account credentials) or `GOOGLE_DRIVE_KEY_FILE` (path to a service-account JSON), plus `GOOGLE_DRIVE_SUBJECT` for domain-wide delegation, `GOOGLE_DRIVE_ID` to target a Shared Drive, and `GOOGLE_DRIVE_ROOT_FOLDER_ID` to override the bucket root (when only `GOOGLE_DRIVE_ID` is set, `rootFolderId` defaults to the drive id so Shared Drives work with no extra config). `onedrive()` reads `ONEDRIVE_ACCESS_TOKEN` (static token) or the `ONEDRIVE_TENANT_ID` + `ONEDRIVE_CLIENT_ID` + `ONEDRIVE_CLIENT_SECRET` triple (client-credentials/app-only auth), plus `ONEDRIVE_DRIVE_ID` / `ONEDRIVE_SITE_ID` / `ONEDRIVE_USER_ID` to target a specific drive — the existing "client-credentials needs a target" guard still applies. Explicit options continue to take precedence over env vars; missing-auth error messages now mention the env fallback names. --- # files-sdk@1.2.0 Source: https://files-sdk.dev/changelog/files-sdk-1-2-0 ### Minor Changes - 9758347: Add AI SDK tools subpath (`files-sdk/ai-sdk`) exporting `createFileTools(...)` — wraps a configured `Files` instance as a set of Vercel AI SDK tools (`listFiles`, `getFileMetadata`, `downloadFile`, `getFileUrl`, `uploadFile`, `deleteFile`, `copyFile`, `signUploadUrl`) ready to plug into `generateText` / `streamText` / any agent. Mirrors `@github-tools/sdk`'s ergonomics: write tools require approval by default (configurable globally or per-tool via `requireApproval`), `readOnly: true` strips writes entirely, and `overrides` lets callers patch tool descriptions/titles/etc. without touching `execute`. Individual tool factories (`uploadFile`, `downloadFile`, …) are also exported for cherry-picking. `ai` and `zod` are optional peer dependencies — only required when consuming the new subpath. - 2d811b1: Add Claude Agent SDK tools subpath (`files-sdk/claude`) exporting `createClaudeFileTools(...)` — wraps a configured `Files` instance as an in-process MCP server ready to drop into `query()` from [`@anthropic-ai/claude-agent-sdk`](https://docs.claude.com/en/api/agent-sdk/overview) (the renamed Claude Code SDK). The Claude Agent SDK consumes tools differently than the OpenAI/Vercel adapters: tools are bundled into an `SdkMcpServer` and surfaced to the agent via `mcpServers` + `allowedTools`, with approval enforced through a top-level `canUseTool` callback. The factory returns all four pieces: ```ts const tools = createClaudeFileTools({ files }); for await (const msg of query({ prompt: "List my files.", options: { mcpServers: tools.mcpServers, allowedTools: tools.allowedTools, canUseTool: tools.canUseTool, }, })) { /* ... */ } ``` Same eight file operations as the other AI subpaths (`listFiles`, `getFileMetadata`, `downloadFile`, `getFileUrl`, `uploadFile`, `deleteFile`, `copyFile`, `signUploadUrl`) with the same approval-gating defaults, `readOnly` mode, and per-tool `overrides` (description + MCP `annotations`). The bundled `canUseTool` denies approval-gated writes; compose your own using `tools.needsApproval(name)` for human-in-the-loop UX — it accepts both bare names (`"uploadFile"`) and the MCP-prefixed form (`"mcp__files__uploadFile"`) the SDK passes in. The MCP server name defaults to `"files"` and is configurable via `serverName`, which also flows through to the `mcp____*` strings in `allowedTools`. Read tools get a `readOnlyHint` annotation; writes get `destructiveHint` (`copyFile` / `signUploadUrl` use `idempotentHint` instead). Individual tool factories (`claudeUploadFile`, `claudeDownloadFile`, …) are also exported as `SdkMcpToolDefinition` instances for callers that want to compose their own `createSdkMcpServer` rather than use the bundled one. `@anthropic-ai/claude-agent-sdk` and `zod` are optional peer dependencies — only required when consuming the new subpath. - d6adeae: Add OpenAI tools subpath (`files-sdk/openai`) with two factories: - `createResponsesFileTools(...)` — for OpenAI's native [Responses API](https://platform.openai.com/docs/api-reference/responses). Returns `{ definitions, execute, needsApproval }`. `definitions` is the array of function-tool specs to pass into `openai.responses.create({ tools })`. `execute(call)` runs a `function_call` item and returns a `function_call_output` ready to push into the next turn's input — JSON parse failures and Zod validation errors come back as the tool's output so the model can self-correct. - `createAgentsFileTools(...)` — for the [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/) (`@openai/agents`). Returns a record of `tool()` outputs ready to spread into `new Agent({ tools })`. Both wrap the same eight file operations as `files-sdk/ai-sdk` (`listFiles`, `getFileMetadata`, `downloadFile`, `getFileUrl`, `uploadFile`, `deleteFile`, `copyFile`, `signUploadUrl`) with the same approval-gating defaults, `readOnly` mode, and per-tool overrides. Schemas + execute logic are extracted to a shared internal module so the three subpaths can't drift apart. `openai` and `@openai/agents` are optional peer dependencies — install only the one(s) you use. The subpath requires Zod 4. --- # files-sdk@1.3.0 Source: https://files-sdk.dev/changelog/files-sdk-1-3-0 ### Minor Changes - 2d3a569: Add Appwrite adapter at `files-sdk/appwrite` exporting `appwrite()`, a wrapper around the official `node-appwrite` SDK's `Storage` API. Auto-loads `endpoint`, `projectId`, and `key` from `APPWRITE_ENDPOINT` / `APPWRITE_PROJECT_ID` / `APPWRITE_API_KEY` (with `NEXT_PUBLIC_*` fallbacks for the first two), or accepts an existing `Client` or `Storage` instance via `client`. `list({ prefix })` is forwarded as a `startsWith("$id", prefix)` query against the canonical file ID — files created outside the adapter where the display `name` differs from `$id` won't be matched by prefix. `upload()` buffers stream bodies up-front since `InputFile.fromBuffer` has no streaming form, throws on `UploadOptions.cacheControl` and non-empty `UploadOptions.metadata` (Appwrite has no equivalent fields), and silently ignores `UploadOptions.contentType` (Appwrite auto-detects mime from the payload). `copy()` is read-then-write — Appwrite has no server-side copy primitive, so it costs an egress + an ingest and is not atomic. `url()` throws by default (Appwrite SDKs cannot mint signed read URLs with API keys); set `public: true` on a public bucket to return the constructed permanent `view` URL. `signedUploadUrl()` throws — Appwrite has no presigned upload primitive; use JWTs or the client SDK for direct uploads. Keys (Appwrite file IDs) must start with `[a-zA-Z0-9]` and use only `[a-zA-Z0-9._-]`, max 36 characters — invalid keys throw a `FilesError("Provider", ...)` before the API call. Errors are relabelled as `Appwrite error`, with `404`/`401`+`403`/`409` mapped to `NotFound`/`Unauthorized`/`Conflict`. - ed87e51: Add Backblaze B2 adapter at `files-sdk/backblaze-b2`, a thin S3 wrapper that derives the endpoint from the cluster code (`s3..backblazeb2.com`), defaults to virtual-hosted-style addressing, and auto-loads credentials from `B2_APPLICATION_KEY_ID` / `B2_APPLICATION_KEY`. Errors are relabelled as `Backblaze B2 error` and `publicBaseUrl` accepts B2's friendly download URL prefix for skipping signing on public buckets. - 2a35ce1: Add `exists(key)` to the Files API. Returns `true` when the object exists and `false` when the adapter reports a not-found error, without fetching the object body. Implemented across all built-in adapters. - 8ae51f0: Add Exoscale Object Storage (SOS) adapter at `files-sdk/exoscale`, a thin S3 wrapper that derives the endpoint from the zone code (`sos-.exo.io` — `ch-gva-2`, `ch-dk-2`, `de-fra-1`, `de-muc-1`, `at-vie-1`, `at-vie-2`, `bg-sof-1`), defaults to virtual-hosted-style addressing, and auto-loads credentials from `EXOSCALE_API_KEY` / `EXOSCALE_API_SECRET`. Exoscale calls these zones but they fill the SigV4 region slot. Errors are relabelled as `Exoscale error`. - 2c52f56: Add `files.file(key)` to return a `FileHandle` bound to a single key. The handle exposes `upload`, `download`, `head`, `exists`, `delete`, `url`, `signedUploadUrl`, `copyTo`, and `copyFrom` without re-passing the key each time. It's a thin wrapper over the same `Files` methods, so adapters do not need to implement anything extra. - 8ae51f0: Add Filebase adapter at `files-sdk/filebase`, a thin S3 wrapper around Filebase's S3-compatible gateway in front of decentralized storage networks (IPFS, Sia, Storj — the backing network is chosen per-bucket in the dashboard). Uses the fixed `https://s3.filebase.com` endpoint with virtual-hosted-style addressing, defaults the SigV4 region to `"us-east-1"`, and auto-loads credentials from `FILEBASE_ACCESS_KEY_ID` / `FILEBASE_SECRET_ACCESS_KEY`. `publicBaseUrl` accepts an IPFS/Sia/Storj gateway prefix for skipping signing on public objects. Errors are relabelled as `Filebase error`. - 8ae51f0: Add IBM Cloud Object Storage adapter at `files-sdk/ibm-cos`, a thin S3 wrapper that derives the endpoint from the region code (`s3..cloud-object-storage.appdomain.cloud` — `us-south`, `us-east`, `eu-de`, `eu-gb`, `jp-tok`, `au-syd`, `br-sao`, `ca-tor`, …), defaults to virtual-hosted-style addressing, and auto-loads credentials from `IBM_COS_ACCESS_KEY_ID` / `IBM_COS_SECRET_ACCESS_KEY`. Auth uses IBM Cloud's HMAC credentials (tick "Include HMAC Credential" in the service-credential Advanced options), not IAM API keys. For direct (no-egress) access from inside the same IBM Cloud region, pass `https://s3.direct..cloud-object-storage.appdomain.cloud` as an explicit `endpoint`. Errors are relabelled as `IBM Cloud Object Storage error`. - 8ae51f0: Add iDrive e2 adapter at `files-sdk/idrive-e2`, a thin S3 wrapper that takes an explicit `endpoint` (iDrive e2 hostnames are tied to the provisioned bucket cluster and don't follow a public pattern — copy it from the iDrive e2 dashboard under Access Keys → Endpoint), defaults the SigV4 region to `"us-east-1"`, and auto-loads credentials from `IDRIVE_E2_ACCESS_KEY_ID` / `IDRIVE_E2_SECRET_ACCESS_KEY`. Errors are relabelled as `iDrive e2 error`. - 8ae51f0: Add Oracle Cloud Infrastructure Object Storage adapter at `files-sdk/oracle-cloud`, a thin S3 wrapper around OCI's S3 compatibility layer. Requires both the tenancy `namespace` and a `region` to derive the endpoint (`.compat.objectstorage..oraclecloud.com`); defaults to path-style addressing since OCI's wildcard TLS cert doesn't cover bucket subdomains under the namespace-prefixed host. Auth uses OCI's HMAC _Customer Secret Keys_ (distinct from regular API signing keys); credentials auto-load from `OCI_ACCESS_KEY_ID` / `OCI_SECRET_ACCESS_KEY`. Errors are relabelled as `Oracle Cloud error`. - 8ae51f0: Add OVHcloud Object Storage adapter at `files-sdk/ovhcloud`, a thin S3 wrapper that derives the endpoint from the region code (`s3..io.cloud.ovh.net` — High Performance S3 tier), defaults to virtual-hosted-style addressing, and auto-loads credentials from `OVH_ACCESS_KEY_ID` / `OVH_SECRET_ACCESS_KEY`. For the Standard (Swift-backed) tier, pass `https://s3..cloud.ovh.net` as an explicit `endpoint`. Errors are relabelled as `OVHcloud error`. - 8ae51f0: Add Scaleway Object Storage adapter at `files-sdk/scaleway`, a thin S3 wrapper that derives the endpoint from the region code (`s3..scw.cloud` — `fr-par`, `nl-ams`, `pl-waw`), defaults to virtual-hosted-style addressing, and auto-loads credentials from `SCW_ACCESS_KEY` / `SCW_SECRET_KEY`. Errors are relabelled as `Scaleway error`. - ed87e51: Add Tigris adapter at `files-sdk/tigris`, a thin S3 wrapper around Tigris's globally-distributed object storage. Uses the fixed `https://fly.storage.tigris.dev` endpoint with virtual-hosted-style addressing, defaults the SigV4 region to `"auto"` since Tigris doesn't route by region, and auto-loads credentials from `TIGRIS_ACCESS_KEY_ID` / `TIGRIS_SECRET_ACCESS_KEY`. Errors are relabelled as `Tigris error`. - 8ae51f0: Add Vultr Object Storage adapter at `files-sdk/vultr`, a thin S3 wrapper that derives the endpoint from the region code (`.vultrobjects.com` — `ewr`, `sjc`, `ams`, `blr`, `del`, `sgp`, `lux`), defaults to virtual-hosted-style addressing, and auto-loads credentials from `VULTR_ACCESS_KEY_ID` / `VULTR_SECRET_ACCESS_KEY`. Errors are relabelled as `Vultr error`. - ed87e51: Add Wasabi adapter at `files-sdk/wasabi`, a thin S3 wrapper that derives the endpoint from the region code (`s3..wasabisys.com`), defaults to virtual-hosted-style addressing, and auto-loads credentials from `WASABI_ACCESS_KEY_ID` / `WASABI_SECRET_ACCESS_KEY`. Region names mirror AWS but the endpoints are Wasabi's own; errors are relabelled as `Wasabi error`. ### Patch Changes - 2aa92e1: URL-encode keys in `joinPublicUrl` to prevent injection attacks via special characters (`?`, `#`, spaces) in file keys. Uses segment-by-segment encoding to preserve `/` as a path separator. **Note:** Pass raw keys — this function handles encoding. Pre-encoded keys will be double-encoded (e.g. `%20` becomes `%2520`). - 8982c51: Expand test coverage for `box`, `fs`, `onedrive`, `supabase`, and `openai/responses` adapters. Adds tests covering `mapBoxError` / `mapGraphError` non-API error shapes, trailing-slash key handling, no-extension content-type inference, cache-miss reuse and non-file conflict paths in Box, trailing-slash URL trimming in Supabase, and ENOENT mid-page plus non-ENOENT walk errors in the fs adapter. No behavior changes. --- # files-sdk@1.4.0 Source: https://files-sdk.dev/changelog/files-sdk-1-4-0 ### Minor Changes - ef0d6af: Add Alibaba Cloud Object Storage Service (OSS) adapter (`files-sdk/alibaba`). Thin wrapper around the S3 adapter — endpoint derived from the region code (`oss-.aliyuncs.com`), virtual-hosted-style addressing, errors relabelled as "Alibaba Cloud error". Auto-loads from `ALIBABA_ACCESS_KEY_ID` and `ALIBABA_ACCESS_KEY_SECRET`. - d619709: Add `files` CLI for agents and scripts. One binary covers every adapter via `--provider ` with lazy imports — cold-start cost matches whichever single provider you select. Each `Adapter` method maps to a subcommand (`upload`, `download`, `head`, `exists`, `delete`, `copy`, `list`, `url`, `sign-upload`), with JSON-by-default output, `stdin`/`stdout` streaming for binary bodies, `--dry-run` and `--verbose` modes, and a stable exit-code mapping (`NotFound` → 1, `Provider` → 2, `Unauthorized` → 3, `Conflict` → 4). Provider credentials come from each adapter's existing env-var conventions, and `--config-json` is an escape hatch for the long tail of adapter options. `files ... mcp` boots a stdio MCP server exposing every command as a tool — provider and credentials bind at startup, so the agent only passes operation arguments. - d0aec82: Add Cloudinary adapter (`files-sdk/cloudinary`). Defaults to `resource_type: "raw"` for arbitrary-bytes storage; switch to `image`/`video` for transforms. Reads `CLOUDINARY_URL` or individual `CLOUDINARY_*` env vars. Full Adapter surface including signed delivery URLs for `private`/`authenticated` types and form-POST signed upload URLs. - 8b62142: Add Firebase Storage adapter (`files-sdk/firebase-storage`). Wraps the official `firebase-admin` SDK; the underlying `getStorage().bucket()` returns a `@google-cloud/storage` `Bucket`, so V4 signed read URLs, POST policy uploads with `maxSize`, server-side copy, and the full metadata round-trip all work out of the box. Auto-loads credentials from `FIREBASE_PROJECT_ID` / `FIREBASE_CLIENT_EMAIL` / `FIREBASE_PRIVATE_KEY` / `FIREBASE_STORAGE_BUCKET`, falling back to a service-account JSON path (`GOOGLE_APPLICATION_CREDENTIALS`) and then to Application Default Credentials. Accepts an existing `App` or `Bucket` via `app` to share initialization with Firestore/Auth. The bucket name defaults to `.firebasestorage.app` when neither `bucket` nor `FIREBASE_STORAGE_BUCKET` is set. Firebase's `?alt=media&token=…` download-token URL form is out of scope for v1 — reach for `adapter.raw` if you need it. - 8b62142: Add PocketBase adapter (`files-sdk/pocketbase`). Wraps the official `pocketbase` JS SDK and maps the unified key/blob API onto a dedicated collection: each upload becomes (or updates) a record whose configurable `keyField` (unique-indexed text, default `"key"`) holds the user-facing key and whose configurable `fileField` (single-value file, default `"file"`) holds the body. Auto-loads from `POCKETBASE_URL` plus either `POCKETBASE_ADMIN_EMAIL` + `POCKETBASE_ADMIN_PASSWORD` (admin login on first call) or `POCKETBASE_AUTH_TOKEN` (pre-issued token); accepts an existing `PocketBase` client via `client`. `url()` returns `pb.files.getURL()`, threading a short-lived file token from `pb.files.getToken()` for authenticated clients; set `publicBaseUrl` for a CDN override. `signedUploadUrl()` throws — PocketBase has no presigned upload primitive. `copy()` is read-then-write (no server-side copy). `list()` paginates via page number encoded as a numeric cursor string. `UploadOptions` `cacheControl` and `metadata` throw — PocketBase has no per-file HTTP cache headers and no arbitrary-metadata field on the file; add extra typed columns to the collection and write via `raw` if you need them. `responseContentDisposition` on `url()` throws — use `raw` and the `?download=true` query string instead. - d0aec82: Add SharePoint adapter (`files-sdk/sharepoint`). Resolves `siteUrl` and named `documentLibrary` to a drive via Microsoft Graph, then delegates to the OneDrive adapter for file operations. Falls back to `SHAREPOINT_*` env vars then to `ONEDRIVE_*`. Resolution is lazy and cached after the first call. - ef0d6af: Add Tencent Cloud Object Storage (COS) adapter (`files-sdk/tencent`). Thin wrapper around the S3 adapter — endpoint derived from the region code (`cos..myqcloud.com`), virtual-hosted-style addressing, errors relabelled as "Tencent Cloud error". Auto-loads from `TENCENT_SECRET_ID` and `TENCENT_SECRET_KEY`. Bucket name must include the `-` suffix per COS's namespacing. - ef0d6af: Add Yandex Object Storage adapter (`files-sdk/yandex`). Thin wrapper around the S3 adapter — fixed global endpoint (`storage.yandexcloud.net`), region defaults to `ru-central1` for signing, virtual-hosted-style addressing, errors relabelled as "Yandex Cloud error". Auto-loads from `YANDEX_ACCESS_KEY_ID` and `YANDEX_SECRET_ACCESS_KEY`. - de63748: Add Bun S3 adapter at `files-sdk/bun-s3`, backed by Bun's native `Bun.S3Client` instead of `@aws-sdk/client-s3`. Use this when you're already on Bun and want to skip the AWS SDK dependency. Implements the full adapter surface (upload, download, head, exists, delete, copy, list, url, signedUploadUrl) with three deliberate limitations vs `files-sdk/s3`: `copy()` is client-side (Bun has no server-side `CopyObject` primitive), and `upload(metadata|cacheControl)` plus `signedUploadUrl(maxSize)` throw because `Bun.S3Client` doesn't expose equivalent options. Pass `client: Bun.s3` to reuse the global singleton, or hand in any custom `Bun.S3Client`-shaped instance. - 28e3243: Add Bunny Storage adapter (`files-sdk/bunny-storage`). Wraps the official `@bunny.net/storage-sdk` and connects to a Storage Zone via zone name + access key + region. Auto-loads from `BUNNY_STORAGE_ZONE` / `BUNNY_STORAGE_ACCESS_KEY` / `BUNNY_STORAGE_REGION`, with `STORAGE_*` accepted as aliases (the names used in the Bunny SDK's README). `url()` requires `publicBaseUrl` (typically a Bunny Pull Zone) and returns a permanent CDN URL — Bunny has no signed-read primitive, so `expiresIn` is ignored and `responseContentDisposition` throws. `signedUploadUrl()` throws because Bunny writes require the Storage API `AccessKey` header. `copy()` is a read-then-write (no server-side copy primitive in the SDK). Custom `metadata` and `cacheControl` on upload throw — configure cache behavior on the Pull Zone instead. - 78bcf37: Move provider SDKs to optional peer dependencies. Installing `files-sdk` no longer pulls in every provider SDK by default — the package fully installs at a fraction of the previous size, and unused providers can't drag in transitive CVEs. Install only what you use: ```sh # S3 (and any S3-compatible: R2, MinIO, DigitalOcean Spaces, …) npm install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner # GCS npm install files-sdk @google-cloud/storage google-auth-library # Azure npm install files-sdk @azure/storage-blob @azure/identity ``` **Breaking (install-time only):** if you upgrade and your project doesn't list the relevant provider SDK in its own `package.json`, the next adapter import will throw `ERR_MODULE_NOT_FOUND`. Fix is one `npm install`. The published JS for each adapter subpath (`files-sdk/s3`, `files-sdk/gcs`, …) is byte-identical to the previous release — provider SDKs were already externalized, so runtime behavior, tree-shaking, and bundle sizes don't change. The `files` CLI keeps `commander` as a regular dep, so `npx files` works out of the box. Fixes #34. ### Patch Changes - a53be2d: Expand adapter test coverage for error-recovery branches that were previously unexercised: `exists()` swallowing a thrown `NotFound` (azure, gcs, netlify-blobs, r2) versus rethrowing other mapped errors; the supabase stream-download error envelope; and dropbox's `exists()` returning false for `folder`/`deleted` `.tag`s plus the `shared_link_already_exists` recovery falling through when no usable URL is embedded. No runtime behavior changes. --- # files-sdk@1.5.0 Source: https://files-sdk.dev/changelog/files-sdk-1-5-0 ### Minor Changes - c6b4df1: `upload`, `download`, `head`, and `exists` now accept an array for bulk operations, mirroring `delete`. Pass the usual single argument for the original behavior (resolves to one result, throws on failure); pass an array to operate on many in one call and get back a structured result instead of throwing on partial failure — so you can see exactly which keys succeeded and which failed: ```ts const up = await files.upload( [ { key: "avatars/a.png", body: a, contentType: "image/png" }, { key: "avatars/b.png", body: b }, ], { concurrency: 8, stopOnError: false } ); up.uploaded; // UploadResult[] — successes, in the order supplied up.errors; // undefined when every item succeeded const down = await files.download(["a.png", "b.png"]); // { downloaded, errors? } const meta = await files.head(["a.png", "b.png"]); // { files, errors? } const there = await files.exists(["a.png", "b.png"]); // { existing, missing, errors? } ``` `upload`'s array items are flat — each carries its own `key`, `body`, and optional `contentType` / `cacheControl` / `metadata`. No provider exposes a native batch primitive for these operations, so the SDK always fans out to per-key calls with bounded `concurrency` (default 8); `stopOnError: false` (default) attempts every item and collects per-key failures in `errors`, while `stopOnError: true` stops at the first failure. All array forms honor the client's `prefix` and report the keys the caller passed, not the internal prefixed paths. Invalid keys are reported in `errors` rather than thrown. `exists` splits results into `existing` / `missing` and only routes hard errors (auth, transport) to `errors`. The `files` CLI's `head` and `exists` commands and the MCP `head` / `exists` tools accept multiple keys too. - ed72daf: Add a Convex storage adapter (`files-sdk/convex`). Convex file storage is only reachable from inside a Convex function, so the adapter wraps the function context — `convex({ ctx })`, constructed per request inside an action, mutation, or query — and maps the unified `Adapter` surface onto `ctx.storage` / `ctx.db.system`. Because Convex assigns the storage id (`Id<"_storage">`) and exposes no writable metadata, the storage id is the key: `upload()` returns the assigned id, and `download`/`head`/`delete`/`url` take it back. Available operations follow Convex's context rules — `upload`/`download` need an action, `list` needs a query/mutation — and the adapter throws a descriptive error when a primitive is unavailable. `copy`, custom `metadata`, and `cacheControl` are unsupported; `url()` returns a permanent serving URL; `signedUploadUrl()` returns Convex's raw-body POST upload URL. `convex` is an optional peer dependency. - bad4a80: `delete()` now accepts an array of keys for bulk deletion. Pass a string to remove one object (resolves to `void`, throws on failure as before); pass an array to remove many in one call and get back a structured `{ deleted, errors? }` result instead of throwing on partial failure — so you can see exactly which keys failed: ```ts const result = await files.delete( ["avatars/a.png", "avatars/b.png", "avatars/c.png"], { concurrency: 8, stopOnError: false } ); result.deleted; // string[] — keys removed, in the order supplied result.errors; // undefined when every key succeeded ``` Adapters with a native bulk primitive use it — S3 sends `DeleteObjects` (chunked into batches of 1000, the provider limit), Supabase uses `remove(keys)`, and UploadThing uses `deleteFiles(keys)` — while every other adapter fans out to single deletes with bounded `concurrency` (default 8). `stopOnError: false` (default) attempts every key and collects per-key failures in `errors`; `stopOnError: true` stops at the first failure. Invalid keys are reported in `errors` rather than thrown, and the array form honors the client's `prefix` and is no-op friendly on providers that treat a missing key as success. The `files` CLI's `delete` command and the MCP `delete` tool accept multiple keys too. - 9e9fa13: Add FTP and SFTP adapters (`files-sdk/ftp`, `files-sdk/sftp`) for on-prem and legacy file servers. Both expose the standard unified surface, so they're interchangeable with the cloud adapters: ```ts import { Files } from "files-sdk"; import { sftp } from "files-sdk/sftp"; const files = new Files({ adapter: sftp({ host: "files.example.com", username: process.env.SFTP_USERNAME!, privateKey: process.env.SFTP_PRIVATE_KEY!, root: "/uploads", }), }); await files.upload("reports/q1.csv", csv, { contentType: "text/csv" }); ``` FTP uses [`basic-ftp`](https://www.npmjs.com/package/basic-ftp) (with FTPS via `secure: true`); SFTP uses [`ssh2-sftp-client`](https://www.npmjs.com/package/ssh2-sftp-client). Both are optional peer dependencies. These adapters are **Node-only** (raw sockets — no edge/browser/Workers support) and connect per operation by default; pass a pre-connected `client` to reuse one connection for batch work. Keys resolve under a configurable `root` with a `..` traversal guard, `list` walks the tree recursively with cursor pagination, and `deleteMany` reuses a single connection. These protocols store no MIME type (inferred from the file extension), no arbitrary `metadata`/`cacheControl` (both throw), and serve no HTTP — `url()` requires a `publicBaseUrl` pointing at an HTTP server fronting the same tree, and `signedUploadUrl()` throws. `copy` round-trips the bytes through the client since neither protocol has a portable server-side copy. - 1eb1dfc: Add a `files-sdk/providers` export: a zero-dependency catalog of every storage provider and the environment variables each one reads. `PROVIDERS` maps each slug to its display name, description, optional peer dependencies, and a structured env spec — `required` vars, mutually exclusive `credentialModes` (so Azure's connection-string-or-key-or-SAS choice is expressible), `optional` tuning vars, and non-env `config`. Every variable is tagged `secret` and `readBy` (`"files-sdk"` vs the underlying SDK's `"sdk-chain"`, so AWS/GCS credential-chain vars aren't mislabeled as required). Helpers: `getProvider`, `listEnvVars`, `getSecretEnvVars`. `PROVIDER_NAMES` and the `Provider`/`ProviderSlug` types are also re-exported from the package root. Useful for sync engines, config UIs, and onboarding flows that need to enumerate providers and their required configuration up front. ### Patch Changes - e80e922: Add `signal`, `timeout`, and `retries` to every operation. Set them on the `Files` constructor as defaults and override per call (a per-call value wins). `retries` is a number or `{ max, backoff }`; only `Provider` failures are retried — `NotFound`, `Unauthorized`, `Conflict`, aborts, and timeouts are returned immediately, and `ReadableStream` uploads are never retried because a consumed stream can't be replayed. The default backoff is exponential (`100 * 2 ** (attempt - 1)` ms, capped at 30s, no jitter); pass your own `backoff({ attempt, error })` for jitter or a different curve. `timeout` is applied per attempt and aborts the operation rather than triggering a retry. A `signal` always fails fast at the `Files` layer for every adapter; the underlying provider request is also cancelled on the S3 adapter and the S3-compatible catalog, Vercel Blob and UploadThing's fetch-backed reads, Azure, Google Drive, and PocketBase (across their operations), Supabase (`download` and `list` — the only methods its SDK lets a signal through), and the fetch-backed downloads of Box, Cloudinary, and Dropbox. Adapters whose SDK exposes no cancellation (GCS, Firebase Storage, Netlify Blobs, Appwrite, Bunny, Bun S3, and the R2 binding path) still fail fast at the `Files` layer but leave the in-flight request running. - f774aa2: Add Azure AD / Managed Identity support to the Azure adapter via a `credential` (`TokenCredential`) option. Token-authenticated adapters mint User Delegation SAS URLs for `url()`, `signedUploadUrl()`, and same-container `copy()`, so signed URLs keep working without a storage account key. Set `useUserDelegationSas: false` to opt out of SAS signing for token-only setups. - dbda237: Add a `prefix` option to the `Files` constructor. When set, every key is resolved relative to the prefix - reads, writes, copies, listings, URLs, and signed uploads - and the prefix is stripped back off the keys (and `name`) returned in results, so your application code works in its own namespace: ```ts const users = new Files({ adapter: s3({ bucket: "uploads" }), prefix: "users", }); await users.upload("123/avatar.png", file); // writes users/123/avatar.png const stored = await users.head("123/avatar.png"); stored.key; // "123/avatar.png" - prefix stripped ``` Leading and trailing slashes on the prefix are normalized (`"/users/"` and `"users"` behave identically), and `list()` scopes the underlying query on a path boundary so a `prefix: "users"` instance never matches the sibling `users-archive/`. - d921741: Harden three internal regexes against polynomial ReDoS. The trailing-slash/`[. ]`-stripping patterns in `normalizePrefix` (core, used by every adapter's prefix handling), the `fs` adapter's Windows trailing-noise check, and the `bunny-storage` key parser each anchor with a `(?` or `` form. Bumps the `@vercel/blob` peer dep floor to `^2.4.0`, which is the first version that ships the OIDC options. --- # files-sdk@1.6.0 Source: https://files-sdk.dev/changelog/files-sdk-1-6-0 ### Minor Changes - 12d6218: Bring the CLI (and MCP server) to full parity with the SDK surface. Every `Files` capability is now reachable from the `files` binary: - **Global `--key-prefix`** scopes every operation under a base path (the instance prefix from `new Files({ prefix })`, distinct from the one-off `list --prefix` filter). **Global `--timeout` / `--retries`** set the per-attempt timeout and retry count for all commands. - **`download --range start-end`** downloads a byte range (0-based, inclusive), e.g. `0-1023` or `1024-`. - **`upload --multipart`** (with `--part-size` / `--multipart-concurrency`) uploads large objects in parallel parts. - **`head` / `exists` / `delete`** accept `--concurrency` and `--stop-on-error` to tune the bulk fan-out for many keys. - **`list --all`** walks every page (following the cursor) and returns all items in one result. - **`upload --dir `** uploads a whole local tree (keyed by relative path, content type inferred per file), and **`download --out-dir `** downloads many keys into a directory — both built on the SDK's bulk array forms. - **`transfer`** copies every object from the configured (source) provider to another provider given as a JSON config (`--to`), streaming each body across backends. `--prefix` filters the walk and `--no-overwrite` skips keys already present at the destination. The MCP server mirrors all of the above: the `upload` tool takes `multipart`, `download` takes a byte `range`, the `head` / `exists` / `delete` tools take `concurrency` / `stopOnError`, `list` takes `all`, and a new `transfer` tool copies objects across providers. The global `--key-prefix` / `--timeout` / `--retries` bind to the server's `Files` instance at startup. - 0bb7ca3: Add `transfer` for cross-provider migration. `transfer(source, dest, options?)` streams every object from one `Files` instance to another — the one operation the unified surface uniquely enables, since `copy`/`move` live inside a single adapter. It's built entirely on public primitives (the source's `listAll` + streaming `download`, the destination's `exists` + `upload`), so no adapter implements anything new. ```ts import { Files, transfer } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { r2 } from "files-sdk/r2"; const from = new Files({ adapter: s3({ bucket: "old" }) }); const to = new Files({ adapter: r2({ bucket: "new", accountId, accessKeyId, secretAccessKey }), }); const { transferred, skipped, errors } = await transfer(from, to, { prefix: "uploads/", onProgress: ({ done, key }) => console.log(done, key), }); ``` Both sides are full `Files` instances, so each leg honors its own `prefix`, retries, timeouts, and hooks. Each object is streamed download-to-upload — the destination never buffers a whole large file. Body, content type, and user metadata travel; `etag`/`lastModified` are destination-assigned and `Cache-Control` is not carried. Like the bulk array methods, `transfer` doesn't throw on partial failure: results come back as `{ transferred, skipped?, errors? }` in walk order. Options cover `prefix`, `transformKey`, `overwrite` (skip keys already present), `concurrency` (default 8), `limit` (walk page size), `stopOnError` (sequential, bail at first failure), `signal`, and `onProgress`. - 5d24bc8: Add `hooks` to `new Files(...)` so applications can observe SDK activity with `onAction`, `onError`, and `onRetry`. Each hook is fire-and-forget (called, not awaited) and receives a small, caller-facing event — the operation `type`, the public `key` / `keys` (or `from` / `to` for `copy`), timing, and the final result or error. It mirrors the lightweight `onProgress` callback style. - c50a55a: Add an in-memory adapter at `files-sdk/memory`. It implements the full `Adapter` contract backed by a `Map`, so you can test code that uses `Files` without touching disk or real storage — the same swap-in-via-env story as the other adapters, but with nothing to clean up. ```ts import { Files } from "files-sdk"; import { memory } from "files-sdk/memory"; const files = new Files({ adapter: memory() }); await files.upload("hello.txt", "hi"); (await files.download("hello.txt")).text(); // "hi" ``` Zero dependencies and isomorphic (no `node:fs`/`node:crypto`), so it runs unchanged in Node, Bun, Deno, the browser, and edge runtimes. Pass `initial` to pre-populate fixtures, and reach into `adapter.raw` (the backing `Map`) to inspect or reset the store between tests: ```ts const adapter = memory({ initial: { "users/1.json": '{"id":1}' } }); adapter.raw.clear(); ``` `url()` returns an opaque, non-fetchable `memory://${key}` and `signedUploadUrl()` a `memory://` placeholder — there's no server backing the store. It's a test/reference adapter, not for production. - 67349f4: Add `move` and `listAll`. `files.move(from, to, options?)` renames a key. It uses the adapter's native rename where one exists (the `fs` adapter renames in place atomically; Cloudinary uses its server-side `rename`, keeping the same `asset_id` with no re-upload) and otherwise falls back to `copy` + `delete` — the same two-step every object store takes, since none offer an atomic move. Moving a key onto itself is a no-op, so the fallback can't copy-then-delete a file out of existence. `move` throws on Convex, where `copy` does (immutable storage ids, no rename). ```ts await files.move("uploads/tmp-abc.png", "avatars/user-123.png"); ``` `FileHandle` gains the matching `moveTo` / `moveFrom`, and `move` fires the lifecycle hooks (`onAction` / `onError` / `onRetry`) with a new `"move"` action type. `files.listAll(options?)` walks every page as an async iterable, following the cursor for you: ```ts for await (const file of files.listAll({ prefix: "avatars/" })) { console.log(file.key, file.size); } ``` `prefix` scopes the walk and `limit` sets the per-page size; each page is a real `list` call, so retries, timeouts, and prefix scoping all apply. Custom adapters can implement an optional `move(from, to, opts?)` to provide a native rename; omitting it keeps the copy + delete fallback. The CLI gains a `move ` command and the MCP server exposes a matching `move` tool. - a96874f: `upload` now accepts a `multipart` option for uploading large bodies in parallel parts. ```ts await files.upload("backups/db.tar", stream, { multipart: true, // or { partSize, concurrency } }); ``` - **S3 and the S3-compatible adapters** (incl. R2 over HTTP) run multipart through `@aws-sdk/lib-storage`, falling back to a single `PutObject` for small bodies. Unknown-length `ReadableStream` bodies now use multipart automatically, even without the flag. - **OneDrive** uploads above its 250 MB simple-upload limit (and any `multipart` request) now go through a chunked upload session instead of throwing — large files just work. - **GCS** and **Firebase Storage** switch to a resumable upload when `multipart` is set; `partSize` maps to the chunk size. - **Azure Blob** maps `partSize`/`concurrency` to its parallel block-upload tuning. - **Dropbox** now streams `ReadableStream` bodies through its upload session chunk-by-chunk instead of buffering the whole file in memory; `partSize` tunes the chunk size (rounded to a 4 MiB multiple). - The array form of `upload` accepts a per-item `multipart` toggle/tuning too. Other adapters already stream natively or only accept a fully-buffered body, so they ignore the option. - 64cf324: `download` now accepts a `range` option for fetching a contiguous byte slice of an object — the primitive behind video seeking and resumable downloads. ```ts // Bytes 0–1023 (end is inclusive, matching the HTTP Range header) → 1024 bytes. const head = await files.download("video.mp4", { range: { start: 0, end: 1023 }, }); // Omit end to read from an offset to EOF — e.g. resume an interrupted download. const rest = await files.download("video.mp4", { range: { start: 1024 } }); ``` Both bounds are 0-based and `end` is inclusive, mirroring the `bytes=start-end` request the supporting adapters issue. The returned `StoredFile` carries just the requested bytes and reports the range length as its `size`. `range` works with `as: "stream"` so you never buffer the whole slice. - **S3 and every S3-compatible adapter** (R2 over HTTP, MinIO, DigitalOcean Spaces, Wasabi, Tigris, Backblaze B2, Storj, Hetzner, Akamai, and the rest of the `s3()` family) issue a ranged `GetObject`. - **Bun S3** slices via `S3File.slice`, **GCS** and **Firebase Storage** via `createReadStream`/`download` byte offsets, **Azure Blob** via its offset/count download, and the **R2 Workers binding** via its native `range` option. - The local **`fs`** adapter reads only the requested bytes off disk, and the in-memory adapter slices its buffer. - The fetch-based adapters — **UploadThing, Box, Vercel Blob (public), Cloudinary, PocketBase, Dropbox, OneDrive, SharePoint, and Google Drive** — send an HTTP `Range` header and verify the host replied `206 Partial Content`, throwing if it ignored the range and returned the whole object (so the bandwidth saving is never silently lost). Adapters whose provider has no range primitive (Supabase, Appwrite, Netlify Blobs, Bunny Storage, Convex, and Vercel Blob private blobs) throw a `FilesError` rather than downloading the whole object and slicing it client-side. Custom adapters opt in by setting `supportsRange: true` and honoring `DownloadOptions.range`; the `Files` wrapper validates the range and gates unsupported adapters before any provider call. - 841175a: `upload` now accepts an `onProgress` callback for reporting realtime progress — e.g. to drive a progress bar. ```ts await files.upload("big.zip", stream, { onProgress: ({ loaded, total }) => console.log( total ? `${Math.round((loaded / total) * 100)}%` : `${loaded} bytes` ), }); ``` Granularity depends on the body and the adapter: - A `ReadableStream` body is reported byte-by-byte on every adapter, as the bytes are consumed (`total` is omitted, since the length is unknown). - A buffered body (`File`, `Blob`, `ArrayBuffer`, `Uint8Array`, `string`) reports `{ loaded: 0, total }` then `{ loaded: total, total }` by default. - Adapters with a native upload-progress hook report true byte-level progress for every body type (buffered included): S3 and the S3-compatible adapters, R2 (HTTP), Azure Blob, Google Cloud Storage, Firebase Storage, Vercel Blob, and FTP. The S3 family uses `@aws-sdk/lib-storage` (a new optional peer dependency loaded only when `onProgress` is used) and also gains multipart for large files; GCS and Firebase Storage switch to a resumable upload when `onProgress` is set. The array form of `upload` accepts `onProgress` too; each report carries the item's `key`. Custom adapters can opt into reporting progress themselves by setting `reportsUploadProgress: true` and calling `opts.onProgress`. ### Patch Changes - 52daa66: Update bundled and peer dependencies. The CLI's `commander` runtime dependency moves to v14. Several optional provider-SDK peer floors are raised to the majors now built and tested against: - `@anthropic-ai/claude-agent-sdk` → `^0.3.0` (claude adapter) - `@googleapis/drive` → `^20.0.0` (google-drive adapter) - `google-auth-library` → `^10.0.0` (gcs / google-drive auth) - `node-appwrite` → `^25.0.0` (appwrite adapter) - `pocketbase` → `^0.27.0` (pocketbase adapter) No public API or behaviour changes. If you use one of the adapters above, upgrade its peer to the new major. - 26989e0: The published package now ships its documentation. The full docs are bundled at `node_modules/files-sdk/docs` (per-adapter pages under `docs/adapters/`, AI tools under `docs/ai/`, plus `overview`, `api`, `cli`, `providers`, and `troubleshooting`), so tools and agents can read version-matched reference material offline instead of relying on the hosted site. - 7027836: In-memory adapter (`files-sdk/memory`): give `metadata` the same value semantics the bytes already have. The adapter cloned an entry's bytes on the way in but stored and returned the `metadata` object by reference, so three aliases leaked: mutating the object passed to `upload()` (or an `initial` seed) after the call reached into the store, mutating a `head()`/`download()` result's `metadata` reached back into the store, and `copy()` left the source and destination sharing one mutable metadata object — mutating one silently changed the other. Metadata is now shallow-cloned on write and on read, so each stored entry owns its own copy and every read hands back a fresh one, matching how a real backend round-trips metadata. Bytes behavior is unchanged. - 293ba1d: `onProgress` is now truly fire-and-forget: a throwing progress reporter can no longer fail or retry the upload it observes. Previously, a buffered upload's final progress report ran inside the retryable attempt, so a throw was caught by the retry layer, mislabelled a provider error, and re-uploaded the body up to `retries` times before rejecting; on the streaming path a throw errored the underlying stream and failed the upload. All three wrapper-driven `onProgress` calls now route through the same swallow-and-ignore guard the `hooks` callbacks use, matching the contract already documented on `FilesHooks` ("a hook that throws can never fail the operation it observes"). Self-reporting adapters (`reportsUploadProgress`) are unaffected — they own their own reporting. - 1b978b9: Fix `signedUploadUrl({ maxSize })` failing with `501 Not Implemented` on Cloudflare R2. The R2 adapter inherited the S3 adapter's behaviour of routing `maxSize` through a presigned `POST` policy (`content-length-range`). Cloudflare R2 does not implement the S3 `POST Object` API, so those uploads failed at upload time with `501 Not Implemented`. R2 now throws a clear `Provider` error when `maxSize` is passed (matching how the Azure and Supabase adapters handle the same limitation), instead of handing back a POST form R2 can't serve. Omit `maxSize` to get a presigned `PUT` URL, and enforce upload caps at your application gateway. Fixes #49. --- # files-sdk@1.7.0 Source: https://files-sdk.dev/changelog/files-sdk-1-7-0 ### Minor Changes - 3c8abf3: Add `sync()` — an incremental, optionally-pruning mirror between two providers. It skips objects already identical at the destination (compare by size + etag, size, or a custom predicate), can prune destination keys the source no longer has (mirror mode), and supports `dryRun` to preview the reconciliation plan. Surfaced at parity as the CLI `sync` command and a write-gated MCP `sync` tool. - d998ef6: Add directory-style listing to `list`: a new `delimiter` option collapses keys into S3-style common prefixes ("folders"), returned in `ListResult.prefixes`. Supported on every adapter with a folder or prefix model — the object stores (S3 family, R2, GCS, Firebase Storage, Azure) and `fs`/memory/FTP/SFTP/Google Drive/Cloudinary accept any delimiter; the folder-based providers (Vercel Blob, Netlify Blobs, Supabase, Dropbox, Box, OneDrive, SharePoint) accept `"/"`. Adapters with no folder concept (UploadThing, Appwrite, PocketBase, Convex, Bun's S3) advertise `supportsDelimiter: false` and throw rather than silently returning a flat list. The CLI and MCP server expose this too: `files list --delimiter /` returns the direct files in `items` and the subfolders in a `prefixes` array, and the MCP `list` tool gains the same `delimiter` argument. Both throw on adapters with no folder concept and reject being combined with `--all` / `all` (which walks the whole tree). - 0345169: Add read-only `Files` instances. Pass `readonly: true` to the constructor, or derive a locked view from an existing client with `files.readonly()`, when a caller should be able to read storage but never mutate it: ```ts const files = new Files({ adapter: s3({ bucket: "uploads" }), readonly: true, }); const readOnly = files.readonly(); // reuses the same adapter, prefix, timeout, retries, and hooks ``` Reads stay available (`download`, `head`, `exists`, `list`, `listAll`, `url`). Every write surface — `upload`, `delete`, `copy`, `move`, `signedUploadUrl`, and the equivalent `file(key)` helpers (`upload`, `delete`, `copyTo`, `copyFrom`, `moveTo`, `moveFrom`, `signedUploadUrl`) — now fails immediately, before the adapter is touched, with a new normalized `FilesError { code: "ReadOnly" }`. The failure is deterministic and is not retried; `onError` and the final `onAction({ status: "error" })` hooks still fire. The `raw` escape hatch is not governed by the guard — code that writes through `files.raw` bypasses it by design. - dbf6ded: `upload` now accepts a `control` option for **pause-able and resumable uploads**. Construct an `UploadControl`, pass it in, and pause, resume, or abort the upload — or persist `control.toJSON()` and resume it later (even in a new process or after a page reload) with `UploadControl.from(token)`. ```ts import { Files, UploadControl } from "files-sdk"; const control = new UploadControl(); const promise = files.upload("big.iso", file, { control, multipart: { partSize: 16 * 1024 * 1024 }, onProgress: ({ loaded, total }) => bar.set(loaded, total), }); control.pause(); // in-flight parts settle, the promise stays pending save(control.toJSON()); // serializable session token — persist anywhere control.resume(); // continue // …or, after a crash / reload, in a new process: const result = await files.upload("big.iso", file, { control: UploadControl.from(load()), }); ``` ### Patch Changes - 1ff2550: Azure gains a native `deleteMany` backed by the Blob Batch API (256 keys per batch, idempotent on already-missing blobs); `stopOnError` falls back to sequential deletes. Previously it fanned out to single deletes. - e1d09a6: Validate Microsoft Graph pagination cursors against the adapter root before following them for OneDrive and SharePoint list calls. - e1d09a6: Cap AI tool download `maxBytes` overrides at 10 MiB and reject oversized values in both schema validation and direct executor calls. - e1d09a6: Bound CLI MCP downloads by checking object metadata and requested byte ranges before transferring response bodies. - e1d09a6: Reject `.` and `..` segments in `Files` prefixes and prefixed keys before resolving local filesystem paths, so prefixed fs adapters cannot escape their configured root. - 1ff2550: FTP & SFTP `move()` now uses a native rename (`RNFR`/`RNTO` and the SFTP `RENAME` op) instead of a copy + delete body round-trip. The destination's parent directory is created first where needed. - 1ff2550: FTP & SFTP now support ranged downloads (`download(key, { range })`): SFTP uses native read-stream `start`/`end` offsets; FTP begins the transfer at the `REST` start offset and trims a bounded `end` client-side. Both adapters now advertise `supportsRange`. - e1d09a6: Start the MCP server in read-only mode by default and require `--allow-writes` before registering mutation tools. - 1ff2550: Gate unsupported `metadata` / `cacheControl` centrally in the `Files` wrapper via new `Adapter.supportsMetadata` / `Adapter.supportsCacheControl` flags — exactly like `supportsRange`. Every adapter is flagged accurately and the per-adapter inline throws (Convex, FTP, SFTP, Dropbox, Box, OneDrive, Cloudinary, Appwrite, PocketBase, Bunny Storage, Bun's S3) are removed in favor of the one gate. **Behavior change:** Vercel Blob (`metadata`), UploadThing (`metadata`/`cacheControl`), and SharePoint (`metadata`/`cacheControl`) previously dropped these options silently and now throw a `FilesError`, matching every other adapter. - 1ff2550: R2 (HTTP) now advertises `supportsRange`, so ranged downloads work in HTTP mode — it delegates to `s3()`, which honors the `Range` request. The R2 Workers binding already supported them. - e1d09a6: Reject `responseContentDisposition` for fs, FTP, and SFTP public URLs because those static URLs cannot bind the override into a signature. - e1d09a6: Reject Azure signed upload `contentType` overrides because Azure SAS URLs do not bind the request Content-Type into the signature. - e1d09a6: Reject Google Drive, OneDrive, and SharePoint signed upload `maxSize` and `minSize` options because their upload sessions cannot enforce a server-side content-length policy. - e1d09a6: Reject relative path segments in OneDrive and SharePoint delegated paths before building Microsoft Graph item URLs, keeping `rootFolderPath` scoped to its configured folder. - e1d09a6: Scope Google Drive virtual-key file ID resolution to `rootFolderId` by including the configured root folder parent in Drive lookup queries. --- # files-sdk@1.8.0 Source: https://files-sdk.dev/changelog/files-sdk-1-8-0 ### Minor Changes - 87607ec: Add a `compression()` plugin at `files-sdk/compression` for transparent, at-rest compression. Bodies are gzipped (or deflate / deflate-raw) on upload with the algorithm and original size recorded in metadata, and decompressed on download (bulk calls too); incompressible data is stored verbatim so storage never grows. Uses only the Compression Streams API — no native dependencies — and works on any adapter that supports metadata. - d2fa5e0: Add a `contentType()` plugin at `files-sdk/content-type` that decides an upload's `Content-Type` from its bytes instead of the client's claim. It magic-byte-sniffs the body on `upload` and either corrects the stored type to match (the default) or rejects a mismatch, so a `.png` whose bytes are really HTML/SVG can't be stored under an image type and served inline. Recognizes the common images, PDF, and — via a leading text scan — HTML, SVG, and XML. It writes no metadata and only reads the first 512 bytes, so known-length bodies are peeked with no copy and streams stay streaming; `signedUploadUrl()` fails closed (a direct client upload bypasses the sniff). Also exports `detectContentType()`. No native dependencies; works on any adapter. - 5ad680e: Add a `dedup()` plugin at `files-sdk/dedup` for content-addressed de-duplication. On `upload` the body is hashed (SHA-256) and its bytes are stored only once at a content-addressed blob under a store prefix (`.dedup/` by default); the logical key holds a tiny pointer to it, so re-uploading content already in the store skips the byte upload, and `copy` / `move` of a de-duplicated file is near-free and shares the blob. Reads are transparent — `download` follows the pointer (ranges included, since blobs are stored verbatim), and `head` / `list` report the logical size with internal fields stripped — for bulk calls too. Uses only the Web Crypto API — no native dependencies — and works on any adapter that supports metadata. It buffers the body to hash it (so it doesn't suit unknown-length streams or resumable uploads), `url()` / `signedUploadUrl()` fail closed, and orphaned blobs aren't garbage-collected. Place it before `compression()` / `encryption()` in the array — encrypted bytes don't de-dup. - feaf806: Add an `encryption()` plugin at `files-sdk/encryption` for provider-agnostic, at-rest envelope encryption. A per-object data key encrypts the body with AES-256-GCM and your master key wraps it into the object's metadata; downloads decrypt transparently (bulk calls too). Uses only the Web Crypto API — no native dependencies — and works on any adapter that supports metadata. Also exports `generateEncryptionKey()`. - 4d40229: Add a `files.search(pattern, options?)` method that finds objects whose key matches a pattern. By default `pattern` is a standard glob (powered by picomatch: `*` within a path segment, `**` globstar across segments, `?`, `[a-z]` classes, `{a,b}` braces, `!` negation; a glob with no wildcards is an exact match); set `match` to `"regex"`, `"substring"`, or `"exact"`, or pass a `RegExp` directly, to change that. It returns a streaming async iterable of `StoredFile` built on `listAll`, so it walks every page lazily (stays memory-bounded, `break` or `maxResults` to stop early) and works on every adapter with no per-provider capability. A glob's literal prefix is pushed down to the underlying `list` automatically (`uploads/2024/*.pdf` scopes the walk to the `uploads/2024` prefix); for a regex/substring/case-insensitive search, pass `prefix` to bound the walk. The CLI gains a `files search ` command (`--match`/`--regex`/`--prefix`/`--limit`/`--max-results`/`--case-insensitive`) and the MCP server a `search` tool. - 3a42a18: Add an opt-in plugin system to `Files`. Plugins wrap every operation in an ordered onion — they can transform, veto, or observe (the interceptable superset of `hooks`) — and can contribute new namespaced surface. ```ts const files = createFiles({ adapter: s3({ bucket: "uploads" }), plugins: [ { name: "uppercase", wrap: handlers({ upload: (op, next) => next({ ...op, body: (op.body as string).toUpperCase() }), }), }, ], }); ``` Each plugin offers two optional capabilities: `wrap` (intercept any operation via the `next` onion) and `extend` (add methods like `files.usage()`). Ships with the `handlers()` helper for authoring per-verb `wrap`s with automatic passthrough, and the `createFiles()` factory that surfaces `extend` methods on the instance type. Plugins run inside the `onAction`/`onError` hooks but outside retry and key prefixing, and intercept both single and bulk operations. - 79e0104: Add a `tracing()` plugin at `files-sdk/tracing` for OpenTelemetry spans around every operation. Each call opens one span named `files.` carrying the caller-facing key (or `from` / `to` for `copy` / `move`), a `files.bulk` flag for batch items, and a cheap result attribute on success (`files.size`, `files.exists`, `files.count`); a throw is recorded with `recordException` and an `ERROR` status, then re-thrown untouched. Spans are opened with `startActiveSpan`, so each op span nests under your active request span and the sub-operations inner plugins issue nest beneath it in turn. `@opentelemetry/api` is an **optional peer dependency**: the tracer defaults to the global `trace.getTracer("files-sdk")` (a no-op until you register an OpenTelemetry SDK), or pass your own to scope the instrumentation name/version. Tune span names with `spanPrefix` and attach or redact attributes with `attributes(op)` (return `{ "files.key": undefined }` to keep sensitive keys out of traces). It's body-transparent (sizes come from declared metadata, never the bytes, so streaming / ranges / `url()` keep working), counts one span per logical operation rather than per retry attempt, and opens a span per item of a bulk call. Place it first (outermost) to span the caller-facing operation with inner-plugin work nested beneath, or last to time only the provider call. - 60f3b63: Add a `usage()` plugin at `files-sdk/usage` for metering storage, bandwidth, and operation counts. It tallies every operation on a `Files` instance and surfaces the running totals via `files.usage()`: each call counts as one operation (with a per-verb `operationsByKind` breakdown), `upload` adds its result size to `bytesUp`, and `download` / `head` wrap the returned body so the bytes you actually read add to `bytesDown` — metered lazily, chunk-by-chunk, so an unread body costs nothing and a fire-and-forget hook couldn't do it. Pass `{ group }` to bucket usage per tenant or prefix and read it back with `usageByGroup()`; `resetUsage()` starts a fresh window. It's body-transparent (no buffering, no metadata, no native deps, so streaming / ranges / `url()` keep working), counts logical operations rather than retry attempts, and counts each item of a bulk call. Place it first (outermost) to meter logical bytes and caller-facing operations, or last to meter bytes-on-the-wire to the provider. Construct with `createFiles` so `files.usage()` shows up on the type. - 8c68c34: Add a `validation()` plugin at `files-sdk/validation` — a fail-closed guard that vets writes before any bytes reach the adapter. Enforce a max/min size, an allowed-MIME-type list (exact or `type/*`), and a key-naming rule (a `RegExp` or predicate); the key rule also guards the destination of `copy`/`move`. It transforms nothing and stores no metadata, so reads, `url()`, `copy`, and `move` pass straight through, while `signedUploadUrl()` fails closed when a size or type rule is set (a presigned upload bypasses the plugin). No native dependencies; works on any adapter. - 3cecd4c: Add a `versioning()` plugin at `files-sdk/versioning` that snapshots an object's prior bytes before any overwrite or delete and adds `files.versions(key)` / `files.restore(key, versionId?)` to roll a key back. Snapshots are server-side copies under a configurable prefix (`.versions/` by default), so it's body-transparent — streaming, range downloads, `url()`, and `signedUploadUrl()` keep working, and it composes with `compression()` / `encryption()` by snapshotting whatever they stored. Optional `limit` caps the versions kept per key; version objects are hidden from `list()`. It's the first plugin to use `extend`, so use `createFiles` to surface the new methods on the type. No native dependencies; works on any adapter. ### Patch Changes - 5ad680e: Fix plugin cross-kind re-routing inside bulk operations. A plugin whose `wrap` calls `next()` with a different verb than the one it's intercepting — e.g. `dedup()`'s `exists` probe, or `versioning()`'s snapshot `head` + `copy` — misrouted when it ran inside `upload([...])` / `download([...])` / `head([...])` / `exists([...])` / `delete([...])`, because each bulk item was dispatched with a base locked to that one verb. The bulk bases now delegate any re-routed, cross-kind sub-op to the single-operation path, so it behaves identically in a bulk call as in a single one; the item's own verb keeps its retry-free, hook-quiet semantics. - 0f3771e: Switch the build from tsup to Bun's bundler (for JavaScript) plus tsgo (for type declarations), orchestrated by `scripts/build.ts`. tsup is no longer maintained and its declaration emit needed an enlarged Node heap; the replacement builds the whole package — every adapter, plugin, and the CLI — in well under a second with no heap flag. The published ESM output and `exports` map are unchanged, so imports resolve identically. The only packaging difference is that type declarations are now emitted per source file rather than rolled up into bundled `.d.ts` files; type resolution for consumers is equivalent. --- # files-sdk@1.9.0 Source: https://files-sdk.dev/changelog/files-sdk-1-9-0 ### Minor Changes - ff814cc: Add an `audit()` plugin at `files-sdk/audit` that writes a structured who/what/when record of every mutation to an **awaited** sink — the durable, awaitable counterpart to the fire-and-forget `onAction` hook. Each audited operation produces one `AuditRecord` carrying the verb, the caller-facing key (or `from` / `to`), an optional `actor`, the start time and duration, the outcome, and — on a successful `upload` — the stored size. Because the sink is awaited, the operation doesn't resolve until the record is written, giving you ordering and back-pressure a hook can't: on a successful operation a rejecting sink fails the call (the mutation happened but wasn't recorded — fail closed), while on a failed operation the operation's own error always wins so a sink problem can never mask why the call failed. By default it records the mutating verbs (`upload`, `delete`, `copy`, `move`, `signedUploadUrl`); pass `events: "all"` to also audit reads, or an explicit list to record exactly the verbs you name. Resolve `actor` synchronously from your request context to attribute each record. It's body-transparent (never buffers, transforms, or reads the body — `size` comes from declared metadata), writes no object metadata, and has no native dependencies, so it works on any adapter. Plugins run outside retries (so a retried call is still one record) on caller-facing keys; bulk `upload([...])` / `delete([...])` fan out to one record per item, each flagged `bulk: true`. It's `wrap`-only, so plain `new Files({ plugins })` works. Place it first (outermost) so it records the caller's logical intent — a `delete` an inner `softDelete()` turns into a `move` is still audited as the `delete` the caller asked for. - daca585: Add a `cache()` plugin at `files-sdk/cache` — an LRU/KV cache in front of the cheap read verbs. A repeat `head()` or `url()` (and, opt-in, a small `download()`) for an unchanged key is served from memory instead of round-tripping to the provider; any write through the instance (`upload`, `delete`, `copy`, `move`) invalidates the affected key so the next read re-fetches. `head` caches metadata only (a hit's body still lazy-fetches on access, matching the uncached `head` contract); `url` caches per url-options signature and **caps each entry at its own `expiresIn`** so a presigned URL is never handed out past its signature; `download` is off by default and, when enabled via `operations: ["download"]`, buffers only **known-length bodies at or under `maxBytes`** (default 1 MiB) so streaming and large objects keep working. Defaults to a bounded in-memory LRU (`maxEntries`, default 1000), or pass your own `CacheStore` to back it with a shared KV. Entries honor a `ttl` (default 60s; `0` disables time-based expiry). It writes **no object metadata** and has **no native dependencies**, so it works on any adapter, and runs **outside** retries so a hit skips the retry loop entirely. It uses `extend` for `invalidateCache(key?)`, `cacheStats()`, and `resetCacheStats()` — construct with `createFiles` to surface them on the type. Place it **first** (outermost) so a hit short-circuits before the rest of the pipeline does any work; writes made out-of-band (a presigned-URL upload, or a change straight against the provider) won't invalidate, so call `invalidateCache()` and treat the cache as eventually-consistent. - 83d6eb4: Add a `failover()` plugin at `files-sdk/failover` that reads/writes the primary and falls back to one or more secondary adapters when a backend is down — a live, per-operation failover chain. The **primary** is the instance's own adapter (reached through the rest of the onion, so it keeps retry and prefixing); the **secondaries** are backup adapters passed in `secondaries` (a single `Adapter` or an array for a multi-region chain), each wrapped in its own internal `Files` so it gets the same retry, capability gating, and `StoredFile` normalization. Every verb runs the same way: try the primary; if it throws and `shouldFailover` says so, try the next backend, and so on — the first to succeed wins, and if the chain is exhausted the last error is thrown. The default predicate fails over **only** on `Provider` errors (network / timeout / 5xx — "the backend is down") and never on an aborted request or a definitive answer from a healthy backend (`NotFound`, `Unauthorized`, …), so a genuine 404 stays a 404 instead of being masked by a replica; pass your own `shouldFailover` to widen it (e.g. read through to a replica on `NotFound`) or narrow it. This is the **availability** counterpart to `tiering()` (which _partitions_ by key/size): failover treats each secondary as a full replica, so it never splits or merges across backends — `list` returns the first reachable backend's page (not a merged one), and writes land on the first reachable backend rather than fanning out to all (that's `replication()`). A streaming `upload` (a `ReadableStream` body) can't be replayed, so it runs against the primary alone and isn't failed over. An optional `onFailover` callback (fire-and-forget; a throw from it is swallowed) reports each fail over with the operation and the backend indices, for metrics / alerting. It's body-transparent, has no native dependencies, and adds no surface (`wrap` only), so it works with plain `new Files({ plugins })`. Place it last (innermost) so body-transforming plugins like `encryption()` wrap every backend, and give each secondary its own bucket / container (secondaries receive caller-facing keys, without the instance `prefix`). Failover buys availability, not convergence — reconcile a secondary written during an outage with `sync` / `transfer`, or keep it current with `replication()`. - 581c97f: Add a queryable `files.capabilities` surface that reports what the underlying adapter can do, so callers, AI tool wrappers, and validators can branch up front instead of relying on a throw at call time. It returns an `AdapterCapabilities` snapshot with eight fields, each mirroring an operation the unified API actually exposes: `rangeRead`, `uploadProgress`, `delimiter`, `metadata`, `cacheControl`, and `multipart` are derived live from the same per-adapter flags and optional methods the wrapper already gates on (so they can never drift from runtime behavior), while `serverSideCopy` and `signedUrl` (`{ supported; maxExpiresIn? }`) are declared per-adapter and default to the conservative value when unset — a caller that doesn't advertise reads as "no", never a wrong "yes". `signedUrl.supported` is `true` when `url()` can mint a signed or tokenized URL (not just a permanent public link); `maxExpiresIn` is set only where a provider enforces a hard `expiresIn` ceiling in code (e.g. Dropbox's 4-hour temporary links), not for soft infra limits or config-dependent caps. Custom adapters can set the new optional `supportsServerSideCopy` and `signedUrl` fields alongside the existing `supports*` flags; both are advisory and gate nothing. See the new Capabilities and Provider gaps documentation. - 81e0e64: Add a `neon` adapter at `files-sdk/neon` for [Neon](https://neon.com) branchable object storage over its S3-compatible API. A thin wrapper around the S3 adapter — errors relabelled, with path-style addressing on by default because Neon requires it (the wildcard TLS cert covers a single subdomain level, occupied by the branch id, so the bucket name travels in the request path). It reads the standard `AWS_*` variables that `neon dev` / `neon env pull` inject for the linked branch — `endpoint` from `AWS_ENDPOINT_URL_S3`, region from `AWS_REGION` (then `NEON_STORAGE_REGION`, then `us-east-1`), and credentials through the AWS SDK chain (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`) — so inside a Neon Function or after an env pull it works from env alone: `neon({ bucket: "images" })`. Catalogued in `files-sdk/providers` and exposed through the CLI. - 2275982: Add an opt-in `receipts` option that surfaces a provenance `Receipt` for each mutating call (`upload`, `delete`, `copy`, `move`) — built for AI tool wrappers and agents that need to attest "this exact content landed at this key". It's **off by default**: an instance without the option records nothing and hashes nothing, so existing behavior is unchanged. Turn it on with `receipts: true` to attach a `Receipt` (`{ op, provider, key, bytes?, etag?, sha256?, durationMs, ts }`) to the success `onAction` event of each mutating call — an additive `receipt` field on the existing hook, with no new operation, callback, or changed return type. Every field except `sha256` is derived from the work the SDK already does for the hook (timing, the adapter name, the caller-facing key, and `bytes` / `etag` read straight off the `UploadResult`), so plain `receipts: true` adds no per-call cost. `sha256` is the one field with a real per-call cost and is opt-in by name: pass `receipts: { sha256: true }` to fingerprint the upload body as passed to `upload()` — taken before any plugin transform, so it matches what `download` gives back rather than the (possibly encrypted/compressed) bytes on disk — a lowercase-hex SHA-256, present only on an `upload` of a buffered body. A streaming upload is never buffered to hash it (so it carries no fingerprint), and `delete` / `copy` / `move` transfer no content of their own; with `sha256` off, the body is never read. Reads, `signedUploadUrl`, failures, bulk array calls, and receipts-off instances all leave `event.receipt` unset. See the new Receipts documentation. - 5a77a58: Add a `signedUrlPolicy()` plugin at `files-sdk/signed-url-policy` that enforces safe defaults on the two URL-minting operations, turning the security caveats `url()` and `signedUploadUrl()` document into the default. On `url()` it forces a download `Content-Disposition` (default `"attachment"`, so user-uploaded HTML/SVG can't execute inline at your origin — the stored-XSS warning made a default) while preserving a caller's existing `attachment` (and its `filename`), and clamps `expiresIn` to `maxExpiresIn`. On `signedUploadUrl()` it clamps `expiresIn` to the same cap and, when `maxUploadSize` is set, guarantees a server-enforced `maxSize` is always present (injected when absent, clamped when over) — so an adapter that can't bind a size limit fails closed loudly instead of minting an unbounded URL. It writes no metadata, transforms nothing on disk, never throws of its own accord, and lets every other verb pass straight through; with no options set it still applies the headline default (`url()` forces `attachment`). Set `disposition: false` to opt out of the disposition guard. Place it first (outermost) so it sees the caller's original request and its rewritten options reach the signing adapter. - ae58680: Add a `softDelete()` plugin at `files-sdk/soft-delete` that turns `delete` into a recoverable move into a trash prefix - a recycle bin for any adapter. Instead of destroying an object, a `delete` server-side moves it to `"/"` (`.trash/` by default); the bytes only leave storage when you `purge()`. It adds three methods via `extend` (so construct with `createFiles`): `trashed()` lists what's in the trash (each entry carries the original `key` plus a downloadable `trashKey`), `restore(key)` moves the trashed copy back over the live key (overwriting a re-created one, throwing when nothing's trashed), and `purge(key?)` permanently deletes one item or empties the whole trash (idempotent). Like `versioning()` it's body-transparent - it never buffers, transforms, or reads the body, so streaming, range downloads, `url()`, and `signedUploadUrl()` all keep working - and has no native dependencies. Trashed objects are hidden from `list()` (unless you list within the prefix); a `delete` of a key inside the trash prefix is a real delete (that's how `purge()` works); deleting a missing key stays a no-op; and bulk `delete([...])` soft-deletes every key. One trashed copy is kept per key (re-deleting replaces it - reach for `versioning()` to keep every generation). Place it first (outermost) so it relocates whatever the rest of the pipeline stored. - ce69a47: Add a `tiering()` plugin at `files-sdk/tiering` that routes operations between a hot and a cold adapter by size, prefix, or age. The **hot** tier is the instance's own adapter (reached through the rest of the onion); the **cold** tier is a second adapter passed in `cold` (wrapped in its own internal `Files`, so it gets the same retry, capability gating, and `StoredFile` normalization). A required `route({ key, size? })` function decides each operation's tier — `size` is the body's declared length on `upload` (when known), and omitted everywhere else. `upload` lands in the routed tier; `download` / `head` / `url` / `exists` consult it; `delete` removes it; `copy` / `move` locate the source, route the destination by key, and either use a native same-tier op or stream the bytes across when the tiers differ; `list` merges a page from each tier (keys sorted within a page) and paginates the two independently via a composite cursor; `signedUploadUrl` signs against the routed tier. With `fallback: true`, an object's tier is treated as discoverable rather than fixed — reads fall through to the other tier on a miss, `delete` clears both, and an `upload` evicts the other tier so exactly one copy exists; turn it on for `size`-based routing or when you move objects with the new methods. It adds two methods via `extend` (so construct with `createFiles`): `tierOf(key)` reports which tier holds a key, and `tier(key, target)` streams an object across tiers (the lever for age-based transitions — list, check `lastModified`, then tier it down). It's body-transparent (a cross-tier copy streams, never buffers) and has no native dependencies. Place it last (innermost) so body-transforming plugins like `encryption()` wrap both tiers, and address objects by caller-facing keys (the cold adapter doesn't receive the instance `prefix` — give it its own bucket / container). - 649ac09: The `validation()` plugin now throws a dedicated `ValidationError` (exported from `files-sdk/validation` along with the `ValidationReason` type) with a `reason` discriminant — `"size"`, `"type"`, or `"key"` — so callers can branch on which rule failed without parsing the message. It's backward compatible: `ValidationError extends FilesError`, keeps `code: "Provider"`, and the messages are unchanged, so existing catches keep working. `maxSize`/`minSize` share `reason: "size"` (the message says which bound), and the `signedUploadUrl()` fail-closed throw stays a plain `FilesError` — it's the plugin refusing an unenforceable operation, not the file failing a rule. - 6aca1e5: Add a `zip()` plugin at `files-sdk/zip` for bundling stored objects into ZIP archives and back out of them. An `extend`-only (Tier C) plugin contributing three methods: `files.zip(selection)` streams many keys as one standard ZIP archive (entries download lazily one at a time, so memory stays flat — pipe it straight into a `Response`), `files.zipTo(key, selection)` stores that archive back as an object, and `files.unzip(key, { into })` extracts an archive's entries into individual objects with content types inferred from their extensions. A selection is an explicit key array or `{ prefix }` (resolved via `listAll`); `method: "store" | "deflate"` picks the compression (deflate via the platform `CompressionStream` — no native deps, works on any adapter) and `name(key)` remaps entry paths. Everything runs through the fully-wrapped instance, so it composes with `encryption()` / `compression()` transparently. Classic ZIP only (no ZIP64: 65,535 entries / 4 GiB caps fail closed), entry names are validated on both sides (duplicates, `..` zip-slip segments, absolute paths), extraction verifies CRC-32/size and refuses encrypted entries and unknown methods, and `unzip` buffers the whole archive (the central directory lives at the end) while `zip` streams. ### Patch Changes - 53da200: Document why the Azure resumable-upload probe is safe to treat staged (uncommitted) blocks as skippable: blocks Azure garbage-collects before finalization make `commitBlockList` fail loudly (`InvalidBlockList`) rather than committing with gaps, and a retry re-probes correctly. Comment-only; no behavior change. - bcad8b4: Fix untyped `Blob`/`File` uploads being sent with an empty `Content-Type`. `Blob.type` is `""` (never nullish) when no type was given, so the documented `application/octet-stream` fallback behind a `??` was dead code — the provider received `contentType: ""`. Fixed in the core body normalizer and the same pattern in the box, onedrive, supabase, google-drive, dropbox, r2, uploadthing, and convex adapters. - 77f6bc6: Fix the array forms of `download`/`head`/`exists`/`delete` ignoring the constructor-level `signal` and `timeout` defaults. The bulk bases call the adapter directly to stay retry-free (as documented), but that also skipped the instance-wide abort signal and timeout — aborting the constructor signal mid-bulk cancelled nothing, and a configured `timeout` never bounded bulk reads or deletes (bulk upload already honored both). Bulk per-item calls now run under the same signal/timeout plumbing as single operations, still without retries. - 15567cf: Fix the bulk worker pool dying on a sparse/`undefined` array slot. The per-worker guard `return`ed instead of skipping the slot, so with `concurrency: 1` (or as many holes as workers) every key after the hole was silently neither processed nor reported in `results`/`errors`. Only reachable past the type system (a sparse array or an `undefined` element cast in), but the recovery is now to skip just that slot. - 56965b4: Fix `cache()` serving presigned URLs past their signature when `url()` is called without `expiresIn`. The signature-lifetime cap only applied when the caller passed `expiresIn`, but the adapter signs default calls with a finite lifetime too — so with a long `ttl` (or `ttl: 0`, which disables time-based expiry entirely) the cache kept handing out dead links indefinitely. Entries for default-signed URLs are now capped at the assumed signature lifetime, configurable via the new `defaultUrlExpiresIn` cache option (defaults to the SDK-wide 3600s; set it to match your adapter if you changed its default). - 0d45bb7: Fix CLI and MCP output of bulk partial-failure errors. The `errors` arrays embed live `FilesError` instances, and a bare `JSON.stringify` drops `message` (a non-enumerable `Error` property) while serializing the enumerable `cause` — the raw provider error, which can carry request ids and response headers the SDK explicitly warns against shipping across a trust boundary. All CLI/MCP serialization now goes through a replacer that emits `{ code, message, aborted, timedOut }` for any embedded `FilesError` and strips `cause`. - 30f75cc: Fix the CLI truncating piped output on partial failures. Commands called `process.exit()` immediately after writing the structured result to stdout, and POSIX pipe writes are asynchronous — a large payload (e.g. a bulk `head` with errors) could be cut off mid-JSON before the consumer received it. Commands now signal failure via `process.exitCode` and let the process end once stdout drains. - 74a1226: Fix the CLI eagerly importing optional provider peer dependencies. The bundler previously inlined the registry's lazily-imported provider modules into `dist/cli/index.js`, hoisting their external imports (e.g. `@netlify/blobs`) to the top level — so `files --help` crashed with `ERR_MODULE_NOT_FOUND` unless every optional peer was installed. The build now emits shared chunks so the registry's `await import(...)` calls stay genuinely dynamic: provider-independent commands run without any optional peers installed, and a missing peer only surfaces when its provider is actually selected. - 4c66027: Fix the CLI blaming `--config-json` for malformed JSON passed to `transfer --to` / `sync --to`. The shared JSON parser hardcoded the flag name in its error message; it now names the flag the user actually passed. - ee52de2: Fix the CLI silently swallowing non-EPIPE stdout errors. The EPIPE-as-success handler (for `files … | head`-style pipelines), by being registered, also suppressed Node's default throw for every other stdout error — an EIO/EBADF or a full disk behind a redirect let the command exit 0 having written nothing. Non-EPIPE stdout errors now report to stderr and exit 2. - 9cd61f4: Fix CLI integer flags silently truncating trailing garbage. `--part-size 5MB` parsed to 5 bytes, `--timeout 1s` to 1 millisecond, and `--limit 1.9` to 1 — `parseInt` only rejected fully non-numeric input. Integer flags now require a plain integer and fail loudly otherwise. - 3584ab9: Fix `contentType()` leaving the caller's source stream locked and open when a stream upload is rejected. With `onMismatch: "reject"` / `onUnknown: "reject"` (or any downstream failure before the replay body was consumed), the peek reader held its lock forever and the underlying request body / file handle was never cancelled. The replay body is now cancelled best-effort when the upload throws. - e904016: Correct the Dropbox adapter's `expiresIn` documentation. `filesGetTemporaryLink` takes no expiry parameter — every temporary link lives ~4 hours regardless of what's requested — but the docs claimed `expiresIn` was "honored up to the 4h cap". It is validated only (values above 14400s throw); a shorter `expiresIn` is accepted but the link outlives it, so it must not be relied on as a security control with this adapter. - 024946a: Harden `encryption()` against envelope-metadata tampering and document its threat model precisely. GCM already authenticates the ciphertext and the wrapped DEK, but `fsenc_size` — the declared plaintext size that `head()`/`list()` report — is plain metadata an attacker with raw provider write access could forge; `download()` now verifies it against the decrypted length and throws on a mismatch. The JSDoc now also states explicitly that the envelope is not bound to its object key (an attacker with raw provider write access can splice a whole envelope onto another key): binding to keys would break the documented server-side `copy`/`move` and key-aliasing plugin compositions, so tenants needing that isolation should use separate KEKs. - fcc252e: Fix `failover()` never failing over on timeouts. The docs promised the default predicate covers "network failures, timeouts, and 5xx", but a per-attempt `timeout` surfaces as an `aborted` error, which the predicate excluded — so a hung primary (the canonical case the plugin exists for) surfaced the timeout instead of trying the secondary. `FilesError` now carries a `timedOut` flag (set only by the configured `timeout`, never by a caller's abort signal), and the default predicate fails over on timeouts while still respecting deliberate caller aborts. - 781aecc: Harden the fs adapter's resumable-upload `adopt()` against doctored resume tokens. The persisted token's `tempPath` was adopted verbatim, so a tampered token (e.g. one stored in Redis/a DB and rehydrated via `UploadControl.from`) could point the partial-file writes, the completing rename, and the discard delete at an arbitrary filesystem path outside the adapter root. The temp path is fully derived from the traversal-checked key, so it is now recomputed and a token whose `tempPath` doesn't match is rejected — which also catches tokens minted against a different adapter root. - 9188022: Fix silent file corruption in FTP/SFTP resumable uploads when a chunk is retried. `uploadAt()` appended at the server-side EOF without consulting the chunk's `offset`, so a per-chunk retry after a partial append — or after a lost success reply — appended the chunk again, leaving duplicated bytes in the middle of the file while the upload "succeeded". The drivers now verify the remote size matches the expected offset before appending and, on a mismatch, skip the write and report the server's real offset so the orchestrator re-slices from there. - c4b1426: Fix the Google Drive adapter creating a duplicate file on every overwrite. Drive has no unique-name constraint and the adapter always called `files.create`, so uploading an existing key a second time left two files carrying the same virtual key — from then on every `head`/`download`/`delete`/`url` on that key from a fresh instance threw `Conflict` (the writer's own id cache masked it). Writes now look the key up first: `upload()` updates the existing file in place, `copy()` deletes the clobbered destination file after a successful copy, and resumable uploads / `signedUploadUrl()` initiate `PATCH` update sessions against the existing file id instead of creating a new one. - 9ea505f: Stop retrying deterministic failures. The "server ignored the requested byte range" and "only supports the / delimiter" guards throw `Provider`-coded errors from inside the retryable adapter call, and `Provider` was the one code the retry loop treats as transient — so a ranged `download()` with retries against a host that ignores `Range` re-issued (and re-transferred) the full GET on every attempt with backoff in between before surfacing the error. `FilesError` now carries a `permanent` flag that opts a deterministic failure out of retries, set by both guards. - 3ade008: Fix the offset-HTTP resumable driver (GCS/Firebase/Google Drive) optimistically advancing past a chunk on a `308` response with no `Range` header. In this protocol that response means the server persisted nothing (the probe path already maps it to offset 0), so assuming the whole chunk landed silently skipped its bytes and made the upload fail later at a confusing offset. The chunk now throws a retryable error instead, so the per-chunk retry re-sends it and a token resume re-probes the true offset. - d99e757: Fix `control.abort()` racing session creation in resumable uploads. Aborting while `driver.begin()` (or a resume `probe()`) was in flight found no discard hook installed yet, so the just-created provider-side session (e.g. an S3 multipart upload, billed until aborted) was never discarded — and the session assignment then re-populated a live token onto the aborted control, violating `abort()`'s terminal contract. The orchestrator now notices the abort right after session setup, discards the provider session, and keeps the control terminal. - 3ade008: Fix `onProgress` reporting `loaded: Number.MAX_SAFE_INTEGER` when a resumed offset-mode session had already finalized server-side. The probe signals "already done" with a past-the-end sentinel offset, which the orchestrator forwarded verbatim to progress reporting — any UI computing `loaded / total` showed a ~9·10¹⁵-byte upload. The orchestrator now clamps the starting offset to the body size; the upload still completes with the probed result as before. - 7b7c731: Fix multipart resumable uploads continuing in the background after a part fails. When one part exhausted its retries, `upload()` rejected but the sibling workers kept slicing and uploading every remaining part (burning bandwidth and provider requests), `onProgress` kept firing after rejection, the pause gate flipped the control's status from `"error"` back to `"uploading"`, and a later `resume()` could wake paused workers into the dead run. A part failure now latches the run: new dispatches stop, in-flight sibling attempts are aborted via a run-scoped signal, parked workers wake up and bail, and the control's status stays `"error"`. - ea7051e: Fix `softDelete()` dropping the caller's operation options on the trash move. A `signal`/`timeout`/`retries` passed to `files.delete(key, opts)` was silently ignored for the re-routed move, making the delete un-abortable and unbounded. The options now thread through. - d0061bb: Fix the Supabase adapter passing `responseContentDisposition` straight through as Supabase's `download` filename. Supabase's `download: string` option means "attachment **named** this", so `responseContentDisposition: "attachment"` served a file literally named `attachment`, and a full `attachment; filename="report.pdf"` value produced a garbled filename embedding the whole header. Bare `attachment` now maps to `download: true`, a `filename=` parameter maps to that name, and dispositions Supabase can't express (e.g. `inline`) throw instead of being mislabeled. - 2e4d2e2: Fix the Supabase adapter's flat `list()` missing nested objects. The no-delimiter path used the legacy V1 `list()` API, which is folder-scoped and non-recursive — a bucket with nested keys (`docs/a.txt`) listed phantom zero-byte rows for the folders and never returned the nested objects, so `listAll`, `search()`, `sync`/`transfer`, and every list-based plugin silently missed them; a partial prefix (`prefix: "do"`) returned nothing at all. The flat path now uses the V2 list API: a recursive string-prefix scan over full keys with a real server cursor. Note that flat-list cursors are now opaque V2 cursors rather than numeric offsets — don't persist cursors across versions. - 2b8780f: Fix `tiering()`'s `tierOf()`/`tier()` ignoring the instance `prefix`. The extend methods built their hot-tier runner from the bare adapter, while every other operation goes through the plugin chain and gets the prefix applied — so with `prefix` set, `files.exists(key)` was `true` but `files.tierOf(key)` returned `undefined`, and `files.tier(key, …)` threw `NotFound` (or touched a same-named unprefixed object). The extend runner now re-applies the prefix. `Files` also gains a public `prefix` getter so plugins can do the same. - 9235eab: Fix `tiering()`'s merged `list()` emitting a both-tier key twice across pages. The "hot wins" dedup was per page while the two tiers paginate independently, so a key present in both tiers (exactly the stale-shadow state `fallback` mode anticipates after a crash mid-eviction) appeared twice — with potentially different sizes/etags — once each tier's stream reached it, breaking `listAll`/`sync`/`search` consumers. Merged listing is now globally key-ordered: each page emits entries only up to the lowest page boundary among tiers that still have more, holding the rest back via a `skip` marker in the composite cursor, which makes cross-page duplicates (of keys and of delimiter prefixes) impossible. An undecodable composite cursor now throws instead of silently restarting the listing from the top. Composite cursors changed shape — don't carry a list cursor across versions. - dac57c2: Fix `transfer()` and `sync()` leaking the source download stream when the destination upload fails. A destination that rejects before draining the body (auth error, rejected metadata, a fail-closed plugin) left the already-opened source stream — an HTTP response or file descriptor — neither drained nor cancelled, leaking one per failed key on a large walk. The stream is now cancelled best-effort before the per-key error is recorded. - bec8e9f: Correct the UploadThing adapter's `copy()` documentation: it claimed the re-upload streams without buffering, but `uploadFiles` requires a Blob, so the body is fully buffered in memory — exactly the multi-GB scenario the comment claimed to protect against. The comment now states the real behavior and its memory implications. No behavior change. - f390c80: Fix `usage()` miscounting `bytesDown` for buffer-backed bodies read via `stream()`. The wrapper eagerly marked `stream()` as counted, which only holds for read-once stream sources — buffer-backed files (the memory adapter, or anything a transforming plugin buffered) have a repeatable `stream()`, so reading one twice double-counted, and opening a stream without reading it zeroed out the count of a later `text()`/`arrayBuffer()` that actually moved the bytes. The count is now claimed by the first read channel that actually moves bytes, at most once per body. - c095770: Fix a nested-key collision in the `versioning()` plugin's version store. `a`'s version directory (`.versions/a/`) is a prefix of `a/b`'s (`.versions/a/b/`), so `versions("a")` reported `a/b`'s snapshots as versions of `a`, `restore("a")` could silently overwrite `a` with `a/b`'s old bytes, and pruning `a` could delete `a`'s only snapshot while counting `a/b`'s against the limit. Version ids never contain `/`, so listings now ignore anything deeper than the key's own directory, and `restore()` rejects a `versionId` containing `/`. The on-disk layout is unchanged — existing version stores keep working. - 9055fba: Fix `versioning()`'s prune reading only the first list page. Once a key's history exceeded one provider page, `items.length <= max` could be satisfied by a partial page and pruning was skipped or under-counted, so the configured `limit` wasn't enforced promptly. Prune now paginates the version directory to exhaustion, like `versions()` does. - ce0c3f5: Fix an off-by-one in the `zip()` plugin's classic-format limits. The writer accepted exactly 65,535 entries and sizes/offsets of exactly `0xFFFFFFFF` — but those are the ZIP64 sentinel values, which the plugin's own `unzip()` (and any ZIP64-aware reader) treats as "the real value lives in a ZIP64 record", so such an archive couldn't be read back. The limit checks are now `>=`, refusing the sentinel values themselves. --- # files-sdk@2.0.0 Source: https://files-sdk.dev/changelog/files-sdk-2-0-0 ### Major Changes - 2b81046: Release **files-sdk 2.0** — the full-stack release. Alongside the core `Files` API, the SDK now reaches the browser with framework client bindings (`files-sdk/react`, `files-sdk/vue`, `files-sdk/svelte`), a server gateway (`files-sdk/api`) with route handlers for Next.js, Hono, Express, Fastify, Koa, Elysia, Nitro, SvelteKit, Astro, Bun, and Deno, and a shadcn UI component registry wired to the `useFiles` hook. See the accompanying changesets for the full surface. ### Minor Changes - 58757d7: Add an Archil adapter (`files-sdk/archil`) for [Archil](https://archil.com) disks over their S3-compatible API. The disk id is the path-style bucket and the endpoint is derived from the Archil region; SigV4 enables byte ranges, multipart, and presigned URLs. Supports a `branch` option (branch-scoped access) and an optional `disk` instance exposed at `adapter.disk` for Archil-native operations. - 31c9c3f: Add `files-sdk/api` — `createFilesRouter`, a server gateway exposing the whole `Files` verb set (upload, download, head, exists, list, search, url, delete, copy, move, capabilities, signed upload URLs) over a single endpoint, with deny-by-default per-operation `authorize` (throw to deny, return a key-prefix/expiry/read-only constraint), redirect-or-proxy streaming downloads (Range/206 + client-disconnect abort), keyless presign→complete uploads with a proxy fallback, HMAC round-trip tokens, and an origin allowlist. - 91d20ef: Add `files-sdk/astro` — `createRouteHandler(router)` returns `{ GET, POST, PUT }` for an Astro endpoint (`GET` serves downloads, `POST` the JSON verbs, `PUT` the upload byte path). The handlers are Web-native, so the route runs on Node and edge adapters alike. The endpoint must run per-request: set `prerender = false` (or `output: "server"`) with an SSR adapter. - 31c9c3f: Add `files-sdk/client` — `createFilesClient`, a framework-agnostic verb client for the gateway; `download` returns the same lazy `StoredFile` the server SDK returns. - 31c9c3f: Add `files-sdk/express` — `createRouteHandler(router)` returns a Node `(req, res)` handler that bridges `IncomingMessage`/`ServerResponse` to the Web `Request`/`Response` the gateway speaks (also works with Connect and a raw `http.createServer`). A client disconnect aborts the upstream read on a proxied download. Mount it before any body parser so the gateway can read the raw upload/JSON body. - 91d20ef: Add `files-sdk/fastify` — `createRouteHandler(router)` returns a Fastify `(request, reply)` handler that `reply.hijack()`s and bridges the raw `IncomingMessage`/`ServerResponse` to the Web `Request`/`Response` the gateway speaks (the same seam as `files-sdk/express`). A client disconnect aborts the upstream read on a proxied download. Drop Fastify's built-in body parsers (`removeAllContentTypeParsers()` + a no-op `addContentTypeParser("*", …)`) so the gateway can read the raw upload/JSON body. - 31c9c3f: Add `files-sdk/hono` — `createRouteHandler(router)` returns a single Hono handler (`app.all("/api/files", handler)`). Web-native, so it runs on Workers, Bun, Deno, and Node. - 91d20ef: Add `files-sdk/koa` — `createRouteHandler(router)` returns a Koa handler that sets `ctx.respond = false` and bridges `ctx.req`/`ctx.res` to the Web `Request`/`Response` the gateway speaks (the same seam as `files-sdk/express`). A client disconnect aborts the upstream read on a proxied download. Mount it before any body parser so the gateway can read the raw upload/JSON body. - 31c9c3f: Add `files-sdk/next` — `createRouteHandler` to mount the gateway in the Next.js App Router. - 91d20ef: Add `files-sdk/nitro` — `createRouteHandler(router)` returns an h3 event handler for Nitro (and Nuxt server) routes that marshals `event.node.req` into the Web `Request` the gateway speaks and returns the Web `Response` for Nitro to flush, hiding the `toWebRequest(event)` step. A client disconnect aborts the upstream read on a proxied download. Targets Nitro v2 / h3 v1, where `event.node.req` is present on every preset. - 9923947: Expose the `versioning()` and `softDelete()` plugin verbs through the gateway, client and `useFiles` hook. `createFilesRouter` now dispatches `versions` / `restoreVersion` / `trashed` / `restoreTrashed` / `purge` (each a new deny-by-default `FilesOperation`, answered only when the matching plugin wraps the `Files` instance — otherwise a 422), and `createFilesClient` / `useFiles` gain matching methods (`files.versions(key)`, `files.restoreVersion(key, versionId?)`, `files.trashed()`, `files.restoreTrashed(key)`, `files.purge(key?)`). Trash listing and "empty trash" are key-prefix-scoped, so a multi-tenant `authorize` keyPrefix never leaks or purges another tenant's trash. - 31c9c3f: Add `files-sdk/react` — `useFiles({ endpoint })` returning every verb (imperative, with ambient upload progress/error) plus optional reactive `useList`/`useFile`/`useSearch` hooks. Emitted as a `"use client"` module. - 31c9c3f: Add `files-sdk/svelte` — the Svelte binding: `useFiles` returning Svelte stores for the ambient state, plus `useList`/`useFile`/`useSearch` query stores. Store-based (no Svelte runtime dependency). - 91d20ef: Add `files-sdk/sveltekit` — `createRouteHandler(router)` returns `{ GET, POST, PUT }` for a SvelteKit `+server.ts` endpoint (`GET` serves downloads, `POST` the JSON verbs, `PUT` the upload byte path). The handlers are Web-native, so the route runs on the Node and edge adapters alike. This is the server binding, distinct from the `files-sdk/svelte` client store. - b20440c: Add a shadcn component registry of `useFiles`-wired UI, installable with `npx shadcn add`. Upload + display: `dropzone`, `file-list`, `file-preview`, `upload-progress`, `multipart-uploader`. Navigation + actions: `file-browser` (folder tree via `list({ delimiter })` + breadcrumbs), `file-search` (`search()` with glob/regex/substring/exact), `share-dialog` (`url()` / `signedUploadUrl()` with expiry + copy), `file-actions` (copy/move/rename/download/delete menu), `capabilities-badges` (`capabilities()` as feature badges). Plugin showcases: `version-history` (`versioning()` — list + restore snapshots) and `trash-bin` (`softDelete()` — restore + purge soft-deleted files). The components ship in the docs site rather than the package, but they're a first-class part of the SDK surface. - 31c9c3f: Add `files-sdk/vue` — the Vue 3 twin of the React hook: a `useFiles` composable returning refs for the ambient state, plus reactive `useList`/`useFile`/`useSearch` composables over the same gateway. --- # files-sdk@2.1.0 Source: https://files-sdk.dev/changelog/files-sdk-2-1-0 ### Minor Changes - 6ee0980: Add `files-sdk/tanstack-start` server adapter — `createRouteHandler` mounts the Files gateway in a TanStack Start server route (`server.handlers`). - 5f7c09b: Add a WebDAV adapter (`files-sdk/webdav`) backed by the `webdav` client. HTTP-based, with native server-side `copy`/`move`, ranged and streaming downloads, and content-type round-tripping. Works against Nextcloud, ownCloud, Apache `mod_dav`, `sabre/dav`, and other WebDAV servers. Available in the CLI and MCP as the `webdav` provider. ### Patch Changes - 77ae3a8: Reject Cloudinary signed upload URLs when a server-enforced max size is requested. - 8d4e235: Disable Convex signed upload URLs because generated upload capabilities cannot bind SDK keys or upload limits. - 283f658: Remove public fallback upload-token secrets from the docs demo routes. - 4ea52ce: Cap public docs demo uploads on the `/api/files` in-memory route. - 763bc47: Cap public docs demo uploads on the `/api/files-trash` in-memory route. - 79c8c60: Cap public docs demo uploads on the `/api/files-versions` in-memory route. - 63607ba: Disable fs signed upload URLs because the adapter cannot signature-bind upload constraints. - dd12299: Reject filesystem adapter reads when symlinks resolve outside the configured root. - 8a0bf64: Bind MCP transfer and sync destinations at server startup instead of accepting provider config from tool calls. - 8d396d2: Enforce approval-gated OpenAI Responses write tool calls inside execute(). - 6d29787: Encode public URL dot segments so keys cannot escape configured base paths. - 0703817: Validate persisted resumable upload session URLs before resuming provider uploads. - d54f272: Honor router authorization maxResults when serving list requests. - 92e5700: Require same-origin requests by default for state-changing files router operations. - 52bf6f9: Enforce router upload byte limits while streaming bodies without content-length headers. - 2fc4d87: Force safe attachment dispositions for gateway URL requests unless server authorization overrides them. - 9c41388: Assemble the search ReDoS test pattern at runtime to clear a CodeQL `js/redos` false positive; no runtime behavior change. - a33b424: Reject unsafe regular expressions in search APIs before walking provider keys. - f1f0012: Match stateful (`g`/`y`) `RegExp` search patterns against every key independently, and rebuild caller-supplied regexes so search never mutates the original instance. - f1891ad: Clamp router signed upload URL size limits to the configured max upload size. - 737f82f: Pin signedUrlPolicy upload URL expiry to maxExpiresIn when runtime callers omit expiresIn. - 6f5ea66: Apply router `filterKeys` authorization to soft-delete purge operations. - 5bbf406: Reject UploadThing signed upload URLs when a server-enforced max size is requested. - ee4f6f9: Limit ZIP extraction entry counts and decompressed sizes before uploading entries. --- # files-sdk@2.2.0 Source: https://files-sdk.dev/changelog/files-sdk-2-2-0 ### Minor Changes - cd93019: Add first-class NestJS support (#95). New `files-sdk/nestjs` subpath exports a dynamic `FilesModule` (`forRoot()` / `forRootAsync()`) that configures the gateway, mounts it at a configurable `path` (default `/api/files`) through Nest's middleware layer, and shares the `Files` instance via DI — `@InjectFiles()` / `FILES` token, with the configured router under `FILES_API`. Works on both the Express adapter (create the app with `bodyParser: false`) and the Fastify adapter (no parser configuration needed — middleware runs before body parsing). `@nestjs/common` is a new optional peer dependency. - 38317d8: Lightweight `aws4fetch`-powered engine for Cloudflare R2 (#76). `r2({ client: "fetch" })` runs the HTTP adapter on a SigV4-signed `fetch` core (~2.5 KB gzipped, Web Crypto only) instead of `@aws-sdk/client-s3` — no `@aws-sdk/*` installs needed. It covers upload, download (+ ranges), head, exists, delete, list (+ delimiter), server-side copy, presigned `url()`, and `signedUploadUrl()`; multipart/resumable uploads throw with guidance to the default `"aws-sdk"` client, and stream bodies are buffered before the single PUT. Hybrid binding mode (binding + HTTP credentials) now signs `url()` / `signedUploadUrl()` through the same fetch core unconditionally, so binding and hybrid Workers never pull the AWS SDK into their bundle. Adds `aws4fetch` as a regular (tree-shaken, ~2.5 KB) dependency. - 8e6fee9: React Native / Expo support for `files-sdk/client` and the framework hooks. `upload()` now accepts a `NativeFileRef` (`{ uri, name, type, size }` — the shape Expo pickers return): presigned-POST targets stream the descriptor through React Native's `FormData`, and every other path resolves the `uri` to a Blob automatically. `download()` falls back to buffering via `arrayBuffer()` on runtimes whose `fetch` never exposes `Response.body` (React Native), instead of returning an empty stream. Byte-body uploads fall back to sending raw bytes when the runtime's `Blob` cannot be constructed from `ArrayBuffer` parts. Adds a React Native docs page under UI → Client. ### Patch Changes - 8992d9a: `FilesModule.forRootAsync()`: the `useFactory` return type now excludes `global` (new `FilesModuleFactoryResult` type). A `global` returned from the factory was silently ignored — the `DynamicModule` needs it before the factory runs, so it only takes effect on `FilesModuleAsyncOptions` itself. Returning it from the factory is now a type error instead of a no-op. - 8992d9a: `r2()` binding mode: `url()` and `signedUploadUrl()` reject again instead of throwing synchronously on misconfiguration (no hybrid credentials, `responseContentDisposition` without signing). A refactor had made these the only adapter methods that could throw before a `.catch` handler was attached, breaking direct/plugin adapter callers. Also corrects the `R2BindingOptions.bucket` doc comment — it is required for hybrid signing, not an error label. - 8992d9a: `StoredFile.blob()` now works on React Native. RN's `Blob` cannot be constructed from raw bytes, so `blob()` on a downloaded file threw at Blob construction — the exact platform the RN client work targets. On runtimes without byte-part Blobs, `blob()` now consumes the response's native `Response.blob()` instead, and later `text()`/`arrayBuffer()` calls read back through that Blob (via `Blob#arrayBuffer()` or `FileReader`). If bytes were already materialized first, `blob()` throws a clear `FilesError` explaining the ordering instead of an opaque platform error. - 8992d9a: The `fetch` S3 client (`r2({ client: "fetch" })` and hybrid binding signing) now fails closed on keys containing `.` or `..` path segments. WHATWG `URL` — used by both the SigV4 signer and `fetch` itself — collapses dot segments (even percent-encoded ones) before signing, so such keys were silently signed and sent for a _different_, normalized key, and under path-style addressing a `..` segment could escape the bucket entirely. These keys now throw a permanent `Provider` error with guidance to use the `aws-sdk` client, which addresses them literally. - 8992d9a: The `fetch` S3 client now maps post-dispatch failures to `FilesError` like everything else: a download/list/lazy-body read dying mid-stream, and signing errors in `url()` / `signedUploadUrl()` (e.g. an invalid `contentType` header value), previously escaped as raw runtime `TypeError`s when the adapter was used directly. - 5d83642: Fix `useFiles` recreating its internal store on every render. The store is now initialized lazily once (matching the abort-controller ref pattern), avoiding the wasted `createStore()` call and throwaway allocation on each render. --- # files-sdk@2.2.1 Source: https://files-sdk.dev/changelog/files-sdk-2-2-1 ### Patch Changes - d296f26: Expose the lazy provider-aware `loadFiles` runtime loader through `files-sdk/loader`. - c04e0d0: Keep `@aws-sdk/*` out of the static module graph reachable from `files-sdk/r2` so binding- and fetch-mode Workers bundle without the optional peers installed. Consumer bundlers (e.g. rolldown-vite with the Cloudflare Vite plugin) resolve even dynamically-imported chunks at build time, so the s3 entry's static SDK imports behind the r2 adapter's lazy boundary hard-errored with `MISSING_EXPORT` against the optional-peer placeholder (#105). The s3 engine is now SDK-parameterized (`s3/core.js`); `files-sdk/s3` wires it from static imports as before, while the r2 aws-sdk engine loads the SDK modules via dynamic import on first use. No behavior change for `files-sdk/s3` consumers. --- # files-sdk@2.2.2 Source: https://files-sdk.dev/changelog/files-sdk-2-2-2 ### Patch Changes - 82c198e: Avoid loading Firebase Admin when the Firebase Storage adapter receives an initialized bucket. - 1082186: Refresh environment-provided Vercel Blob credentials for every operation. --- # files-sdk@2.2.3 Source: https://files-sdk.dev/changelog/files-sdk-2-2-3 ### Patch Changes - 5dd79f3: Preserve the caller's query string on gateway-minted proxy upload targets, so a per-request `files` factory selected via the endpoint query (e.g. `useFiles({ endpoint: "/api/files?bucket=images" })`) resolves the same instance across the presign/proxy/complete round-trip. - ab2aa2f: Infer the content type from the key when listing S3-compatible buckets, instead of reporting every object as `application/octet-stream`. --- # Introduction Source: https://files-sdk.dev/docs ## What is Files SDK? A single TypeScript API for object storage that works the same way across AWS S3, Cloudflare R2, Vercel Blob, Google Cloud Storage, Azure, Supabase, the S3-compatible long tail (MinIO, Backblaze, Wasabi, R2, Scaleway, OVH, Hetzner, Tigris, Storj, Filebase, Akamai, IDrive, Vultr, IBM COS, Oracle, Exoscale, DigitalOcean Spaces), the consumer-style providers (Dropbox, Box, Google Drive, OneDrive, SharePoint), the upload-focused services (UploadThing, Cloudinary), the BaaS stack (Appwrite, PocketBase, Firebase Storage), and a local `fs` adapter for tests. Ten methods cover the surface area you actually use: `upload`, `download`, `head`, `exists` (each taking one key or an array for bulk), `delete` (one key or an array for bulk), `copy`, `move`, `list` (or `listAll` to walk every page), `url`, `signedUploadUrl`. When you need provider-specific power - S3 versioning, lifecycle rules, multipart, ACLs - drop down to the native client via `files.raw`, which stays typed per adapter. ## Why Files SDK? Every storage SDK ships with its own shape: `PutObjectCommand`, `put()`, `uploadStream`, `createWriteStream`, presigner factories, ACL nouns, error envelopes. Switching providers - or supporting more than one - means rewriting the call sites and re-learning the error model. Files SDK collapses that into one class and one error type: - **One API** - same call shape across every adapter. The code that uploads to S3 is the code that uploads to Vercel Blob. - **Normalized errors** - `FilesError` with a small enum of codes (`NotFound`, `Unauthorized`, `Conflict`, `ReadOnly`, `Provider`), with the original error preserved on `cause`. - **Lazy SDK loading** - adapters are subpath exports (`files-sdk/s3`, `files-sdk/r2`, ...). The provider SDK you don't use isn't bundled. - **Typed escape hatch** - `files.raw` is typed as the underlying client (S3Client, R2Bucket, VercelBlobClient, ...), so the unified API never traps you. - **Agent-friendly CLI** - one `files` binary, JSON output, stdin/stdout streaming, plus a built-in [MCP](https://modelcontextprotocol.io) server. Same semantics as the SDK. ## Next steps - [Installation](/docs/installation) - install the SDK and the adapter peer dependencies. - [Usage](/docs/usage) - construct a `Files` instance and run the core methods. - [API reference](/docs/api) - the full method surface, options, and the `StoredFile` type. - [Adapters](/docs/adapters) - per-provider setup, options, and gotchas. - [CLI](/docs/cli) - the same API as an agent-friendly `files` binary with JSON output and an MCP server. - [UI](/docs/ui) - React, Vue, and Svelte bindings backed by a server gateway. - [Plugins](/docs/plugins) - encryption, compression, validation, versioning, and more as an ordered pipeline. - AI integrations - hand your bucket to [OpenAI](/docs/ai/openai), [Claude](/docs/ai/claude), or the [Vercel AI SDK](/docs/ai/vercel) as ready-made tools. - [FAQ](/docs/faq) - common questions, answered. - [Changelog](/changelog) - what shipped in each release. --- # Akamai Cloud Object Storage Source: https://files-sdk.dev/docs/adapters/akamai ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { akamai } from "files-sdk/akamai"; const files = new Files({ adapter: akamai({ bucket: "uploads", region: "us-iad-1", // or "nl-ams-1", "fr-par-1", "us-east-1", ... // accessKeyId / secretAccessKey auto-loaded from // AKAMAI_ACCESS_KEY_ID / AKAMAI_SECRET_ACCESS_KEY }), }); ``` Akamai Cloud Object Storage (formerly Linode Object Storage) via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the region/cluster code (`us-iad-1`, `nl-ams-1`, `fr-par-1`, ...), virtual-hosted-style addressing, errors relabelled. The endpoint domain `linodeobjects.com` is unchanged from the Linode era - only the product branding moved to Akamai. Auto-loads from `AKAMAI_ACCESS_KEY_ID` and `AKAMAI_SECRET_ACCESS_KEY`. Generate access keys in the Akamai Cloud Manager under Object Storage -> Access Keys. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Alibaba Cloud OSS Source: https://files-sdk.dev/docs/adapters/alibaba ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { alibaba } from "files-sdk/alibaba"; const files = new Files({ adapter: alibaba({ bucket: "uploads", region: "cn-hangzhou", // or "cn-shanghai", "ap-southeast-1", ... // accessKeyId / secretAccessKey auto-loaded from // ALIBABA_ACCESS_KEY_ID / ALIBABA_ACCESS_KEY_SECRET }), }); ``` Alibaba Cloud Object Storage Service (OSS) via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the region code (`cn-hangzhou`, `cn-shanghai`, `ap-southeast-1`, ...), virtual-hosted-style addressing, errors relabelled. Auto-loads from `ALIBABA_ACCESS_KEY_ID` and `ALIBABA_ACCESS_KEY_SECRET`. Generate AccessKey pairs in the Alibaba Cloud console under RAM -> Users. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Appwrite Source: https://files-sdk.dev/docs/adapters/appwrite ## Installation `node-appwrite` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk node-appwrite ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { appwrite } from "files-sdk/appwrite"; const files = new Files({ adapter: appwrite({ bucket: "uploads", // Auto-loads from APPWRITE_ENDPOINT, APPWRITE_PROJECT_ID, // and APPWRITE_API_KEY. Or pass an existing node-appwrite // Client or Storage instance via `client`. // // Note: Appwrite keys (IDs) must be alphanumeric/dashes // and max 36 chars. Slashes (/) are not supported. }), }); ``` ## Options ## Limitations File IDs (keys) must start with an alphanumeric and use only `[a-zA-Z0-9._-]`, max 36 characters (no slashes) - invalid keys are rejected before the API call. `list({ prefix })` queries `startsWith("$id", ...)` against the canonical file ID, so files created outside the adapter where the display `name` differs from `$id` won't be matched by prefix. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Stream bodies are buffered up-front - `InputFile.fromBuffer` has no streaming form, so streamed uploads can't avoid materializing the body in memory. User `metadata` and `cacheControl` throw - Appwrite's `createFile` has no equivalent fields. `contentType` is silently ignored - Appwrite auto-detects mime from the payload and has no override. | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Read-then-write - Appwrite has no server-side copy primitive, so the source is downloaded and re-uploaded. Costs an egress + an ingest; not atomic. | | `url` | ⚠️ | Throws by default because Appwrite SDKs cannot mint presigned reading URLs with keys. Set `public: true` at construction to return the constructed Appwrite public CDN URL. `expiresIn` and `responseContentDisposition` are ignored. | | `signedUploadUrl` | ❌ | No presigned upload primitive in Appwrite. Use JWTs or client SDKs for direct uploads. | --- # Archil Source: https://files-sdk.dev/docs/adapters/archil ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. The `disk` package is also optional - install it only if you pass a `Disk` instance to reach Archil-native operations. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { archil } from "files-sdk/archil"; const files = new Files({ adapter: archil({ bucket: "dsk-0123456789abcdef", // your Archil disk id region: "aws-us-east-1", // accessKeyId / secretAccessKey auto-loaded from // ARCHIL_S3_ACCESS_KEY_ID / ARCHIL_S3_SECRET_ACCESS_KEY }), }); ``` Archil disks via their S3-compatible API. A thin wrapper around the S3 adapter - the disk id is the path-style bucket, the endpoint is derived from the Archil region (`aws-us-east-1`, `gcp-us-central1`, ...), and SigV4 signs every request, so byte ranges, multipart uploads, and presigned `url()` / `signedUploadUrl()` all work. Auto-loads `ARCHIL_S3_ACCESS_KEY_ID`, `ARCHIL_S3_SECRET_ACCESS_KEY`, and `ARCHIL_REGION`. ### Branches Set `branch` to scope the whole unified surface to a branch of the disk - `upload`, `download`, `list`, and presigned URLs all read and write that branch: ```ts lineNumbers const preview = new Files({ adapter: archil({ branch: "preview", bucket: "dsk-0123456789abcdef", region: "aws-us-east-1", }), }); ``` ### Archil-native operations The unified surface covers object storage. For Archil-native operations that aren't object storage - `exec`, `grep`, `appendObject`, `share` - pass a `Disk` instance (from the `disk` package); it's exposed at `files.adapter.disk`, and `bucket` / `region` are inferred from it: ```ts lineNumbers import { getDisk } from "disk"; const disk = await getDisk("dsk-0123456789abcdef"); const files = new Files({ adapter: archil({ disk }) }); await files.upload("src/app.ts", code); // unified surface await files.adapter.disk?.exec("tsc --noEmit"); // Archil-native ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | SigV4 presigned, or `publicBaseUrl` when set. | | `signedUploadUrl` | ✅ | | --- # Azure Blob Storage Source: https://files-sdk.dev/docs/adapters/azure ## Installation `@azure/storage-blob` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @azure/storage-blob ``` ## Usage Azure Blob Storage via the official `@azure/storage-blob` SDK. Five credential modes: connection string, account name + account key, account name + token credential, account name + SAS token, or anonymous (public-read containers only). Connection-string parsing recovers the account name + key so signing methods keep working. ```ts lineNumbers import { Files } from "files-sdk"; import { azure } from "files-sdk/azure"; import { DefaultAzureCredential } from "@azure/identity"; const files = new Files({ adapter: azure({ container: "uploads", // Auto-loads from AZURE_STORAGE_CONNECTION_STRING, or // AZURE_STORAGE_ACCOUNT_NAME + AZURE_STORAGE_ACCOUNT_KEY. // Pass connectionString / accountKey / credential / sasToken to override. // accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME, // credential: new DefaultAzureCredential(), // Azure AD / Managed Identity }), }); ``` ## Options ## Limitations Azure AD / Managed Identity is supported via the `credential` option (install `@azure/identity`); it authenticates SDK calls and mints User Delegation SAS URLs for `url()`, `signedUploadUrl()`, and `copy()`. The principal needs blob data access plus permission to call `generateUserDelegationKey`. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Server-side copy via `syncCopyFromURL` - capped at 256 MB source size. Larger blobs need `beginCopyFromURL` (poller); drop down to `adapter.raw` for that. SAS-only adapter mode reuses the configured token; shared-key mode mints a 5-min read SAS. | | `url` | ⚠️ | Signs a SAS read URL. Shared-key mode uses the account key; TokenCredential mode uses User Delegation SAS. Throws in SAS-only or anonymous mode (no signer available). Pass `accountKey` + `accountName`, a connection string with an account key, `credential` + `accountName`, or set `publicBaseUrl` for a public container. | | `signedUploadUrl` | ⚠️ | PUT URL only - Azure has no POST policy equivalent. `maxSize` throws because Azure SAS has no `content-length-range` policy, and `contentType` throws because Azure SAS does not bind Content-Type into the signature; enforce those checks at your application gateway instead. Shared-key mode uses the account key; TokenCredential mode uses User Delegation SAS. Throws in SAS-only or anonymous mode (no signer available). The returned headers include the required `x-ms-blob-type: BlockBlob`. | --- # Backblaze B2 Source: https://files-sdk.dev/docs/adapters/backblaze-b2 ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { backblazeB2 } from "files-sdk/backblaze-b2"; const files = new Files({ adapter: backblazeB2({ bucket: "uploads", region: "us-west-002", // or "us-east-005", "eu-central-003", ... // accessKeyId / secretAccessKey auto-loaded from // B2_APPLICATION_KEY_ID / B2_APPLICATION_KEY }), }); ``` Backblaze B2 via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the cluster code (`us-west-002`, `us-east-005`, `eu-central-003`, ...), virtual-hosted-style addressing, errors relabelled. Auto-loads from `B2_APPLICATION_KEY_ID` and `B2_APPLICATION_KEY`. Generate an application key in the Backblaze console under Account -> Application Keys; the bucket's cluster is shown next to its endpoint. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Box Source: https://files-sdk.dev/docs/adapters/box ## Installation `box-typescript-sdk-gen` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk box-typescript-sdk-gen ``` ## Usage Box via the official `box-typescript-sdk-gen` SDK. Box files live by ID, not by path, so the adapter walks `rootFolderId` and translates virtual keys (`docs/a.txt`) into nested Box subfolders, auto-creating intermediate folders on `upload()`. Five auth shapes (pre-built client, developer token, OAuth refresh-token, Client Credentials Grant, JWT server auth) cover scripts, user apps, and enterprise installs - token lifecycle is handled by the SDK's built-in `Authentication` classes. ```ts lineNumbers import { Files } from "files-sdk"; import { box } from "files-sdk/box"; // Server-side: Client Credentials Grant (recommended for backend services). // The SDK manages access-token lifetime internally - no manual refresh // bookkeeping in the adapter. const files = new Files({ adapter: box({ ccg: { clientId: process.env.BOX_CLIENT_ID!, clientSecret: process.env.BOX_CLIENT_SECRET!, enterpriseId: process.env.BOX_ENTERPRISE_ID!, }, rootFolderId: process.env.BOX_ROOT_FOLDER_ID, // defaults to "0" (account root) // publicByDefault: true → upload() also calls addShareLinkToFile and // url() returns the link's download_url. }), }); // Other auth shapes the adapter accepts: // developerToken: process.env.BOX_DEVELOPER_TOKEN // dev-console token // oauth: { clientId, clientSecret, refreshToken } // user-app flow // jwt: { configJsonString } // JWT server auth // client: yourBoxClient // pre-built escape hatch ``` ## Options ## Limitations `list()` is not recursive - it returns immediate-children files only at `rootFolderId`. For deep enumeration, drop to `raw.folders.getFolderItems` and recurse manually. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Two-stage: walks/creates parent folders by ID under `rootFolderId`, then `uploads.uploadFile` (≤50 MB) or `chunkedUploads.uploadBigFile` (>50 MB). Re-uploads against existing leaf names route through `uploadFileVersion` (overwrite). Stream bodies are buffered up-front - Box's upload manager takes a Node `Readable`, not a Web stream. User `metadata` and `cacheControl` throw - Box exposes file metadata via classifications and metadata templates; drop to `raw.fileMetadata.*` if you need it. Pause/resume via `control` is in-process only — the chunked-upload commit needs a whole-file digest, so a token cannot resume in a new process. | | `download` | ⚠️ | Resolves the file ID, then fetches `getDownloadFileUrl` for both buffered and streaming reads - the SDK's native `downloadFile` returns a Node `Readable` that's awkward to expose isomorphically, so the adapter routes through standard HTTP, which gives a `ReadableStream` body. | | `delete` | ✅ | | | `list` | ⚠️ | Returns immediate-children files only at `rootFolderId` - no recursion, and subfolders are filtered out. `prefix` is filename-prefix only (matched client-side within the page). Pagination uses Box's offset, encoded as a numeric cursor string. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | Box doesn't store user-supplied content types on file content - `head()` returns a type inferred from the filename extension (or `application/octet-stream` when unknown). `size`, `etag`, and `lastModified` come from `getFileById`. | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ⚠️ | Default mints a signed download URL via `getDownloadFileUrl` - Box controls the TTL server-side, so `expiresIn` is accepted for API symmetry but is not honoured. With `publicByDefault: true`, `upload()` calls `addShareLinkToFile` (open access) and `url()` returns the link's `download_url`. With `publicBaseUrl`, returns `/`. `responseContentDisposition` always throws - Box's URLs have no Content-Disposition override. | | `signedUploadUrl` | ❌ | Throws - Box uploads require a multipart POST with both an `attributes` JSON part and the file bytes part, which fits neither the SDK's PUT-with-headers nor S3-style POST-with-form-fields shape. Use `upload()` server-side, or Box's UI Elements / Content Uploader for browser flows. | --- # Bun S3 Source: https://files-sdk.dev/docs/adapters/bun-s3 ## Installation This adapter has no extra peer dependencies, but it requires the Bun runtime - it's built on Bun's native `Bun.S3Client`, which Node doesn't provide. (Outside Bun it throws unless you hand it a `Bun.S3Client`-shaped `client` yourself.) ```package-install files-sdk ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { bunS3 } from "files-sdk/bun-s3"; const files = new Files({ adapter: bunS3({ bucket: "uploads", region: "us-east-1", // accessKeyId / secretAccessKey auto-loaded by Bun from // S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY (or AWS_* equivalents) }), }); // Or hand it the singleton Bun.s3 client directly: new Files({ adapter: bunS3({ client: Bun.s3 }) }); ``` Three things differ from `files-sdk/s3`: `copy()` streams bytes through your process because Bun doesn't expose a server-side `CopyObject` primitive; `upload()` throws on `metadata` and `cacheControl` because `Bun.S3Client.write()` has no equivalent options; and `signedUploadUrl()` throws on `maxSize` because Bun exposes presigned URLs only - not S3 POST policy fields. Reach for `files-sdk/s3` on the same bucket when you need any of those. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | User `metadata` and `cacheControl` throw - `Bun.S3Client.write()` exposes neither field. Reach for `s3()` on the same bucket if you need them. Stream bodies are wrapped in a `Response` and handed to Bun's writer. Pause/resume via `control` is in-process only — Bun’s S3 client exposes no resumable upload id, so chunks are buffered and a token cannot resume in a new process. | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Client-side stream copy - `Bun.S3Client` doesn't expose a server-side `CopyObject`, so the source is streamed through this process and re-uploaded. Doubled bandwidth, not atomic, and drops Content-Disposition/cache headers/user metadata/ACL (only Content-Type is preserved). Reach for `s3()` on the same bucket for server-side copy. | | `url` | ✅ | | | `signedUploadUrl` | ⚠️ | PUT URL only - Bun exposes presigned URLs, not S3 POST policy fields, so `maxSize` throws (no `content-length-range` policy). Enforce upload caps at your application gateway instead. | --- # Bunny Storage Source: https://files-sdk.dev/docs/adapters/bunny-storage ## Installation `@bunny.net/storage-sdk` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @bunny.net/storage-sdk ``` ## Usage Bunny Storage via the official `@bunny.net/storage-sdk`. The adapter connects to a Storage Zone with its zone password / API access key and uses Bunny's HTTP Storage API for reads, writes, listing, and deletes. Auto-loads from `BUNNY_STORAGE_ZONE`, `BUNNY_STORAGE_ACCESS_KEY`, and `BUNNY_STORAGE_REGION`; also accepts `STORAGE_ZONE`, `STORAGE_ACCESS_KEY`, and `STORAGE_REGION` as aliases (the names used in the Bunny SDK's README example). ```ts lineNumbers import { Files } from "files-sdk"; import { bunnyStorage } from "files-sdk/bunny-storage"; const files = new Files({ adapter: bunnyStorage({ zone: "uploads", region: "de", // accessKey auto-loaded from BUNNY_STORAGE_ACCESS_KEY // publicBaseUrl: "https://files.example.com", }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Custom `metadata` and `cacheControl` throw — the Bunny Storage TypeScript SDK exposes content-type/checksum but no arbitrary object metadata or per-object cache-control field. Configure cache behavior on the Pull Zone/CDN. Resumable uploads (`control`) are not supported — Bunny Storage uploads in a single PUT. | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | Bunny lists a directory, not a recursive object-prefix scan. The adapter chooses the nearest directory for `prefix`, filters that page client-side, and encodes numeric offsets as cursors after fetching the directory listing. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Read-then-write — Bunny Storage's TypeScript SDK has no server-side copy primitive, so the source is downloaded and re-uploaded. Not server-side atomic. | | `url` | ⚠️ | Requires `publicBaseUrl` (for example a Bunny Pull Zone or custom CDN hostname) and returns `/`. Without it, throws because the Storage API URL requires an `AccessKey` header. `expiresIn` is ignored and `responseContentDisposition` throws — Bunny Storage has no signed-read URL primitive. | | `signedUploadUrl` | ❌ | Throws — Bunny Storage has no presigned upload primitive. Writes go through the Storage API with an `AccessKey` header, so upload server-side via the SDK or proxy through your application. | --- # Cloudinary Source: https://files-sdk.dev/docs/adapters/cloudinary ## Installation `cloudinary` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk cloudinary ``` ## Usage Cloudinary asset CDN via the official `cloudinary` Node SDK. Defaults to `resource_type: "raw"` for arbitrary-bytes storage so keys round-trip cleanly through the adapter; switch to `"image"` or `"video"` if you want Cloudinary's transformation features. Falls back to `CLOUDINARY_URL` or individual env vars when no explicit credentials are passed. ```ts lineNumbers import { Files } from "files-sdk"; import { cloudinary } from "files-sdk/cloudinary"; const files = new Files({ adapter: cloudinary({ // Auto-loads from CLOUDINARY_URL (cloudinary://key:secret@cloud) // or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET. // // Defaults to resource_type: "raw" - closest to S3-style // arbitrary-bytes storage. Switch to "image" / "video" if the // bucket holds those types and you want transforms. resourceType: "raw", type: "upload", }), }); ``` ## Options ## Limitations Cloudinary's SDK keeps configuration as module-level global state, so mounting multiple `cloudinary()` adapters in the same process with different credentials will see only the last config win - use the `client` escape hatch with separately configured SDK instances if you need that. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Bodies are buffered into memory and handed to `upload_stream` - Cloudinary's SDK has no streaming form. User `metadata` and `cacheControl` throw - Cloudinary has no per-asset HTTP cache header and no arbitrary-metadata field on upload; drop to `raw` for `context`. Uploads are scoped to the adapter's `resourceType`/`type` and overwrite (`invalidate: true`). | | `download` | ⚠️ | No streaming primitive - the adapter fetches the delivery URL with `fetch()` to read bytes, so streamed downloads still buffer the body in memory. Metadata comes from a parallel `api.resource` call. | | `delete` | ✅ | | | `list` | ⚠️ | Page size clamped to 500 (Cloudinary Admin API ceiling). Resources are scoped by `resource_type` and `type` at adapter construction, so mixed-type buckets need separate adapters. Pagination uses Cloudinary's opaque `next_cursor`. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Re-upload by URL - Cloudinary has no native copy and `rename` is move-only. The adapter fetches the source delivery URL and ingests it as a new asset under `to`. Produces a new `asset_id`/`etag`, not a byte-identical reference. Costs an egress + an ingest; not atomic. | | `url` | ⚠️ | Public delivery URLs by default (`type: 'upload'`). For `private`/`authenticated` types, mints a signed delivery URL via `private_download_url` (requires `apiSecret` and the asset's stored format - costs a HEAD round-trip per call). `responseContentDisposition` always throws - Cloudinary has no per-request Content-Disposition override (drop to `raw` for the `attachment` flag). | | `signedUploadUrl` | ⚠️ | Form-POST shape with `fields` (`method: 'POST'`), not a single presigned PUT URL - signs Cloudinary's `api_sign_request` payload. Requires `apiSecret`. `maxSize` and `minSize` aren't enforced server-side - use an upload preset with `max_file_size` if you need a cap. `expiresIn` is informational - Cloudinary signatures are fixed at 1h. | --- # Convex Source: https://files-sdk.dev/docs/adapters/convex ## Installation `convex` is already in your project if you're using Convex. It's declared as an optional peer dependency so the adapter's types resolve. ```package-install files-sdk convex ``` ## Usage Convex file storage is only reachable from inside a Convex function — there's no external REST API. So this adapter wraps the function context (`ctx`) and is constructed **per request**, inside your `action`, `mutation`, or `query` handlers. Convex assigns an opaque storage id (`Id<"_storage">`) on upload — you don't choose it. The adapter therefore treats that id as the unified `key`: `upload()` ignores the key you pass and returns the assigned id as `UploadResult.key`, and `download`/`head`/`delete`/`url` take that id as the key. ```ts lineNumbers import { Files } from "files-sdk"; import { convex } from "files-sdk/convex"; import { action, query } from "./_generated/server"; import { v } from "convex/values"; // Upload + download need an *action* (Convex exposes ctx.storage.store / // ctx.storage.get only there). The returned key is the Convex storage id — // persist it in your own table to reference the file later. export const saveFile = action({ args: { bytes: v.bytes() }, handler: async (ctx, { bytes }) => { const files = new Files({ adapter: convex({ ctx }) }); const { key, size } = await files.upload("ignored", new Uint8Array(bytes)); return { size, storageId: key }; }, }); // list() needs a *query* or *mutation* (it reads the _storage system table // via ctx.db). url() works in any context. export const listFiles = query({ handler: async (ctx) => { const files = new Files({ adapter: convex({ ctx }) }); const { items } = await files.list({ limit: 100 }); return items.map((f) => ({ key: f.key, size: f.size, type: f.type })); }, }); ``` ## Options ## Storage layout Files live in Convex storage, tracked by the built-in `_storage` system table. Each file's `key` is its `Id<"_storage">`. Metadata maps from that table: `size` → `size`, `contentType` → `type`, `sha256` → `etag`, `_creationTime` → `lastModified`. Convex has no user-metadata field, so `metadata` is always `undefined`. Don't set a `prefix` on the `Files` instance with this adapter - it would be prepended to the storage id and corrupt it. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Requires an action context — `ctx.storage.store` exists only in actions, so calling `upload` from a mutation or query throws. The caller-supplied key is ignored: Convex assigns the storage id, which is returned as the key. Stream bodies are buffered up-front since `store` takes a Blob. User `metadata` and `cacheControl` throw — the `_storage` table is fixed to contentType/sha256/size. Resumable uploads (`control`) are not supported — `ctx.storage.store` is a single call with no resumable session. | | `download` | ⚠️ | Requires an action context — `ctx.storage.get` exists only in actions, so calling `download` from a mutation or query throws. The body is buffered into memory: Convex returns a Blob, not a stream. | | `delete` | ⚠️ | Requires a writer context (mutation or action); throws in queries. Idempotent — deleting a missing id is a no-op. | | `list` | ⚠️ | Requires a query or mutation context — it reads the `_storage` system table via `ctx.db`, which actions don't have. `prefix` filters by literal storage-id prefix and is rarely meaningful for opaque ids. Pagination uses Convex's `paginate` cursor. List items expose lazy bodies, so reading them needs an action context. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | Reads metadata from the `_storage` system table (`ctx.db.system`) in queries/mutations, or the deprecated `ctx.storage.getMetadata` in actions. The returned body is lazy — reading it calls `ctx.storage.get`, which requires an action context. | | `exists` | ✅ | | | `copy` | ❌ | Throws — Convex assigns immutable storage ids and can't copy to a caller-chosen key. Download the source and `upload()` a new file, then track the new id. | | `url` | ⚠️ | Returns Convex's permanent serving URL (`getUrl`) — it stays valid while the file exists, so `expiresIn` is ignored. `responseContentDisposition` throws: Convex serving URLs have no Content-Disposition override; serve untrusted content through your own HTTP action. | | `signedUploadUrl` | ❌ | Throws — Convex `generateUploadUrl()` cannot bind the caller's SDK key, expiry, size bounds, or content type into the issued upload capability. Upload through a Convex action with `files.upload()` instead, then track the returned storage id. | --- # DigitalOcean Spaces Source: https://files-sdk.dev/docs/adapters/digitalocean-spaces ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { digitaloceanSpaces } from "files-sdk/digitalocean-spaces"; const files = new Files({ adapter: digitaloceanSpaces({ bucket: "uploads", region: "nyc3", // accessKeyId / secretAccessKey auto-loaded from // DO_SPACES_KEY / DO_SPACES_SECRET }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Dropbox Source: https://files-sdk.dev/docs/adapters/dropbox ## Installation `dropbox` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk dropbox ``` ## Usage Dropbox via the official `dropbox` SDK. Path-addressable like OneDrive (`/folder/file.txt`), so virtual keys map directly to Dropbox paths - no virtual-key cache, no bookkeeping. Four auth shapes (pre-built client, static or dynamic access token, OAuth refresh token + app key, or env-var fallback) cover personal Dropbox, Dropbox Business, and team-space deployments. ```ts lineNumbers import { Files } from "files-sdk"; import { dropbox } from "files-sdk/dropbox"; // OAuth2 refresh-token flow (recommended for server-side apps). // The adapter exchanges the refresh token at api.dropboxapi.com/oauth2/token // and caches the access token until ~60s before expiry. const files = new Files({ adapter: dropbox({ refreshToken: process.env.DROPBOX_REFRESH_TOKEN!, appKey: process.env.DROPBOX_APP_KEY!, appSecret: process.env.DROPBOX_APP_SECRET, // omit for PKCE public clients rootFolderPath: "/Uploads", // publicByDefault: true → upload() also creates a public shared link // and url() returns it (rewritten to ?dl=1 for // direct download). }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Single-call `filesUpload` up to Dropbox's 150 MB limit; bodies above that automatically switch to `filesUploadSession*` (chunked, up to 350 GB) buffered into memory. Stream bodies are buffered up-front since the SDK has no streaming form. User `metadata` and `cacheControl` throw - Dropbox has no native arbitrary-metadata field; use `raw` with `property_groups` (registered template required) if you need it. | | `download` | ⚠️ | `filesDownload` buffers the full body - the SDK has no streaming download primitive. For `as: 'stream'`, the adapter mints a temporary link and fetches it via standard HTTP, which exposes a `ReadableStream` body. | | `delete` | ✅ | | | `list` | ⚠️ | Recursive listing under `rootFolderPath` via `filesListFolder({ recursive: true })`; folder entries are filtered out. `prefix` is matched client-side within the returned page and can under-return when the prefix isn't satisfied within a single page. Pagination uses Dropbox's opaque cursor via `filesListFolderContinue`. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | Dropbox doesn't store user-supplied content types - `filesUpload` accepts no Content-Type. `head()` returns a type inferred from the filename extension (or `application/octet-stream` when unknown). `etag` is Dropbox's `rev` field. | | `exists` | ⚠️ | Resolves via `filesGetMetadata` and returns `false` for folder or deleted entries at the path - matches Dropbox's semantics where the same path can hold a folder or a tombstone. Only true file entries return `true`. | | `copy` | ✅ | | | `url` | ⚠️ | Default mints a 4-hour temporary link via `filesGetTemporaryLink` - the API takes no expiry parameter, so `expiresIn` is **validated only**: values above Dropbox's 14400s (4h) fixed lifetime throw, values below are accepted but the link still lives ~4h. Don't rely on a short `expiresIn` as a security control here. With `publicByDefault: true`, `upload()` creates a public shared link and `url()` returns it (rewritten to `?dl=1` for direct download). With `publicBaseUrl`, returns `/`. `responseContentDisposition` always throws - Dropbox links have no Content-Disposition override. | | `signedUploadUrl` | ❌ | Throws - Dropbox's `filesGetTemporaryUploadLink` returns a URL that expects POST with a raw body, which fits neither the SDK's PUT-with-headers nor POST-with-form-fields shape. Use `upload()` or drop to `raw.filesGetTemporaryUploadLink(...)` for client-side uploads. | --- # Exoscale Object Storage Source: https://files-sdk.dev/docs/adapters/exoscale ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { exoscale } from "files-sdk/exoscale"; const files = new Files({ adapter: exoscale({ bucket: "uploads", region: "ch-gva-2", // or "de-fra-1", "at-vie-1", "bg-sof-1", ... // accessKeyId / secretAccessKey auto-loaded from // EXOSCALE_API_KEY / EXOSCALE_API_SECRET }), }); ``` Exoscale Object Storage (SOS) via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the zone code (`ch-gva-2`, `ch-dk-2`, `de-fra-1`, `de-muc-1`, `at-vie-1`, `at-vie-2`, `bg-sof-1`), virtual-hosted-style addressing, errors relabelled. Pass the zone as `region` - Exoscale calls them zones but they fill the SigV4 region slot. Auto-loads from `EXOSCALE_API_KEY` and `EXOSCALE_API_SECRET`. Generate IAM keys in the Exoscale Portal under IAM -> API Keys. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Filebase Source: https://files-sdk.dev/docs/adapters/filebase ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { filebase } from "files-sdk/filebase"; const files = new Files({ adapter: filebase({ bucket: "uploads", // accessKeyId / secretAccessKey auto-loaded from // FILEBASE_ACCESS_KEY_ID / FILEBASE_SECRET_ACCESS_KEY }), }); ``` Filebase via its S3-compatible API. Filebase fronts decentralized storage networks (IPFS, Sia, Storj) behind a standard S3 gateway - the network is chosen per-bucket in the dashboard, not per-request. A thin wrapper around the S3 adapter pointed at `https://s3.filebase.com`, with errors relabelled. Auto-loads from `FILEBASE_ACCESS_KEY_ID` and `FILEBASE_SECRET_ACCESS_KEY`. Generate access keys in the Filebase console under Access Keys. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Firebase Storage Source: https://files-sdk.dev/docs/adapters/firebase-storage ## Installation `firebase-admin` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk firebase-admin ``` ## Usage Firebase Cloud Storage via the official `firebase-admin` SDK. The Admin SDK's `getStorage().bucket()` returns a `@google-cloud/storage` `Bucket` under the hood, so every primitive (server-side copy, V4 signed URLs, POST policy uploads) maps onto the GCS surface - with Firebase-flavoured credential conventions and a default bucket name derived from your project ID. ```ts lineNumbers import { Files } from "files-sdk"; import { firebaseStorage } from "files-sdk/firebase-storage"; const files = new Files({ adapter: firebaseStorage({ bucket: "my-project.firebasestorage.app", // Auto-loads credentials from FIREBASE_PROJECT_ID, // FIREBASE_CLIENT_EMAIL, FIREBASE_PRIVATE_KEY, or falls back to // Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS, // gcloud auth, GCE metadata). Or pass an existing firebase-admin App // or @google-cloud/storage Bucket via `app`. }), }); ``` `upload` reports true byte-level [progress](/docs/api/upload#progress-tracking) via `onProgress`. As with GCS, passing `onProgress` switches the upload to a **resumable** request (the only path that emits progress), which adds one round trip. ## Options ## Limitations Firebase's `?alt=media&token=...` download-token URL form is out of scope for v1 - `url()` always returns either a V4 signed read URL or your configured `publicBaseUrl`. Reach for `adapter.raw` (the underlying `@google-cloud/storage` `Bucket`) if you need to mint Firebase download tokens or use any GCS-side feature that isn't in the unified API. Stream uploads use single-request mode; multi-GB resumable uploads also need `raw`. ## Compatibility | Method | Status | Notes | | ----------------- | :----: | ----- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Filesystem Source: https://files-sdk.dev/docs/adapters/fs ## Installation This adapter has no extra peer dependencies - the runtime (Node or Bun) provides everything it needs. ```package-install files-sdk ``` ## Usage Local filesystem. The dev/test adapter - point it at a directory and it implements the same `Adapter` contract as the cloud adapters using `node:fs/promises`. Each upload writes the body and a sidecar `.meta.json` file alongside it (Content-Type, ETag, user metadata) so reads round-trip cleanly. Not for production: there's no replication, no signing, no auth. ```ts lineNumbers import { Files } from "files-sdk"; import { fs } from "files-sdk/fs"; // Writes objects under `./.uploads` with a sidecar `.meta.json` // per file for Content-Type, ETag, and user metadata. Designed for // dev and CI - same Adapter contract as the cloud adapters, so swap // it in via env without changing call sites. const files = new Files({ adapter: fs({ root: "./.uploads", // Optional: configure if a dev server exposes the same root over // HTTP, so url() returns a browser-friendly URL instead of file://. // urlBaseUrl: "http://localhost:3000/files", }), }); ``` ## Options ## Storage layout Body at `` `${root}/${key}` ``; sidecar at `` `${root}/${key}.meta.json` ``. Sidecars survive `cp -r` / `git mv` / partial-tree deletion. `list()` hides them. ETag is a SHA-1-derived stable hash computed at upload time. Files written into `root` by hand without a sidecar are still readable - `contentType` falls back to `application/octet-stream` and `etag` is absent. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ⚠️ | Returns a `file://` URL by default - fine for CLIs and tests, not browsers. With `urlBaseUrl` set, returns `/` so a dev server (Next.js `/public` mount, `serve-static`, etc.) can deliver the body. `responseContentDisposition` throws because neither `file://` nor static-server URLs have a signature mechanism in which to bind the override. | | `signedUploadUrl` | ❌ | Throws - the fs adapter has no built-in upload server, signer, or verifier, so it cannot bind expiry, content type, or size limits into an upload capability. Upload through `files.upload()` or an application route that enforces those controls server-side. | --- # FTP Source: https://files-sdk.dev/docs/adapters/ftp ## Installation `basic-ftp` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk basic-ftp ``` ## Usage FTP and FTPS via the [`basic-ftp`](https://www.npmjs.com/package/basic-ftp) library. Virtual keys map to paths under a configurable `root` on the server, with a `..` traversal guard. **Node-only** — FTP uses raw sockets, so this adapter does not run on edge/browser/Workers runtimes. By default the adapter opens a fresh connection per operation and closes it afterwards. For batch work, connect once and pass the `client` so every call reuses the same connection — you own its lifecycle. ```ts lineNumbers import { Files } from "files-sdk"; import { ftp } from "files-sdk/ftp"; const files = new Files({ adapter: ftp({ host: "ftp.example.com", user: process.env.FTP_USERNAME!, password: process.env.FTP_PASSWORD!, secure: true, // FTPS over explicit TLS — strongly recommended root: "/uploads", // virtual keys resolve under here; defaults to "." }), }); await files.upload("reports/q1.csv", csv, { contentType: "text/csv" }); const file = await files.download("reports/q1.csv"); ``` Auth falls back to `FTP_HOST`, `FTP_USERNAME` (alias `FTP_USER`), `FTP_PASSWORD`, `FTP_SECURE` (`"true"` or `"implicit"`), and `FTP_PORT` (default `21`) when the matching option is omitted. > **Plain FTP is cleartext.** Without `secure`, credentials and file contents are transmitted unencrypted. Prefer `secure: true` (explicit TLS / AUTH TLS); use `"implicit"` only for legacy servers. ## Options ## Limitations Connect-per-operation means a high call rate becomes a high connection rate, and FTP servers commonly cap connections per IP - inject a pre-connected `client` for batch jobs. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | User `metadata` and `cacheControl` throw - FTP files have no arbitrary-metadata or cache-header field. `contentType` is accepted for the return value but not stored (it's inferred from the key's extension on read). Stream bodies upload directly. Node-only (raw sockets). | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | Walks the directory tree recursively on every call - FTP has no native prefix scan or pagination - and skips symlinks. `prefix`/`limit`/`cursor` are applied client-side over the full walk, so they're accurate but a large tree means a full traversal per call. Content type is inferred from each key's extension; `lastModified` comes from the listing. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | FTP stores no content type, etag, or user metadata - `head()` infers the type from the key's extension (or `application/octet-stream`) and returns no etag. `size` comes from `SIZE`; `lastModified` is an `MDTM` probe that many servers don't support, so it can be absent. | | `exists` | ✅ | | | `copy` | ⚠️ | Read-then-write - FTP has no server-side copy, so the source is downloaded and re-uploaded over one connection. The whole object is buffered in memory; not atomic. | | `url` | ❌ | Throws unless `publicBaseUrl` is set (an HTTP server fronting the same tree), in which case it returns `/`. FTP serves no HTTP and has no signing primitive. `responseContentDisposition` always throws because the HTTP-front URL cannot bind the override. | | `signedUploadUrl` | ❌ | Throws - FTP has no presigned-upload concept. Use `upload()`, or inject a pre-connected `client` for batch transfers. | --- # Google Cloud Storage Source: https://files-sdk.dev/docs/adapters/gcs ## Installation `@google-cloud/storage` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @google-cloud/storage ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { gcs } from "files-sdk/gcs"; const files = new Files({ adapter: gcs({ bucket: "uploads", // No credentials needed in most setups - the @google-cloud/storage // SDK auto-discovers Application Default Credentials from // GOOGLE_APPLICATION_CREDENTIALS, gcloud auth, or the runtime // service account on Cloud Run / GKE / GCE. }), }); ``` `upload` reports true byte-level [progress](/docs/api/upload#progress-tracking) via `onProgress`. Uploads default to a single simple request; when `onProgress` is passed, the adapter switches to a **resumable** upload (the only path that emits progress), which adds one round trip. ## Options ## Compatibility | Method | Status | Notes | | ----------------- | :----: | ----- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Google Drive Source: https://files-sdk.dev/docs/adapters/google-drive ## Installation `@googleapis/drive` and `google-auth-library` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @googleapis/drive google-auth-library ``` ## Usage Google Drive via the official `@googleapis/drive` v3 client. Drive is a document manager rather than object storage - files have opaque `fileId`s and names can collide, so the adapter maps a unified string key onto Drive's `appProperties` (`fsdkKey`), with a per-instance LRU so reads after the first don't re-issue a lookup. Four auth modes: service-account credentials (inline or via key file), an OAuth refresh token, a pre-built Drive client (the escape hatch), or env-var fallback. ```ts lineNumbers import { Files } from "files-sdk"; import { googleDrive } from "files-sdk/google-drive"; // Service account into a Shared Drive (recommended - the default // service-account quota is 15 GB and not really intended for storage). // Add the service account as a member of the Shared Drive in the // Google Workspace admin console first. const files = new Files({ adapter: googleDrive({ credentials: { client_email: process.env.GOOGLE_DRIVE_CLIENT_EMAIL!, private_key: process.env.GOOGLE_DRIVE_PRIVATE_KEY!, }, driveId: process.env.GOOGLE_DRIVE_ID!, // Shared Drive root id, or a sub-folder id to scope the "bucket". rootFolderId: process.env.GOOGLE_DRIVE_ID!, // publicByDefault: true → grants anyone-with-link reader on upload // and url() returns the Drive download URL. }), }); ``` ## Options ## Limitations Two files with the same virtual key (created out-of-band) make resolution throw `Conflict` rather than picking one silently. User `metadata` keys starting with `fsdk` are reserved - the adapter uses that prefix on Drive's `appProperties` for bookkeeping. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | Drive has no native key field. The adapter scopes by parent folder and filters client-side to files carrying its `fsdkKey` appProperty - files written into the same folder out-of-band are excluded. `prefix` is filtered page-local and can under-return when the prefix isn't satisfied within a single page. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ✅ | | | `exists` | ⚠️ | Drive has no native key field. The adapter resolves by parent folder + `fsdkKey` appProperty, so files written into the same folder out-of-band return `false` even if a file with that name exists. | | `copy` | ✅ | | | `url` | ⚠️ | Throws by default - Drive has no signed URL primitive. With `publicByDefault: true` at construction, `upload()` grants `anyone, reader` and `url()` returns the permanent Drive download URL (`expiresIn` ignored). `responseContentDisposition` always throws - Drive's download URL has no Content-Disposition override. | | `signedUploadUrl` | ⚠️ | Initiates a Drive resumable session via `POST /upload/drive/v3/files?uploadType=resumable` and returns the session URL as a one-shot PUT. `maxSize` and `minSize` throw because Drive sessions do not enforce a server-side `content-length-range` policy; enforce size limits at your application gateway instead. Throws when the adapter was constructed via the pre-built `client` escape hatch (no auth handle to mint access tokens). | --- # Hetzner Object Storage Source: https://files-sdk.dev/docs/adapters/hetzner ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { hetzner } from "files-sdk/hetzner"; const files = new Files({ adapter: hetzner({ bucket: "uploads", region: "fsn1", // or "nbg1", "hel1" // accessKeyId / secretAccessKey auto-loaded from // HCLOUD_ACCESS_KEY_ID / HCLOUD_SECRET_ACCESS_KEY }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # IBM Cloud Object Storage Source: https://files-sdk.dev/docs/adapters/ibm-cos ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { ibmCos } from "files-sdk/ibm-cos"; const files = new Files({ adapter: ibmCos({ bucket: "uploads", region: "us-south", // or "eu-de", "jp-tok", "au-syd", ... // accessKeyId / secretAccessKey auto-loaded from // IBM_COS_ACCESS_KEY_ID / IBM_COS_SECRET_ACCESS_KEY (HMAC credentials) }), }); ``` IBM Cloud Object Storage via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the region code (`us-south`, `us-east`, `eu-de`, `eu-gb`, `jp-tok`, `au-syd`, `br-sao`, `ca-tor`, ...), virtual-hosted-style addressing, errors relabelled. Auth uses IBM Cloud's _HMAC_ credentials (not IAM API keys) - tick "Include HMAC Credential" under Advanced options when creating the service credential. Auto-loads from `IBM_COS_ACCESS_KEY_ID` and `IBM_COS_SECRET_ACCESS_KEY`. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # iDrive e2 Source: https://files-sdk.dev/docs/adapters/idrive-e2 ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { idriveE2 } from "files-sdk/idrive-e2"; const files = new Files({ adapter: idriveE2({ bucket: "uploads", endpoint: "https://q9z7.va.idrivee2-NN.com", // accessKeyId / secretAccessKey auto-loaded from // IDRIVE_E2_ACCESS_KEY_ID / IDRIVE_E2_SECRET_ACCESS_KEY }), }); ``` iDrive e2 via its S3-compatible API. A thin wrapper around the S3 adapter with iDrive-friendly defaults - endpoint is required (iDrive e2 hostnames are tied to the cluster your bucket lives in and don't follow a public pattern; copy it from the iDrive e2 dashboard), region defaulted, errors relabelled. Auto-loads from `IDRIVE_E2_ACCESS_KEY_ID` and `IDRIVE_E2_SECRET_ACCESS_KEY`. Generate access keys in the iDrive e2 dashboard under Access Keys. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # In-Memory Source: https://files-sdk.dev/docs/adapters/memory ## Installation This adapter has no peer dependencies and no runtime requirements - it's pure JavaScript backed by a `Map`, so it runs unchanged in Node, Bun, Deno, the browser, and edge runtimes. ```package-install files-sdk ``` ## Usage The in-memory adapter implements the same `Adapter` contract as every cloud adapter, but stores objects in a `Map` instead of touching disk or a network. That makes it the near-universal choice for testing code that uses `Files` without standing up real storage - and it doubles as the reference adapter, since it's the smallest complete implementation. ```ts lineNumbers import { Files } from "files-sdk"; import { memory } from "files-sdk/memory"; // A fresh, empty store. Same Adapter contract as the cloud adapters, // so you can swap it in for tests without changing any call sites. const files = new Files({ adapter: memory(), }); await files.upload("hello.txt", "hi"); const file = await files.download("hello.txt"); await file.text(); // "hi" ``` ### Seeding fixtures Pass `initial` to pre-populate the store - handy when a test needs objects present up front. Each value is a body (a string or bytes) or an object that also pins `contentType` / `metadata` / `cacheControl`, the way an `upload()` call would. ```ts lineNumbers import { memory } from "files-sdk/memory"; const adapter = memory({ initial: { "users/1.json": '{"id":1}', "logo.png": pngBytes, // a Uint8Array "report.csv": { body: "a,b,c\n1,2,3", contentType: "text/csv", metadata: { owner: "alice" }, }, }, }); ``` The constructor is synchronous, so seed values must convert to bytes without awaiting - that rules out `Blob`/`File` and `ReadableStream`. Seed those by calling `upload()` after construction. ## Options ## Inspecting the store The `raw` escape hatch is the backing `Map`, so a test can read or reset it directly without going through the adapter: ```ts lineNumbers const adapter = memory(); const files = new Files({ adapter }); await files.upload("a.txt", "x"); adapter.raw.size; // 1 adapter.raw.has("a.txt"); // true adapter.raw.clear(); // wipe between tests ``` ## Behavior notes - **Non-persistent.** The store lives in the process. Everything is lost when the process exits, and two `Files` instances built from separate `memory()` calls don't share data. Not for production. - **Value semantics.** Bodies are copied in on upload (and on seed), so mutating a `Uint8Array` you passed in later doesn't change the stored bytes. - **Content ETag.** The `etag` is a stable hash of the bytes (a pure-JS polynomial rolling hash, no `node:crypto`), so re-uploading identical content yields the same ETag - matching how real backends behave. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | Pause/resume via `control` works in-process; a token cannot resume in a new instance (the store lives in process memory). | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ⚠️ | Returns an opaque, non-fetchable `memory://{key}` URL - there's no server backing the store. Throws `NotFound` for a missing key (the way a real fetch would 404). `expiresIn` and `responseContentDisposition` round-trip as query params so URL-building call sites stay testable, but nothing resolves the URL. For working dev URLs use the `fs` adapter with `urlBaseUrl`, or a real cloud adapter. | | `signedUploadUrl` | ⚠️ | Returns an inert `memory://{key}` placeholder PUT target - there's no real upload endpoint, so a client can't actually PUT to it. `expiresIn` round-trips into the URL and `contentType` into the returned headers so the signing flow stays testable; `maxSize` and `minSize` are ignored. Use `upload()` to write bytes. | --- # MinIO Source: https://files-sdk.dev/docs/adapters/minio ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { minio } from "files-sdk/minio"; const files = new Files({ adapter: minio({ bucket: "uploads", endpoint: "http://localhost:9000", // accessKeyId / secretAccessKey auto-loaded from // MINIO_ACCESS_KEY_ID / MINIO_SECRET_ACCESS_KEY }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Neon Source: https://files-sdk.dev/docs/adapters/neon ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { neon } from "files-sdk/neon"; const files = new Files({ adapter: neon({ bucket: "images", // endpoint defaults to AWS_ENDPOINT_URL_S3 // region defaults to AWS_REGION (then NEON_STORAGE_REGION, then us-east-1) // accessKeyId / secretAccessKey resolve from the AWS credential chain // (the AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY Neon injects) }), }); ``` Neon branchable object storage via its S3-compatible API. A thin wrapper around the S3 adapter - errors relabelled, path-style addressing on by default. Declare a bucket in your `neon.ts` policy (`preview.buckets`), then run `neon dev` (or `neon env pull`) to mint a branch credential and inject the standard `AWS_*` variables. Inside a deployed Neon Function the same variables are present, so the adapter works from env alone: ```ts lineNumbers const files = new Files({ adapter: neon({ bucket: "images" }) }); await files.upload("avatars/abc.png", file, { contentType: "image/png" }); const url = await files.url("avatars/abc.png", { expiresIn: 300 }); ``` Path-style addressing is **required**: Neon's wildcard TLS certificate covers a single subdomain level (`*.storage.`), which the branch id occupies, so the bucket name must travel in the request path. The adapter always uses path-style addressing. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Netlify Blobs Source: https://files-sdk.dev/docs/adapters/netlify-blobs ## Installation `@netlify/blobs` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @netlify/blobs ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { netlifyBlobs } from "files-sdk/netlify-blobs"; // On Netlify Functions / Edge / build runtimes, siteID + token are // auto-detected from NETLIFY_BLOBS_CONTEXT - pass them explicitly only // when running outside Netlify (e.g. local scripts, your own server). const files = new Files({ adapter: netlifyBlobs({ name: "uploads", // siteID: process.env.NETLIFY_SITE_ID, // token: process.env.NETLIFY_API_TOKEN, // deployScoped: false, // true uses getDeployStore() // consistency: "eventual", // or "strong" }), }); ``` Netlify Blobs has no native size, content-type, or last-modified fields, so the adapter packs them - plus `cacheControl` and user `metadata` - into Netlify's metadata map at upload time. `head()` and `download()` read them back, so the unified `StoredFile` shape works the same as on the cloud adapters. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Stream bodies are buffered up-front - Netlify's `set()` has no streaming form, so streaming uploads can't avoid materializing the body in memory. Resumable uploads (`control`) are not supported — Netlify Blobs has no chunked/resumable upload primitive. | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | Netlify's list response only carries key + etag - size, content type, and last-modified come from a follow-up `head()` per item, so list entries return `size: 0` and `type: 'application/octet-stream'` by default. The unified `cursor` is not honoured because Netlify's pagination cursor is internal to the SDK; the adapter iterates the SDK's paginated form and stops once `limit` is satisfied, so `limit` does bound server-side I/O. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | Netlify Blobs has no native size, content-type, or last-modified - the adapter packs them into Netlify's metadata at upload time and reads them back via `getMetadata`. Blobs written outside the SDK come back with `size: 0` and `type: 'application/octet-stream'` because the embedded fields are absent. | | `exists` | ✅ | | | `copy` | ⚠️ | Read-then-write - Netlify Blobs has no server-side copy primitive, so the source is fetched and re-uploaded. Not server-side atomic; concurrent writes to the source between the get and put are not detected. | | `url` | ❌ | No URL primitive - Netlify Blobs has no public URL or signing endpoint; reads always go through the SDK with the token. Use `download()` instead, or proxy the body through your application. | | `signedUploadUrl` | ❌ | No presigned upload primitive - Netlify Blobs writes go through the SDK with the token. Upload server-side via the SDK or proxy uploads through your application. | --- # OneDrive Source: https://files-sdk.dev/docs/adapters/onedrive ## Installation `@azure/identity` and `@microsoft/microsoft-graph-client` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @azure/identity @microsoft/microsoft-graph-client ``` ## Usage OneDrive and SharePoint document libraries via the official `@microsoft/microsoft-graph-client` SDK. Microsoft Graph is path-addressable (`/drive/root:/folder/file.txt`), so the adapter maps virtual keys onto real OneDrive paths - no virtual-key cache, no `fsdkKey` bookkeeping. Five auth shapes (app-only, OAuth refresh token, raw access token, pre-built Graph client, or env-var fallback) and four drive targets (`/me/drive`, `driveId`, `siteId`, `userId`) cover the personal-OneDrive, OneDrive-for-Business, and SharePoint-site-library cases. ```ts lineNumbers import { Files } from "files-sdk"; import { onedrive } from "files-sdk/onedrive"; // App-only auth (client credentials) into a SharePoint site library. // Cannot use /me/drive - pass driveId, siteId, or userId instead. const files = new Files({ adapter: onedrive({ clientCredentials: { tenantId: process.env.ONEDRIVE_TENANT_ID!, clientId: process.env.ONEDRIVE_CLIENT_ID!, clientSecret: process.env.ONEDRIVE_CLIENT_SECRET!, }, siteId: process.env.ONEDRIVE_SITE_ID!, rootFolderPath: "Uploads", // publicByDefault: true → upload() also creates an anonymous-view // sharing link and url() returns its webUrl. }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Single-PUT simple upload up to OneDrive's 250 MB limit; larger bodies (or any `multipart` upload) automatically switch to a chunked Graph upload session. `multipart.partSize` tunes the chunk size (rounded to a 320 KiB multiple). User `metadata` and `cacheControl` throw - Graph drive items have no native arbitrary-metadata field; use `raw` to set Open Extensions if you need them. | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | Returns immediate-children files only at `rootFolderPath` - no recursion, and subfolders are filtered out. `prefix` is filename-prefix only (matched client-side within the page). Pagination uses Graph's `@odata.nextLink` as the opaque cursor. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Async copy on Graph (`POST /items/{id}/copy` returns 202 + monitor URL). The adapter polls the monitor every 500 ms until status is `completed`/`failed`, capped by `copyTimeoutMs` (default 60_000). On timeout the call throws `Provider`; tune `copyTimeoutMs` for large files. | | `url` | ⚠️ | Throws by default - Graph has no signed URL primitive. With `publicByDefault: true` at construction, `upload()` calls `createLink` (anonymous-view scope) and `url()` returns the share link's `webUrl`. The link is permanent (`expiresIn` ignored) and `responseContentDisposition` always throws - Graph has no Content-Disposition override. Anonymous links are blocked on tenants where admins disable them. | | `signedUploadUrl` | ⚠️ | Initiates a Graph upload session via `POST /createUploadSession` and returns the session URL as a one-shot PUT (the session URL is pre-authenticated by Graph itself). `maxSize` and `minSize` throw because Graph does not enforce a server-side `content-length-range` policy on upload sessions; enforce size limits at your application gateway instead. | --- # Oracle Cloud Object Storage Source: https://files-sdk.dev/docs/adapters/oracle-cloud ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { oracleCloud } from "files-sdk/oracle-cloud"; const files = new Files({ adapter: oracleCloud({ bucket: "uploads", namespace: "axoki12345", // tenancy Object Storage namespace region: "us-ashburn-1", // accessKeyId / secretAccessKey auto-loaded from // OCI_ACCESS_KEY_ID / OCI_SECRET_ACCESS_KEY (Customer Secret Keys) }), }); ``` Oracle Cloud Infrastructure Object Storage via its S3 compatibility layer. A thin wrapper around the S3 adapter - endpoint derived from your tenancy namespace and region (`.compat.objectstorage..oraclecloud.com`), path-style addressing on (OCI's TLS cert doesn't cover bucket subdomains under the namespace prefix), errors relabelled. Auth uses OCI's HMAC _Customer Secret Keys_, not regular API signing keys - generate them under Profile -> User Settings -> Customer Secret Keys. Auto-loads from `OCI_ACCESS_KEY_ID` and `OCI_SECRET_ACCESS_KEY`. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # OVHcloud Object Storage Source: https://files-sdk.dev/docs/adapters/ovhcloud ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { ovhcloud } from "files-sdk/ovhcloud"; const files = new Files({ adapter: ovhcloud({ bucket: "uploads", region: "gra", // or "sbg", "de", "uk", "waw", "sgp", "syd" // accessKeyId / secretAccessKey auto-loaded from // OVH_ACCESS_KEY_ID / OVH_SECRET_ACCESS_KEY }), }); ``` OVHcloud Object Storage (High Performance S3) via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the region code (`gra`, `sbg`, `bhs`, `de`, `uk`, `waw`, `sgp`, `syd`), virtual-hosted-style addressing, errors relabelled. For the Standard (Swift-backed) tier, pass `https://s3..cloud.ovh.net` as the explicit `endpoint`. Auto-loads from `OVH_ACCESS_KEY_ID` and `OVH_SECRET_ACCESS_KEY`. Generate S3 credentials in the OVHcloud Control Panel under Public Cloud -> Object Storage -> S3 users. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # PocketBase Source: https://files-sdk.dev/docs/adapters/pocketbase ## Installation `pocketbase` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk pocketbase ``` ## Usage PocketBase via the official `pocketbase` JS SDK. PocketBase has no object-store primitive - files live as field values on records inside collections. The adapter maps the unified key/blob API onto a dedicated collection: each upload becomes (or updates) a record whose configurable _key field_ holds the user-facing string key and whose configurable _file field_ holds the body. ```ts lineNumbers import { Files } from "files-sdk"; import { pocketbase } from "files-sdk/pocketbase"; const files = new Files({ adapter: pocketbase({ collection: "files", // Auto-loads url + auth from POCKETBASE_URL, // POCKETBASE_ADMIN_EMAIL + POCKETBASE_ADMIN_PASSWORD, or // POCKETBASE_AUTH_TOKEN. Or pass an existing PocketBase client. // // Collection must already exist with a unique-indexed text `key` // field and a single-value `file` field. Field names are // configurable via `keyField` / `fileField`. }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Stream bodies are buffered up-front - the SDK uploads via multipart `FormData` with a Blob, which has no streaming form. User `metadata` and `cacheControl` throw - PocketBase has no per-file HTTP cache headers and no arbitrary-metadata field on the file; add extra typed columns to the collection and write via `raw` if you need them. Existing keys are updated in place (no duplicate-key error); new keys create a new record. Resumable uploads (`control`) are not supported — PocketBase uploads in a single multipart request. | | `download` | ⚠️ | No streaming primitive - PocketBase's JS SDK has no binary download API, so the adapter resolves the record, mints a short-lived file token via `pb.files.getToken()` when authenticated, and fetches the file URL with `fetch()`. Size and content-type come back from the HTTP response, not the record - PB doesn't store them on the record itself. | | `delete` | ✅ | | | `list` | ⚠️ | PocketBase's stable list API is offset/limit (page/perPage), not cursor-based. The adapter encodes the next page number as a numeric cursor string so the unified API works unchanged. `prefix` is matched server-side via the `~` operator on the configured `keyField`. List items expose lazy bodies (one fetch per `.text()`/`.arrayBuffer()` call) — PocketBase records don't carry size or content-type, so list entries return `size: 0` and `type: 'application/octet-stream'` until the body is read. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | PocketBase records don't carry size, content-type, or etag for their file fields, so `head()` returns `size: 0` and `type: 'application/octet-stream'` until the body is read via the lazy body factory. `lastModified` is sourced from the record's `updated` field. The filename PocketBase generated on upload is exposed under `metadata.filename`. | | `exists` | ✅ | | | `copy` | ⚠️ | Read-then-write — PocketBase has no server-side copy primitive, so the source record's file is downloaded and uploaded as a new record under the destination key. Costs an egress + an ingest; not atomic. | | `url` | ⚠️ | Default returns `pb.files.getURL(record, filename)` — permanent for public collections, threaded with a short-lived file token from `pb.files.getToken()` when the client is authenticated. With `publicBaseUrl`, returns `/`. `expiresIn` is silently ignored — PocketBase fixes the file-token TTL server-side. `responseContentDisposition` always throws — PB has no per-URL Content-Disposition override; use `raw` and the `?download=true` query string instead. | | `signedUploadUrl` | ❌ | Throws — PocketBase has no presigned upload primitive. Writes always go through the authenticated API; mint a short-lived auth token for the client and call `create`/`update` directly, or proxy uploads through your application. | --- # Cloudflare R2 Source: https://files-sdk.dev/docs/adapters/r2 ## Installation The `@aws-sdk/*` packages are only needed for the default `"aws-sdk"` HTTP client. The Workers binding path, hybrid signing, and the [lightweight fetch client](#lightweight-fetch-client) need none of them - `files-sdk` alone is enough: ```package-install files-sdk ``` For the default `"aws-sdk"` HTTP client, `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` Over the HTTP API with the `"aws-sdk"` client, `upload` reports true byte-level [progress](/docs/api/upload#progress-tracking) via `onProgress` when the optional `@aws-sdk/lib-storage` package is installed (it's loaded only when `onProgress` is used). Under the Workers `R2Bucket` binding and the fetch client the SDK reports progress generically instead — byte-level for stream bodies, start and finish for buffered ones. ## Usage Cloudflare R2 over the S3-compatible HTTP API. Auto-loads from `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`. Inside Cloudflare Workers you can pass an `R2Bucket` binding directly instead. ```ts lineNumbers import { Files } from "files-sdk"; import { r2 } from "files-sdk/r2"; const files = new Files({ adapter: r2({ bucket: "uploads", accountId: process.env.R2_ACCOUNT_ID!, // accessKeyId / secretAccessKey auto-loaded // from R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY }), }); ``` `publicBaseUrl` - optional, an `r2.dev` subdomain or custom domain bound to the bucket. When set, `url()` returns `` `${publicBaseUrl}/${key}` `` and skips signing. ## Lightweight fetch client Pass `client: "fetch"` to swap the `@aws-sdk/*` stack for a SigV4-signed `fetch` engine built on [aws4fetch](https://github.com/mhart/aws4fetch) (~2.5 KB gzipped, Web Crypto only). No `@aws-sdk/*` packages are installed or bundled - ideal for Cloudflare Workers and other edge runtimes where the AWS SDK's ~500 KB defeats the point of web-standard tooling. ```ts lineNumbers const files = new Files({ adapter: r2({ bucket: "uploads", accountId: process.env.R2_ACCOUNT_ID!, client: "fetch", }), }); ``` The fetch client covers `upload`, `download` (including [ranges](/docs/api/download#byte-ranges)), `head`, `exists`, `delete`, `list` (including `delimiter` folding), server-side `copy`, presigned `url()`, and `signedUploadUrl()`. Trade-offs against the default `"aws-sdk"` client: - `ReadableStream` bodies are buffered in memory before a single PUT (a lone PUT needs a `Content-Length`, and single-request uploads cap at 5 GB on R2). - `multipart` and resumable (`control`) uploads throw instead of engaging the S3 multipart API. - Bulk deletes fan out as per-key `delete()` calls instead of batched `DeleteObjects` requests. - Byte-level `onProgress` reporting falls back to the SDK's generic reporting. ## Options `R2AdapterOptions` is a union of two shapes depending on whether you have a Workers `R2Bucket` binding available. ### HTTP mode ### Binding mode (inside a Worker) ## Hybrid: binding + HTTP credentials Inside a Worker, you can pass _both_ a binding and HTTP credentials. Reads and writes go through the binding (no egress, no extra round trip); `url()` and `signedUploadUrl()` route through an S3-compatible SigV4 signer because a Worker binding has no signing primitive. Hybrid signing runs on [aws4fetch](https://github.com/mhart/aws4fetch) (Web Crypto only), so neither the binding path nor hybrid mode ever pulls `@aws-sdk/*` packages into the Worker bundle. ```ts lineNumbers // Inside a Cloudflare Worker. The binding handles uploads/downloads // (intra-Worker, no egress fees). The HTTP credentials let url() and // signedUploadUrl() sign presigned URLs the binding alone can't produce. const files = new Files({ adapter: r2({ binding: env.UPLOADS, bucket: "uploads", accountId: env.R2_ACCOUNT_ID, accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY, }), }); ``` ## Signed uploads and `maxSize` [`signedUploadUrl()`](/docs/api/signed-upload-url) returns a presigned **PUT** URL. Unlike S3, R2 does **not** implement the S3 `POST Object` API, so it has no `content-length-range` policy to enforce an upload size cap at the bucket. Passing `maxSize` throws a `Provider` error rather than handing back a POST form that R2 would reject with `501 Not Implemented` at upload time. ```ts lineNumbers // ✅ presigned PUT — the browser uploads with fetch(url, { method: "PUT", body: file }) const upload = await files.signedUploadUrl("avatars/abc.png", { expiresIn: 60, contentType: "image/png", }); // ❌ throws: R2 has no server-enforced size limit await files.signedUploadUrl("avatars/abc.png", { expiresIn: 60, maxSize: 5_000_000, }); ``` To cap upload sizes on R2, enforce the limit at your application gateway before issuing the URL. ## Compatibility ### HTTP mode | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ⚠️ | PUT URL only - Cloudflare R2 doesn't implement the S3 POST Object API, so `maxSize` throws (no `content-length-range` policy; a presigned POST would 501 at upload time). Enforce upload caps at your application gateway instead. | ### HTTP mode (`client: "fetch"`) | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Single PUT - `ReadableStream` bodies are buffered in memory first, and `multipart` / resumable `control` uploads throw. Use the `"aws-sdk"` client for multipart. | | `download` | ✅ | | | `delete` | ✅ | Bulk deletes fan out per key (no batched `DeleteObjects`). | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ⚠️ | PUT URL only - same `maxSize` limitation as above. | ### Binding mode | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Read-then-write - Workers bindings have no native copy command, so the source is fetched and re-uploaded. Not server-side atomic; concurrent writes to the source between the get and put are not detected. | | `url` | ❌ | Throws unless `publicBaseUrl` is set on the adapter (an r2.dev subdomain or a custom domain). For a presigned URL from a Worker, switch to hybrid mode by also passing `accountId` + `accessKeyId` + `secretAccessKey`. | | `signedUploadUrl` | ❌ | Workers bindings can't sign uploads - the secret access key is not available to the runtime. Use hybrid mode (binding + HTTP credentials) to issue presigned upload URLs. | ### Hybrid mode | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Read-then-write - copy goes through the binding (no native copy command on Workers). | | `url` | ✅ | | | `signedUploadUrl` | ⚠️ | PUT URL only - signing routes through the HTTP signer. R2 doesn't implement the S3 POST Object API, so `maxSize` throws (no `content-length-range` policy; a presigned POST would 501 at upload time). Enforce upload caps at your application gateway instead. | --- # Amazon S3 Source: https://files-sdk.dev/docs/adapters/s3 ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` To report true byte-level [upload progress](/docs/api/upload#progress-tracking) via `upload`'s `onProgress` option, also install `@aws-sdk/lib-storage` — it's optional and only loaded when `onProgress` is used. ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { s3 } from "files-sdk/s3"; const files = new Files({ adapter: s3({ bucket: "uploads", region: "us-east-1", // credentials auto-loaded from the AWS chain // (env vars, IAM role, shared profile, ...) }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Scaleway Object Storage Source: https://files-sdk.dev/docs/adapters/scaleway ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { scaleway } from "files-sdk/scaleway"; const files = new Files({ adapter: scaleway({ bucket: "uploads", region: "fr-par", // or "nl-ams", "pl-waw" // accessKeyId / secretAccessKey auto-loaded from // SCW_ACCESS_KEY / SCW_SECRET_KEY }), }); ``` Scaleway Object Storage via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the region code (`fr-par`, `nl-ams`, `pl-waw`), virtual-hosted-style addressing, errors relabelled. Auto-loads from `SCW_ACCESS_KEY` and `SCW_SECRET_KEY`. Generate access keys in the Scaleway console under Identity and Access Management -> API Keys. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # SFTP Source: https://files-sdk.dev/docs/adapters/sftp ## Installation `ssh2-sftp-client` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk ssh2-sftp-client ``` ## Usage SFTP via the [`ssh2-sftp-client`](https://www.npmjs.com/package/ssh2-sftp-client) library (built on `ssh2`). Virtual keys map to paths under a configurable `root` on the remote server, with a `..` traversal guard. **Node-only** — SFTP uses raw sockets, so this adapter does not run on edge/browser/Workers runtimes. By default the adapter opens a fresh connection per operation and closes it afterwards. For batch work, connect once and pass the `client` so every call reuses the same connection — you own its lifecycle. ```ts lineNumbers import { Files } from "files-sdk"; import { sftp } from "files-sdk/sftp"; const files = new Files({ adapter: sftp({ host: "files.example.com", username: process.env.SFTP_USERNAME!, // Password or private-key auth (privateKey takes precedence): privateKey: process.env.SFTP_PRIVATE_KEY!, // passphrase: process.env.SFTP_PASSPHRASE, root: "/uploads", // virtual keys resolve under here; defaults to "." }), }); await files.upload("reports/q1.csv", csv, { contentType: "text/csv" }); const file = await files.download("reports/q1.csv"); ``` Auth falls back to `SFTP_HOST`, `SFTP_USERNAME`, `SFTP_PASSWORD`, `SFTP_PRIVATE_KEY`, `SFTP_PASSPHRASE`, and `SFTP_PORT` (default `22`) when the matching option is omitted. Pass `connectOptions` to forward anything else to `ssh2` (e.g. `hostVerifier`, `algorithms`, `agent`). ### Reusing a connection ```ts import SftpClient from "ssh2-sftp-client"; const client = new SftpClient(); await client.connect({ host: "files.example.com", username, privateKey }); const files = new Files({ adapter: sftp({ client }) }); // ...many operations over the one connection... await client.end(); ``` ## Options ## Limitations Connect-per-operation means a high call rate becomes a high connection rate, and SSH servers commonly cap sessions per IP - inject a pre-connected `client` (shown above) for batch jobs. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | User `metadata` and `cacheControl` throw - SFTP files have no arbitrary-metadata or cache-header field. `contentType` is accepted for the return value but not stored (it's inferred from the key's extension on read). Stream bodies upload directly. Node-only (raw sockets). | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | Walks the directory tree recursively on every call - SFTP has no native prefix scan or pagination - and skips symlinks. `prefix`/`limit`/`cursor` are applied client-side over the full walk, so they're accurate but a large tree means a full traversal per call. Content type is inferred from each key's extension; `size`/`lastModified` come from the listing. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | SFTP stores no content type, etag, or user metadata - `head()` infers the type from the key's extension (or `application/octet-stream`) and returns no etag. `size` and `lastModified` come from `stat`. | | `exists` | ✅ | | | `copy` | ⚠️ | Read-then-write - base SFTP has no portable server-side copy, so the source is downloaded and re-uploaded over one connection. The whole object is buffered in memory; not atomic. | | `url` | ❌ | Throws unless `publicBaseUrl` is set (an HTTP server fronting the same tree), in which case it returns `/`. SFTP serves no HTTP and has no signing primitive. `responseContentDisposition` always throws because the HTTP-front URL cannot bind the override. | | `signedUploadUrl` | ❌ | Throws - SFTP has no presigned-upload concept. Use `upload()`, or inject a pre-connected `client` for batch transfers. | --- # SharePoint Source: https://files-sdk.dev/docs/adapters/sharepoint ## Installation `@azure/identity` and `@microsoft/microsoft-graph-client` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @azure/identity @microsoft/microsoft-graph-client ``` ## Usage SharePoint document libraries via Microsoft Graph. Wraps the `onedrive` adapter and adds SharePoint-shaped resolution - `siteUrl` parsing, named `documentLibrary` lookup, and `SHAREPOINT_*` env-var fallbacks. Resolution is lazy: the first method call triggers Graph traffic to convert names into drive IDs, then subsequent calls reuse the resolved drive. ```ts lineNumbers import { Files } from "files-sdk"; import { sharepoint } from "files-sdk/sharepoint"; const files = new Files({ adapter: sharepoint({ siteUrl: "https://contoso.sharepoint.com/sites/marketing", documentLibrary: "Reports", // optional, omit for default library clientCredentials: { tenantId: process.env.SHAREPOINT_TENANT_ID!, clientId: process.env.SHAREPOINT_CLIENT_ID!, clientSecret: process.env.SHAREPOINT_CLIENT_SECRET!, }, rootFolderPath: "Uploads", }), }); ``` ## Options ## Limitations The adapter delegates to `onedrive` after resolution, so the OneDrive per-method caveats in the table below apply. SharePoint-specific: `siteUrl` parsing errors and missing `documentLibrary` names throw `Provider` on the first method call - resolution is lazy, so construction never fails for these, and the resolved drive is cached for the adapter's lifetime after 1-2 extra Graph round-trips on first use. `delete()` moves items to the recycle bin (soft delete). ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | Delegates to `onedrive` after site/library resolution: single-PUT simple upload up to OneDrive's 250 MB limit; larger bodies (or any `multipart` upload) automatically switch to a chunked Graph upload session. User `metadata` and `cacheControl` throw - Graph drive items have no native arbitrary-metadata field; use `raw` to set Open Extensions if you need them. | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | Delegates to `onedrive`: returns immediate-children files only at `rootFolderPath` - no recursion, and subfolders are filtered out. `prefix` is filename-prefix only (matched client-side within the page). Pagination uses Graph's `@odata.nextLink` as the opaque cursor. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ⚠️ | Delegates to `onedrive`: async copy on Graph (`POST /items/{id}/copy` returns 202 + monitor URL). The adapter polls the monitor every 500 ms until status is `completed`/`failed`, capped by `copyTimeoutMs` (default 60_000). On timeout the call throws `Provider`; tune `copyTimeoutMs` for large files. | | `url` | ⚠️ | Delegates to `onedrive`: throws by default - Graph has no signed URL primitive. With `publicByDefault: true` at construction, `upload()` calls `createLink` (anonymous-view scope) and `url()` returns the share link's `webUrl`. The link is permanent (`expiresIn` ignored) and `responseContentDisposition` always throws. Anonymous links are blocked on tenants where admins disable them. | | `signedUploadUrl` | ⚠️ | Delegates to `onedrive`: initiates a Graph upload session via `POST /createUploadSession` and returns the session URL as a one-shot PUT (the session URL is pre-authenticated by Graph itself). `maxSize` and `minSize` throw because Graph does not enforce a server-side `content-length-range` policy on upload sessions; enforce size limits at your application gateway instead. | --- # Storj Source: https://files-sdk.dev/docs/adapters/storj ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { storj } from "files-sdk/storj"; const files = new Files({ adapter: storj({ bucket: "uploads", // endpoint defaults to https://gateway.storjshare.io (Gateway MT). // Pass a self-hosted Gateway ST URL to override. // accessKeyId / secretAccessKey auto-loaded from // STORJ_ACCESS_KEY_ID / STORJ_SECRET_ACCESS_KEY }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Supabase Storage Source: https://files-sdk.dev/docs/adapters/supabase ## Installation `@supabase/storage-js` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @supabase/storage-js ``` ## Usage Supabase Storage via the official `@supabase/storage-js` SDK. Auto-loads the project URL and an API key from the standard env vars; pass `client` to share an existing `SupabaseClient` with the rest of your app (auth, postgrest). ```ts lineNumbers import { Files } from "files-sdk"; import { supabase } from "files-sdk/supabase"; const files = new Files({ adapter: supabase({ bucket: "uploads", // Auto-loads url + key from SUPABASE_URL / NEXT_PUBLIC_SUPABASE_URL // and SUPABASE_SERVICE_ROLE_KEY / SUPABASE_KEY / // NEXT_PUBLIC_SUPABASE_ANON_KEY. Or pass an existing SupabaseClient // via `client` to share with auth/postgrest. }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | Uses Supabase's V2 list API: a flat, recursive, string-prefix scan over full keys with a real server cursor. (The legacy V1 API is folder-scoped and non-recursive, so it could not back the unified `list` contract.) | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ⚠️ | Default mints a signed read URL via `createSignedUrl` (1-hour default). With `public: true`, returns the permanent unsigned `getPublicUrl` result. With `publicBaseUrl`, returns `/`. `responseContentDisposition` is honored - it threads through Supabase's `download` option in the signed path. | | `signedUploadUrl` | ⚠️ | PUT URL only - Supabase has no POST policy equivalent. `maxSize` throws (Supabase signed upload URLs have no `content-length-range` policy; set the bucket-level size limit in the dashboard instead). `expiresIn` is silently ignored - Supabase fixes the TTL at 2 hours server-side. The returned headers include `x-upsert: true`. | --- # Tencent Cloud Object Storage Source: https://files-sdk.dev/docs/adapters/tencent ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { tencent } from "files-sdk/tencent"; const files = new Files({ adapter: tencent({ bucket: "uploads-1250000000", // - region: "ap-guangzhou", // or "ap-shanghai", "na-siliconvalley", ... // accessKeyId / secretAccessKey auto-loaded from // TENCENT_SECRET_ID / TENCENT_SECRET_KEY }), }); ``` Tencent Cloud Object Storage (COS) via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the region code (`ap-guangzhou`, `ap-shanghai`, `na-siliconvalley`, ...), virtual-hosted-style addressing, errors relabelled. Auto-loads from `TENCENT_SECRET_ID` and `TENCENT_SECRET_KEY`. Generate API keys in the Tencent Cloud console under Cloud Access Management -> API Keys. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Tigris Source: https://files-sdk.dev/docs/adapters/tigris ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { tigris } from "files-sdk/tigris"; const files = new Files({ adapter: tigris({ bucket: "uploads", // endpoint defaults to https://fly.storage.tigris.dev // region defaults to "auto" (Tigris routes globally) // accessKeyId / secretAccessKey auto-loaded from // TIGRIS_ACCESS_KEY_ID / TIGRIS_SECRET_ACCESS_KEY }), }); ``` Tigris globally-distributed object storage via its S3-compatible API. A thin wrapper around the S3 adapter - fixed global endpoint, region defaults to `"auto"` for signing, virtual-hosted-style addressing, errors relabelled. Auto-loads from `TIGRIS_ACCESS_KEY_ID` and `TIGRIS_SECRET_ACCESS_KEY`. Generate access keys in the Tigris console (or via the Fly CLI: `fly storage create`). ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # UploadThing Source: https://files-sdk.dev/docs/adapters/uploadthing ## Installation `uploadthing` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk uploadthing ``` ## Usage UploadThing via the official `uploadthing/server` SDK. UploadThing generates its own internal file keys, so the adapter maps the user-supplied key onto UploadThing's `customId` with `defaultKeyType: "customId"` - every subsequent operation routes by your key, not the auto-generated one. ```ts lineNumbers import { Files } from "files-sdk"; import { uploadthing } from "files-sdk/uploadthing"; // UPLOADTHING_TOKEN is auto-loaded from env. The token is a base64 // JSON of { apiKey, appId, regions[] } - the adapter decodes it at // construction so url() can synthesize the public CDN URL and // signedUploadUrl() can sign a UFS PUT URL without an API round trip. const files = new Files({ adapter: uploadthing({ // acl: "public-read", // default; switch to "private" to mint // // signed URLs through generateSignedURL // slug: "mediaUploader", // required only for signedUploadUrl() }), }); ``` ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | User `metadata` and `cacheControl` aren't supported by the UFS API, so passing either throws rather than silently dropping it. Resumable uploads (`control`) are not supported — UploadThing manages chunking server-side and exposes no resumable session. | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | UploadThing's listFiles is offset/limit, not cursor-based - the adapter encodes the next offset as a numeric cursor. `prefix` is unsupported server-side; the adapter filters the returned page client-side, which under-returns when the prefix isn't satisfied within a single page. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | UploadThing has no metadata endpoint, so `head()` issues a HEAD request against the resolved file URL (signed for private, CDN for public) and parses size/content-type/etag/last-modified from the response headers. User `metadata` isn't supported. | | `exists` | ⚠️ | UploadThing has no metadata endpoint, so `exists()` issues a HEAD request against the resolved file URL (signed for private, CDN for public) and treats `404` as `false`. | | `copy` | ⚠️ | Read-then-write - UploadThing has no server-side copy primitive, so the source is downloaded and re-uploaded. Costs an egress + an ingest; not atomic. | | `url` | ⚠️ | Public adapters return the permanent CDN URL `https://{appId}.ufs.sh/f/{key}` and silently ignore `expiresIn`. Private adapters mint a signed read URL via `generateSignedURL` (1-hour default). `responseContentDisposition` throws either way - UploadThing has no Content-Disposition override on signed or CDN URLs. Use a private adapter for buckets with untrusted user-uploaded content. | | `signedUploadUrl` | ⚠️ | PUT URL only - built against UploadThing's UFS ingest endpoint with an HMAC-SHA256 signature over the URL. `maxSize` is advisory: UploadThing enforces upload caps via the file-router config tied to the adapter's `slug`, not via the URL signature. `minSize` is ignored (no equivalent on UFS). The user-supplied key is bound as `x-ut-custom-id` so subsequent ops can route by it. | --- # Vercel Blob Source: https://files-sdk.dev/docs/adapters/vercel-blob ## Installation `@vercel/blob` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @vercel/blob ``` ## Usage On Vercel, the adapter prefers Vercel's **OIDC authentication** when `VERCEL_OIDC_TOKEN` and `BLOB_STORE_ID` are present (both are auto-injected when the Blob store is connected to the project). OIDC tokens rotate automatically, so they remove the risk that a long-lived secret leaks from the codebase or environment. Off Vercel - or if OIDC isn't configured - the adapter falls back to `BLOB_READ_WRITE_TOKEN`. An explicit `token` option always wins. ```ts lineNumbers import { Files } from "files-sdk"; import { vercelBlob } from "files-sdk/vercel-blob"; // On Vercel: VERCEL_OIDC_TOKEN + BLOB_STORE_ID are auto-injected when the // Blob store is connected to the project (OIDC, recommended). Off Vercel, // or as a fallback, BLOB_READ_WRITE_TOKEN is used. const files = new Files({ adapter: vercelBlob() }); ``` Pass `oidcToken` and `storeId` directly for runtimes that don't expose `process.env` (Vite, etc.), or to bypass env detection entirely: ```ts lineNumbers // Frameworks that don't load .env.local into process.env (Vite, etc.) // need OIDC credentials passed explicitly. const files = new Files({ adapter: vercelBlob({ oidcToken: loadOidcToken(), storeId: loadStoreId(), }), }); ``` `downloadTimeoutMs` bounds the public-URL fetches issued by `download()` and the lazy bodies returned from `head()`/`list()`. Defaults to 5 minutes; pass `0` to disable. A hung CDN response would otherwise leak a fetch that never resolves. `access` selects public or private blobs and is fixed at construction. Default `"public"` matches the existing behavior. With `access: "private"`, uploads use Vercel's private mode and reads route through `blob.get()` with whichever credentials the adapter resolved (OIDC or read-write token) instead of a public URL fetch - there is no permanent public URL for private blobs, so `url()` throws. Need both? Use two adapters. ## Options ## Limitations User `metadata` isn't supported by the underlying API, so passing a non-empty `metadata` throws rather than silently dropping it. `cacheControl` is supported (it maps to the blob's `cacheControlMaxAge`). ## Compatibility ### Public access | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ⚠️ | Returns the permanent CDN URL. `expiresIn` is silently ignored (no signing primitive); `responseContentDisposition` throws (no Content-Disposition override available). Use a different provider for buckets with untrusted user-uploaded content. | | `signedUploadUrl` | ❌ | No presigned upload primitive. Use `handleUpload()` from `@vercel/blob/client` for browser uploads. | ### Private access | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ❌ | No URL primitive for private blobs - the underlying SDK requires an authenticated `blob.get()` call with the token. Use `download()` instead, or instantiate a second public-access adapter. | | `signedUploadUrl` | ❌ | No presigned upload primitive. Use `handleUpload()` from `@vercel/blob/client` for browser uploads. | --- # Vultr Object Storage Source: https://files-sdk.dev/docs/adapters/vultr ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { vultr } from "files-sdk/vultr"; const files = new Files({ adapter: vultr({ bucket: "uploads", region: "ewr", // or "sjc", "ams", "blr", "del", "sgp", "lux" // accessKeyId / secretAccessKey auto-loaded from // VULTR_ACCESS_KEY_ID / VULTR_SECRET_ACCESS_KEY }), }); ``` Vultr Object Storage via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the region code (`ewr`, `sjc`, `ams`, `blr`, `del`, `sgp`, `lux`), virtual-hosted-style addressing, errors relabelled. Auto-loads from `VULTR_ACCESS_KEY_ID` and `VULTR_SECRET_ACCESS_KEY`. Generate access keys in the Vultr customer portal under Object Storage -> your subscription -> Overview. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Wasabi Source: https://files-sdk.dev/docs/adapters/wasabi ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { wasabi } from "files-sdk/wasabi"; const files = new Files({ adapter: wasabi({ bucket: "uploads", region: "us-east-1", // or "eu-central-1", "ap-northeast-1", ... // accessKeyId / secretAccessKey auto-loaded from // WASABI_ACCESS_KEY_ID / WASABI_SECRET_ACCESS_KEY }), }); ``` Wasabi Hot Cloud Storage via its S3-compatible API. A thin wrapper around the S3 adapter - endpoint derived from the region code (`us-east-1`, `eu-central-1`, `ap-northeast-1`, ...), virtual-hosted-style addressing, errors relabelled. Region names mirror AWS but the endpoints are Wasabi's own. Auto-loads from `WASABI_ACCESS_KEY_ID` and `WASABI_SECRET_ACCESS_KEY`. Generate access keys in the Wasabi console under Access Keys. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # WebDAV Source: https://files-sdk.dev/docs/adapters/webdav ## Installation `webdav` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk webdav ``` ## Usage WebDAV via the [`webdav`](https://www.npmjs.com/package/webdav) library. Virtual keys map to paths under a configurable `root` on the server, with a `..` traversal guard. Because WebDAV is plain HTTP (`PROPFIND` / `GET` / `PUT` / `COPY` / `MOVE` / `DELETE`), the adapter is transport-agnostic and works against Nextcloud, ownCloud, Apache `mod_dav`, `sabre/dav`, box.com, and most NAS boxes. ```ts lineNumbers import { Files } from "files-sdk"; import { webdav } from "files-sdk/webdav"; const files = new Files({ adapter: webdav({ baseUrl: "https://cloud.example.com/remote.php/dav/files/alice", username: process.env.WEBDAV_USERNAME!, password: process.env.WEBDAV_PASSWORD!, // authType: "digest", // "basic" (default) | "digest" | "token" | "none" root: "/uploads", // virtual keys resolve under here; defaults to "/" }), }); await files.upload("reports/q1.csv", csv, { contentType: "text/csv" }); const file = await files.download("reports/q1.csv"); ``` Config falls back to `WEBDAV_URL` (alias `WEBDAV_BASE_URL`), `WEBDAV_USERNAME` (alias `WEBDAV_USER`), `WEBDAV_PASSWORD`, and `WEBDAV_AUTH_TYPE` when the matching option is omitted. For OAuth, pass `authType: "token"` with a `token` object. ### Reusing a client The `webdav` client is stateless - it holds config and issues a fresh HTTP request per call - so there's no connection to pool. Pass a pre-configured `client` when you want to control its options (custom headers, a shared instance) directly: ```ts import { createClient } from "webdav"; const client = createClient( "https://cloud.example.com/remote.php/dav/files/alice", { username, password, } ); const files = new Files({ adapter: webdav({ client }) }); ``` ## Options ## Limitations `list` walks the collection tree with one `PROPFIND` per directory rather than a single `Depth: infinity` request - many servers disable infinite-depth listing, so the walk is the portable choice, but a large tree means many round-trips per `list()` call. There's no resumable/chunked upload: WebDAV has no portable append primitive, so `upload({ control })` is unsupported and unknown-length streams are buffered before the `PUT`. ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ⚠️ | User `metadata` and `cacheControl` throw - WebDAV has no arbitrary-metadata or cache-header field. `contentType` is sent as the `PUT` `Content-Type`, so servers that persist it round-trip it on read. Stream bodies are buffered (no chunked upload). | | `download` | ✅ | Ranged reads issue a `Range` request; `as: "stream"` streams the response body without buffering. A server that ignores `Range` throws rather than silently returning the whole object. | | `delete` | ✅ | Idempotent - a missing file is not an error. | | `list` | ⚠️ | Walks the collection tree recursively (one `PROPFIND` per directory). `prefix`/`limit`/`cursor`/`delimiter` are applied client-side over the full walk. `size`/`lastModified`/`type` come from the `PROPFIND` props. | | `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). | | `head` | ⚠️ | `size` and `lastModified` come from `PROPFIND`; the content type is the server's `getcontenttype` prop, falling back to the key's extension. No etag surfaced. | | `exists` | ✅ | A collection (directory) reports `false` - `exists` answers for file keys. | | `copy` | ✅ | Native server-side `COPY` - no body round-trip through this process. | | `move` | ✅ | Native server-side `MOVE`. | | `url` | ❌ | Throws unless `publicBaseUrl` is set (an HTTP server fronting the same tree), in which case it returns `/`. A WebDAV `GET` needs authentication and the protocol has no signing primitive. `responseContentDisposition` always throws because the HTTP-front URL cannot bind the override. | | `signedUploadUrl` | ❌ | Throws - WebDAV has no presigned-upload concept. Use `upload()`. | --- # Yandex Object Storage Source: https://files-sdk.dev/docs/adapters/yandex ## Installation `@aws-sdk/client-s3`, `@aws-sdk/s3-presigned-post`, and `@aws-sdk/s3-request-presigner` are optional peer dependencies of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime. ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` ## Usage ```ts lineNumbers import { Files } from "files-sdk"; import { yandex } from "files-sdk/yandex"; const files = new Files({ adapter: yandex({ bucket: "uploads", // endpoint defaults to https://storage.yandexcloud.net // region defaults to "ru-central1" // accessKeyId / secretAccessKey auto-loaded from // YANDEX_ACCESS_KEY_ID / YANDEX_SECRET_ACCESS_KEY }), }); ``` Yandex Object Storage via its S3-compatible API. A thin wrapper around the S3 adapter - fixed global endpoint, region defaults to `"ru-central1"` for signing, virtual-hosted-style addressing, errors relabelled. Auto-loads from `YANDEX_ACCESS_KEY_ID` and `YANDEX_SECRET_ACCESS_KEY`. Generate static access keys in the Yandex Cloud console for a service account with the `storage.editor` role. ## Options ## Compatibility | Method | Status | Notes | | --- | :-: | --- | | `upload` | ✅ | | | `download` | ✅ | | | `delete` | ✅ | | | `list` | ⚠️ | The S3 list API returns no per-object `Content-Type`, so `type` is inferred from the key's extension (`application/octet-stream` when unknown). Use `head()` for the stored value. | | `search` | ✅ | | | `head` | ✅ | | | `exists` | ✅ | | | `copy` | ✅ | | | `url` | ✅ | | | `signedUploadUrl` | ✅ | | --- # Claude Agent SDK Source: https://files-sdk.dev/docs/ai/claude The `files-sdk/claude` subpath exposes a configured `Files` instance to the [Claude Agent SDK](https://docs.claude.com/en/api/agent-sdk/overview) (`@anthropic-ai/claude-agent-sdk`, the renamed Claude Code SDK). The Agent SDK consumes tools as an in-process MCP server plus an `allowedTools` allow-list and a `canUseTool` approval callback, so `createClaudeFileTools` returns a bundle of all three - pass them straight into `query()`. `@anthropic-ai/claude-agent-sdk` and `zod` are optional peer dependencies - only install them if you're consuming this subpath. ## Installation ```package-install @anthropic-ai/claude-agent-sdk zod ``` ## Quick start `createClaudeFileTools` returns `{ mcpServers, allowedTools, canUseTool, needsApproval, server, serverName }`. The first three slot directly into `query()`'s options; the rest are escape hatches for callers that want to compose with existing MCP servers or wire their own approval UX. ```tsx lineNumbers import { query } from "@anthropic-ai/claude-agent-sdk"; import { Files } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { createClaudeFileTools } from "files-sdk/claude"; const files = new Files({ adapter: s3({ bucket: "uploads" }) }); const tools = createClaudeFileTools({ files }); for await (const message of query({ prompt: "Find every CSV under reports/ and summarize the latest one.", options: { mcpServers: tools.mcpServers, allowedTools: tools.allowedTools, canUseTool: tools.canUseTool, }, })) { // handle messages } ``` ## Approval control The bundled `canUseTool` denies any tool whose `needsApproval` resolves to `true` with a `"requires approval"` message, and allows everything else. `requireApproval` accepts a boolean for the all-or-nothing case, or an object keyed by write tool name for fine-grained control. ```ts lineNumbers // All writes require approval (default) - denied by the bundled canUseTool. createClaudeFileTools({ files }); // Disable the approval gate entirely. createClaudeFileTools({ files, requireApproval: false }); // Granular: only destructive operations need approval. createClaudeFileTools({ files, requireApproval: { deleteFile: true, signUploadUrl: true, uploadFile: false, copyFile: false, }, }); ``` For real human-in-the-loop UX, compose your own `canUseTool` on top of `tools.needsApproval()`. The helper accepts both bare names (`"uploadFile"`) and the MCP-prefixed form (`"mcp__files__uploadFile"`) that the SDK passes in, so the callback is symmetric whichever shape you receive. ```ts lineNumbers import type { CanUseTool } from "@anthropic-ai/claude-agent-sdk"; const tools = createClaudeFileTools({ files }); // Compose your own canUseTool - needsApproval accepts both bare // names ("uploadFile") and the mcp-prefixed form passed in by the SDK. const canUseTool: CanUseTool = async (name, input) => { if (tools.needsApproval(name)) { const approved = await askUser(name, input); return approved ? { behavior: "allow", updatedInput: input } : { behavior: "deny", message: "User rejected the call." }; } return { behavior: "allow", updatedInput: input }; }; ``` ## Read-only mode Pass `readOnly: true` to drop every write tool from the bundled MCP server. The model cannot mutate the bucket regardless of how `requireApproval` is configured. ```ts lineNumbers // Strip every write tool. The model can browse but cannot mutate // the bucket regardless of approval configuration. createClaudeFileTools({ files, readOnly: true }); // allowedTools → ["mcp__files__downloadFile", "mcp__files__getFileMetadata", // "mcp__files__getFileUrl", "mcp__files__listFiles"] ``` ## Server name Claude addresses each MCP tool as `mcp____`. The default server name is `"files"`; override it via `serverName` when you need to namespace alongside another MCP server or just prefer a different label in transcripts. ```ts lineNumbers // Override the MCP server name - affects the mcp____ // prefix the model sees, and the mcpServers map key. const tools = createClaudeFileTools({ files, serverName: "storage" }); // tools.allowedTools → ["mcp__storage__copyFile", ...] // tools.mcpServers → { storage: } ``` ## Cherry-picking tools Each tool factory is exported individually as a `SdkMcpToolDefinition` - bundle them into your own `createSdkMcpServer` call when you want full control over the MCP server shape or want to mix files-sdk tools with your own. ```ts lineNumbers import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"; import { Files } from "files-sdk"; import { claudeDownloadFile, claudeListFiles, claudeUploadFile, } from "files-sdk/claude"; const files = new Files({ adapter }); // Compose your own MCP server with just the tools you want. const server = createSdkMcpServer({ name: "files", version: "1.0.0", tools: [ claudeListFiles(files), claudeDownloadFile(files), claudeUploadFile(files), ], }); ``` --- # OpenAI Source: https://files-sdk.dev/docs/ai/openai The `files-sdk/openai` subpath ships two factories targeting OpenAI directly - one for the native [Responses API](https://developers.openai.com/api/reference/responses/overview) and one for the [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/) (`@openai/agents`). Both wrap the same eight file operations as the Vercel subpath, with the same approval-gating defaults. `openai` and `@openai/agents` are optional peer dependencies - install only the one(s) you use. The subpath requires **Zod 4**: `@openai/agents` peer-requires it, and Zod 4's built-in `toJSONSchema` powers the Responses tool definitions. ## Responses API `createResponsesFileTools` returns `{ definitions, execute, needsApproval }`. Pass `definitions` straight to `openai.responses.create({ tools })`, then call `execute(call)` on each `function_call` item in the response output to get a `function_call_output` ready to push into the next turn's input. ```package-install openai zod ``` ```tsx lineNumbers import OpenAI from "openai"; import { Files } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { createResponsesFileTools } from "files-sdk/openai"; const client = new OpenAI(); const files = new Files({ adapter: s3({ bucket: "uploads" }) }); const ft = createResponsesFileTools({ files }); const input: any[] = [{ role: "user", content: "List my files." }]; while (true) { const res = await client.responses.create({ model: "gpt-4.1", input, tools: ft.definitions, }); const calls = res.output.filter((o) => o.type === "function_call"); if (calls.length === 0) { console.log(res.output_text); break; } for (const call of calls) { let approved = false; if (ft.needsApproval(call.name)) { // surface approval UX for this exact call approved = true; } input.push(call, await ft.execute(call, { approved })); } } ``` `execute` returns JSON parse failures, Zod validation errors, and unapproved write attempts _as the tool's output_, so the model can self-correct on the next turn. For write tools whose `needsApproval(name)` is `true`, call `execute(call, { approved: true })` only after your approval UX approves that exact function call. `FilesError` from the underlying SDK is rethrown - you decide how to surface it. ## Agents SDK `createAgentsFileTools` returns a record of `tool()` outputs keyed by tool name - spread `Object.values()` into `new Agent({ tools })`. Write tools default to `needsApproval: true`; the Agents SDK runner surfaces an `interruption` that your program resolves by approving or rejecting the call. ```package-install @openai/agents zod ``` ```tsx lineNumbers import { Agent, run } from "@openai/agents"; import { Files } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { createAgentsFileTools } from "files-sdk/openai"; const files = new Files({ adapter: s3({ bucket: "uploads" }) }); const tools = createAgentsFileTools({ files }); const agent = new Agent({ instructions: "Help the user manage their files.", name: "Files agent", tools: Object.values(tools), }); const result = await run(agent, "List my files."); ``` Errors thrown from `execute()` are wrapped by the Agents SDK's default `errorFunction` into a model-visible string - the model sees the message and can self-correct on the next turn. This is the standard Agents-SDK pattern, and differs from the Responses flow where `FilesError` rethrows. ## Approval, read-only, overrides Both factories accept the same options shape as the Vercel `createFileTools`: `requireApproval` (boolean or per-tool record), `readOnly` (strips writes entirely), and `overrides` (description, plus `strict` for Responses or `needsApproval` for Agents). ```ts lineNumbers // Same shape across both factories. createResponsesFileTools({ files }); // all writes gated (default) createResponsesFileTools({ files, requireApproval: false }); // disabled createResponsesFileTools({ files, requireApproval: { deleteFile: true, uploadFile: false }, }); createAgentsFileTools({ files, readOnly: true }); // → only listFiles, getFileMetadata, downloadFile, getFileUrl ``` --- # Vercel AI SDK Source: https://files-sdk.dev/docs/ai/vercel The `files-sdk/ai-sdk` subpath exposes a configured `Files` instance to the [Vercel AI SDK](https://ai-sdk.dev) as a set of ready-to-use tools - drop them into `generateText`, `streamText`, or any agent and the model can browse, read, and mutate your bucket through the same unified surface as your application code. Write tools (`uploadFile`, `deleteFile`, `copyFile`, `signUploadUrl`) require user approval by default - designed for human-in-the-loop agents. Read tools (`listFiles`, `getFileMetadata`, `downloadFile`, `getFileUrl`) never require approval. ## Installation `ai` and `zod` are optional peer dependencies - only install them if you're consuming the `files-sdk/ai-sdk` subpath. ```package-install ai zod ``` ## Quick start Construct a `Files` instance the same way you would anywhere else, then pass it to `createFileTools`. The returned object plugs straight into the AI SDK's `tools` field. ```tsx lineNumbers import { Files } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { createFileTools } from "files-sdk/ai-sdk"; import { generateText } from "ai"; const files = new Files({ adapter: s3({ bucket: "uploads", region: "us-east-1" }), }); const result = await generateText({ model: yourModel, tools: createFileTools({ files }), prompt: "Find every CSV under reports/ and summarize the latest one.", }); ``` ## Approval control `requireApproval` accepts a boolean for the all-or-nothing case, or an object keyed by write tool name for fine-grained control. Unspecified entries in the object form default to `true`, so it's safe to opt-in only the cases you trust. ```ts lineNumbers // All writes require approval (default). createFileTools({ files }); // Drop the approval gate entirely. createFileTools({ files, requireApproval: false }); // Granular: only the destructive operations need approval. createFileTools({ files, requireApproval: { deleteFile: true, signUploadUrl: true, uploadFile: false, copyFile: false, }, }); ``` ## Read-only mode Pass `readOnly: true` to drop every write tool. The model cannot mutate the bucket regardless of how `requireApproval` is configured - useful for retrieval-style agents that only need to browse, summarize, or hand the user a download URL. ```ts lineNumbers // Strip every write tool. The model can browse but cannot mutate // the bucket regardless of approval configuration. createFileTools({ files, readOnly: true }); // → { listFiles, getFileMetadata, downloadFile, getFileUrl } ``` ## Overrides Patch any safe `tool()` field on a per-tool basis without touching the underlying implementation. Useful for tightening descriptions to your domain, flipping an individual `needsApproval`, or adding provider-specific `providerOptions`. `execute`, `inputSchema`, and `outputSchema` are intentionally not overridable. ```ts lineNumbers createFileTools({ files, overrides: { listFiles: { description: "List files in the current tenant's bucket" }, deleteFile: { needsApproval: false, title: "Remove file" }, }, }); ``` ## Cherry-picking tools Each tool factory is also exported individually for fully custom setups - useful when you want to mix AI SDK tools across multiple domains and need full control over the returned object's shape. ```ts lineNumbers import { Files } from "files-sdk"; import { listFiles, downloadFile, uploadFile } from "files-sdk/ai-sdk"; const files = new Files({ adapter }); const tools = { listFiles: listFiles(files), downloadFile: downloadFile(files), uploadFile: uploadFile(files), }; ``` --- # API reference Source: https://files-sdk.dev/docs/api The surface is small: a `Files` instance with ten methods, plus `files.file(key)` for a key-bound handle. Each method takes one key (or an array, where bulk applies), accepts the shared `signal` / `timeout` / `retries` options, and throws a normalized `FilesError`. Each method has its own page; this page covers the pieces that cut across all of them. ## Functions - [`upload`](/docs/api/upload) - write a body to a key, one or many. - [`download`](/docs/api/download) - read an object as a [`StoredFile`](/docs/api/stored-file) or a stream. - [`head`](/docs/api/head) - fetch metadata without materializing the body. - [`exists`](/docs/api/exists) - check whether a key exists. - [`delete`](/docs/api/delete) - remove one object or many. - [`copy`](/docs/api/copy) - server-side copy with a read + write fallback. - [`move`](/docs/api/move) - rename a key, native where the provider supports it. - [`list`](/docs/api/list) - cursor-paginated listing, or `listAll` to walk every page. - [`url`](/docs/api/url) - a direct or signed URL to fetch a key. - [`signedUploadUrl`](/docs/api/signed-upload-url) - a presigned PUT/POST contract for direct browser uploads. - [`file`](/docs/api/file) - a `FileHandle` bound to one key. Two **global functions** stand apart from the instance methods — they take two `Files` instances and move objects between backends, built entirely on the methods above: - [`transfer`](/docs/api/transfer) - stream every object from one instance to another. - [`sync`](/docs/api/sync) - mirror one instance onto another (skip-unchanged, prune, dry-run). ## Scoping keys with a prefix Pass `prefix` to the constructor and every key is resolved relative to it — prepended on the way in, stripped on the way out — so application code works in its own namespace. See [Prefixes](/docs/prefixes) for the full behavior, including slash normalization and how `list()` scopes to a path boundary. ## Read-only instances Pass `readonly: true` to `new Files(...)`, or call `files.readonly()`, to create a view that still allows reads (`download`, `head`, `exists`, `list`, `listAll`, `url`) but rejects writes (`upload`, `delete`, `copy`, `move`, `signedUploadUrl`) with `FilesError { code: "ReadOnly" }`. See [Read-only](/docs/readonly) for the full behavior. ## Per-operation options Every method accepts `signal`, `timeout`, and `retries`. Set them once on the constructor as defaults, then override per call - a per-call value wins over the constructor default for that operation. ```ts lineNumbers const files = new Files({ adapter: s3({ bucket: "uploads" }), timeout: 10_000, // default per-attempt timeout for every call retries: { max: 3, backoff: ({ attempt }) => attempt * 500 }, }); // A per-call value wins over the constructor default. await files.head("avatars/abc.png", { timeout: 2_000 }); ``` See [Timeouts](/docs/timeouts), [Retries](/docs/retries), and [Cancellation](/docs/cancellations) for how each behaves, and [Hooks](/docs/usage#hooks) to observe operations as they run. ## The StoredFile type `download`, `head`, and `list` all return [`StoredFile`](/docs/api/stored-file) — a type that mirrors native `File`'s shape and adds the `key`, `etag`, and `metadata` that storage carries. --- # copy Source: https://files-sdk.dev/docs/api/copy `files.copy(from, to, options?)` Copies the object at `from` to `to` and resolves to `void`. Where the provider has a native copy primitive (S3 `CopyObject`, GCS, Supabase, Vercel Blob, Dropbox, Azure, and others) the copy happens entirely server-side — no bytes travel through your process. Adapters without one fall back to a read + write, and a few providers can't copy at all and throw. ```ts lineNumbers await files.copy("avatars/abc.png", "avatars/abc.bak.png"); ``` Both keys are validated, and on a client with a [prefix](/docs/prefixes) both `from` and `to` are resolved against it. The destination is overwritten if it already exists — there is no built-in guard against clobbering. The object's `contentType` and user `metadata` travel with the copy; `lastModified` is set to the time of the copy, since the destination is a fresh write rather than a clone of the source's timestamp. A missing `from` throws [`FilesError`](/docs/api/errors) with `code: "NotFound"`. ## The read + write fallback Adapters with no server-side copy primitive — UploadThing, Netlify Blobs, Cloudinary, and the R2 binding without HTTP credentials — emulate `copy` by downloading the source and re-uploading it to the destination. The source is streamed through rather than buffered, so even multi-GB copies stay within serverless memory limits, but two things differ from a native copy: - **It costs an egress download plus an ingest upload.** For large or frequent copies on these providers, consider doing the copy at the application layer with a storage strategy that copies server-side. - **It is not atomic.** Concurrent writes to `from` between the read and the write are not detected, so the destination may reflect an in-flight change to the source. Each adapter's Compatibility section marks whether it copies natively, falls back, or throws. ## Providers without copy Some backends have no way to copy to a caller-chosen key. Convex, for instance, assigns immutable storage ids, so `copy` throws `FilesError` with `code: "Provider"` rather than silently misbehaving. When you need the operation anyway, `download()` the source and `upload()` it back under a new key, or reach for the [escape hatch](/docs/escape-hatch). ## Options `copy` accepts the shared `OperationOptions` — `signal`, `timeout`, and `retries`. As elsewhere, only `Provider` failures are [retried](/docs/retries); deterministic errors like `NotFound` are returned immediately. ```ts lineNumbers await files.copy("avatars/abc.png", "avatars/abc.bak.png", { signal: controller.signal, retries: 3, }); ``` ## On a `FileHandle` [`files.file(key)`](/docs/api/file) exposes the same operation bound to one key, as `copyTo` (the handle is the source) and `copyFrom` (the handle is the destination): ```ts lineNumbers const avatar = files.file("avatars/abc.png"); await avatar.copyTo("avatars/abc.bak.png"); // copy(key, "avatars/abc.bak.png") await avatar.copyFrom("uploads/new.png"); // copy("uploads/new.png", key) ``` --- # delete Source: https://files-sdk.dev/docs/api/delete `files.delete(key)` · `files.delete(keys)` Pass a single key to remove one object, or an array to remove many in one call. The two forms differ in how failures surface. **One key** removes a single object and resolves to `void`. No-op friendly: a missing key resolves successfully on providers that treat delete as idempotent, and throws [`FilesError`](/docs/api/errors) with `code: "NotFound"` on ones that don't. ```ts lineNumbers await files.delete("avatars/abc.png"); ``` ## Many keys Returns a structured result instead of throwing on partial failure. Adapters with a native bulk primitive (S3 `DeleteObjects`, Azure's Blob Batch API, Supabase, UploadThing) delete in a single request — the S3 path chunks into batches of 1000 and Azure into batches of 256, the respective provider limits — while the rest fan out to single deletes with bounded concurrency (FTP and SFTP reuse one connection for the sequence). Like the single form, it honors the client's `prefix` and is no-op friendly on providers that treat a missing key as success. ```ts lineNumbers const result = await files.delete( ["avatars/a.png", "avatars/b.png", "avatars/c.png"], { concurrency: 8, stopOnError: false } ); result.deleted; // string[] — keys removed, in the order supplied result.errors; // undefined when every key succeeded ``` The array form always resolves to an object of this shape: ```ts type DeleteManyResult = { deleted: string[]; errors?: Array<{ key: string; error: FilesError }>; }; ``` With the default `stopOnError: false`, every key is attempted and per-key failures are collected in `errors`. With `stopOnError: true`, the call stops at the first failure and returns the keys deleted so far plus that error. Invalid keys (empty, or containing null bytes) are reported in `errors` rather than thrown. When a native bulk provider only reports that the whole request failed, the provider error is mapped onto each affected key. ### Options (array form) --- # download Source: https://files-sdk.dev/docs/api/download `files.download(key, options?)` · `files.download(keys)` Reads an object. Returns a [`StoredFile`](/docs/api/stored-file) by default (Blob-backed). Pass `{ as: "stream" }` to opt into a `ReadableStream` for large objects. ```ts lineNumbers const file = await files.download("avatars/abc.png"); // → StoredFile (Blob-backed) const stream = await files.download("avatars/abc.png", { as: "stream" }); // → ReadableStream ``` ## Byte ranges Pass `range` to download only a contiguous slice of the object — the primitive behind video seeking and resumable downloads. Both bounds are **0-based**, and `end` is **inclusive**, matching the HTTP `Range` header (`bytes=start-end`) this maps to. The returned `StoredFile` carries just the requested bytes, and its `size` is the range length, not the full object. ```ts lineNumbers // Bytes 0–1023 (the first 1 KiB). end is inclusive, so this is 1024 bytes. const head = await files.download("video.mp4", { range: { start: 0, end: 1023 }, }); head.size; // 1024 // Omit end to read from an offset to the end — e.g. resume a download. const rest = await files.download("video.mp4", { range: { start: 1024 } }); // Streaming works too, so you never buffer the whole slice. const chunk = await files.download("video.mp4", { as: "stream", range: { start: 0, end: 65_535 }, }); ``` `range` combines with `as: "stream"`, `signal`, `timeout`, and `retries` like any other download option. **Supported** almost everywhere — the SDK threads the range through whatever primitive each provider exposes: - **Native byte ranges** — AWS S3 and every S3-compatible adapter (Cloudflare R2 over HTTP, MinIO, DigitalOcean Spaces, Wasabi, Tigris, Backblaze B2, Storj, Hetzner, Akamai, Scaleway, OVHcloud, iDrive e2, Vultr, Filebase, Exoscale, Oracle Cloud, IBM COS, Tencent COS, Alibaba OSS, Yandex), Bun S3, Google Cloud Storage, Firebase Storage, Azure Blob, the **R2 Workers binding** (native `range` option), the local `fs` adapter, and the in-memory adapter. - **HTTP `Range` header** — UploadThing, Box, Vercel Blob (public), Cloudinary, PocketBase, Dropbox (via its temporary link), OneDrive, SharePoint, and Google Drive issue a `Range` request against the underlying URL/content endpoint. - **Native read offsets** — SFTP threads the range through ssh2 read-stream `start`/`end` byte offsets. FTP begins the transfer at the `REST` start offset and trims a bounded `end` client-side, so an open-ended `{ start }` range transfers only the bytes from the offset on (a bounded `end` still reads to EOF before trimming the tail). For the HTTP-header adapters the SDK verifies the response came back `206 Partial Content` and **throws if the host answered `200`** (i.e. ignored the range and sent the whole object), so a ranged read never silently transfers — and bills you for — the full file. **Throws** a [`FilesError`](/docs/api/errors) on adapters whose provider has no range primitive at all — Supabase, Appwrite, Netlify Blobs, Bunny Storage, Convex, and Vercel Blob **private** blobs (read through the SDK, which can't range). The SDK fails loudly rather than downloading the whole object and slicing it client-side, so the bandwidth saving is never quietly lost. Branch on the adapter's `supportsRange` flag to handle both at runtime: ```ts lineNumbers const opts = files.adapter.supportsRange ? { range: { start, end } } : {}; const file = await files.download(key, opts); ``` An out-of-shape range (`start` negative or non-integer, or `end` below `start`) throws before any provider call. ## Many keys Pass an array to download many in one call. Returns `{ downloaded, errors? }` instead of throwing on partial failure; a missing key lands in `errors`. `as` applies to every download, and the call fans out with bounded `concurrency` (default 8) / `stopOnError`. ```ts lineNumbers const result = await files.download(["avatars/a.png", "avatars/b.png"]); result.downloaded; // StoredFile[] — successes, in the order supplied result.errors; // undefined when every key succeeded ``` --- # Errors Source: https://files-sdk.dev/docs/api/errors Every single-key method throws a `FilesError` on failure. It collapses the dozens of provider-specific error shapes into one type with a small `code` enum, while keeping the original error on `cause` so nothing is lost. ```ts lineNumbers import { FilesError } from "files-sdk"; try { await files.download("missing.png"); } catch (err) { if (err instanceof FilesError && err.code === "NotFound") { // handle gracefully return null; } throw err; } ``` ## Properties - **`code`** — one of the normalized codes below. Match on this for control flow. - **`message`** — a human-readable summary, taken from the provider error where one is available. - **`cause`** — the original provider error, untouched. Reach into it for provider-specific detail: `@aws-sdk` errors, for instance, carry a request ID and HTTP status on `$metadata` and a typed `name` like `NoSuchKey`. - **`aborted`** — `true` when the failure came from a [cancellation](/docs/cancellations) or a [timeout](/docs/timeouts) rather than the provider. Both surface as `code: "Provider"`, so this flag is how you tell an abort apart from a genuine provider failure. ## Codes - `"NotFound"` — the key (or bucket / container) does not exist. - `"Unauthorized"` — credentials missing, expired, or insufficient for the operation. - `"Conflict"` — a precondition failed, e.g. a conditional write that lost a race. - `"ReadOnly"` — the operation tried to mutate a read-only `Files` instance. - `"Provider"` — anything else, including network failures, throttling, timeouts, and aborts. Inspect `cause` for the underlying error. Codes are derived from the provider's own error code and HTTP status (`404` → `NotFound`, `401` / `403` → `Unauthorized`, `409` / `412` → `Conflict`) plus one SDK-native code: `ReadOnly` for write attempts against `new Files({ readonly: true })` or `files.readonly()`. Everything else maps to `Provider`. Only `Provider` failures are [retried](/docs/retries); the rest are deterministic and returned to you immediately. ## Errors in bulk operations The array forms — `upload([…])`, `download([…])`, `delete([…])`, `head([…])`, `exists([…])` — don't throw on partial failure. Each resolves to a structured result that collects per-key failures in an `errors[]` array (each entry a `{ key, error: FilesError }`) alongside the successes, both in the order you supplied. One bad key never sinks the whole batch. See each method's page for the exact result shape. > **Logging note:** `cause` can carry request IDs, response headers, and partial request metadata from `@aws-sdk` and friends. If you forward a `FilesError` to logs that cross a trust boundary, strip or whitelist `cause` rather than `JSON.stringify`-ing the whole thing. For provider-by-provider error gotchas and debugging tips, see [Troubleshooting](/docs/troubleshooting). --- # exists Source: https://files-sdk.dev/docs/api/exists `files.exists(key)` · `files.exists(keys)` Checks whether an object exists without fetching its body. Returns `true` when the key exists and `false` when the provider reports `NotFound`. Permission, auth, and transport failures still throw so callers do not accidentally treat them as a missing file. ```ts lineNumbers const present = await files.exists("avatars/abc.png"); const missing = await files.exists("avatars/missing.png"); // → true / false ``` ## Many keys Pass an array to check many in one call. Returns `{ existing, missing, errors? }`: keys split into `existing` / `missing` (both in input order), with hard errors (auth, transport) collected in `errors` rather than thrown. Fans out with bounded `concurrency` (default 8) / `stopOnError`. ```ts lineNumbers const result = await files.exists(["avatars/a.png", "avatars/b.png"]); result.existing; // string[] — keys that exist result.missing; // string[] — keys the provider reports as absent result.errors; // undefined unless a key hard-errored ``` --- # file Source: https://files-sdk.dev/docs/api/file `files.file(key)` Returns a `FileHandle` bound to `key`: a thin wrapper that exposes `upload`, `download`, `head`, `exists`, `delete`, `url`, `signedUploadUrl`, `copyTo`, and `copyFrom` without re-passing the key each time. Useful when application code works with the same object repeatedly. The key is validated at construction; every method routes through the same `Files` entry points, so adapters do not implement anything extra. On a read-only client (`new Files({ readonly: true, ... })` or `files.readonly()`), the handle still supports reads, but its write helpers (`upload`, `delete`, `copyTo`, `copyFrom`, `moveTo`, `moveFrom`, `signedUploadUrl`) throw `FilesError` with `code: "ReadOnly"`. ```ts lineNumbers const avatar = files.file("avatars/abc.png"); await avatar.upload(file, { contentType: "image/png" }); if (await avatar.exists()) { const meta = await avatar.head(); const url = await avatar.url({ expiresIn: 300 }); } await avatar.copyTo("avatars/abc.bak.png"); await avatar.delete(); ``` --- # head Source: https://files-sdk.dev/docs/api/head `files.head(key)` · `files.head(keys)` Returns the same [`StoredFile`](/docs/api/stored-file) shape as [`download`](/docs/api/download), without materializing the body. Calling a body accessor on the result lazy-fetches. ```ts lineNumbers const info = await files.head("avatars/abc.png"); // → StoredFile with no body materialized ``` ## Many keys Pass an array to fetch metadata for many in one call. Returns `{ files, errors? }` instead of throwing on partial failure (a missing key lands in `errors`), honoring `concurrency` / `stopOnError`. ```ts lineNumbers const result = await files.head(["avatars/a.png", "avatars/b.png"]); result.files; // StoredFile[] — successes, in the order supplied result.errors; // undefined when every key succeeded ``` --- # list Source: https://files-sdk.dev/docs/api/list `files.list(options?)` Cursor-paginated listing with prefix filter. Each item is a [`StoredFile`](/docs/api/stored-file) with a lazy body accessor. ```ts lineNumbers const { items, cursor } = await files.list({ prefix: "avatars/", limit: 100, }); if (cursor) { const next = await files.list({ prefix: "avatars/", cursor }); } ``` ## Walking every page with `listAll` `list` returns one page plus a `cursor`; most callers actually want "walk everything under this prefix", which is a manual cursor loop. `files.listAll(options?)` is that loop as an async iterable: ```ts lineNumbers for await (const file of files.listAll({ prefix: "avatars/" })) { console.log(file.key, file.size); } ``` Each item is the same [`StoredFile`](/docs/api/stored-file) `list` yields. `prefix` scopes the walk; `limit` sets the **page size** each underlying `list` fetches (not a total cap), and a `cursor` resumes from a prior position. Every page is a real `list` call, so it honors the client `prefix`, [retries and timeouts](/docs/retries), and fires one [`onAction`](/docs/api/onaction) `list` event per page. `break` out of the loop to stop early — no further pages are fetched. `listAll` runs on every adapter, since it's built on `list`. Two `list` caveats carry over to the walk: - **[Netlify Blobs](/docs/adapters/netlify-blobs) exposes no pagination cursor.** Call `listAll()` with **no `limit`** and it returns everything in a single page (correct); pass a `limit` and there is no next page to follow, so the walk stops at that many. On Netlify, omit `limit` when you mean "walk everything." - **Non-recursive adapters** ([Box](/docs/adapters/box), [OneDrive](/docs/adapters/onedrive), [SharePoint](/docs/adapters/sharepoint)) list only the immediate children of the root folder. `listAll` walks every page, but it can't descend into subfolders the underlying `list` never returns. ## Listing folders with `delimiter` Pass a `delimiter` to collapse keys at that boundary into **common prefixes** ("folders") — the building block for a file-browser UI. With `delimiter: "/"`, a page returns only the files directly under `prefix` in `items`, and the subfolders in `prefixes` (full keys including the trailing delimiter); keys nested deeper are folded into those prefixes rather than listed. ```ts lineNumbers const { items, prefixes } = await files.list({ prefix: "photos/", delimiter: "/", }); // Render one level of a browser: for (const folder of prefixes ?? []) { console.log("📁", folder); // "photos/2023/", "photos/2024/" } for (const file of items) { console.log("📄", file.key); // "photos/cover.jpg" } ``` `prefixes` is omitted when no `delimiter` is set or none are found. The page `cursor` walks `items` and `prefixes` together (a folder counts as one entry against `limit`); hold `prefix` **and** `delimiter` constant across a paginated sequence, just like `prefix` alone. [`listAll`](#walking-every-page-with-listall) ignores `delimiter` — it walks the whole tree, so use `list` directly for the folder view. ### Provider support - **Any delimiter string** — the object stores with native common-prefix listing: [S3](/docs/adapters/s3) and the whole `s3()` family, [R2](/docs/adapters/r2), [Google Cloud Storage](/docs/adapters/gcs), [Firebase Storage](/docs/adapters/firebase-storage), [Azure Blob](/docs/adapters/azure), plus the local [`fs`](/docs/adapters/fs), in-memory, [FTP](/docs/adapters/ftp), [SFTP](/docs/adapters/sftp), [Google Drive](/docs/adapters/google-drive), and [Cloudinary](/docs/adapters/cloudinary) adapters (which synthesize prefixes from the key list). - **`"/"` only** — the folder-based providers: [Vercel Blob](/docs/adapters/vercel-blob), [Netlify Blobs](/docs/adapters/netlify-blobs), [Supabase](/docs/adapters/supabase), [Dropbox](/docs/adapters/dropbox), [Box](/docs/adapters/box), [OneDrive](/docs/adapters/onedrive), [SharePoint](/docs/adapters/sharepoint). Any other delimiter throws. - **Unsupported** — providers with no folder/prefix concept: [UploadThing](/docs/adapters/uploadthing), [Appwrite](/docs/adapters/appwrite), [PocketBase](/docs/adapters/pocketbase), [Convex](/docs/adapters/convex), and [Bun's S3](/docs/adapters/bun-s3) (whose list response carries no common prefixes). Passing a `delimiter` throws a [`FilesError`](/docs/api/errors) before any provider call rather than silently returning a flat list — branch on `adapter.supportsDelimiter` to check at runtime. ## Options Both `list` and `listAll` take the same options: ## Result --- # move Source: https://files-sdk.dev/docs/api/move `files.move(from, to, options?)` Moves (renames) the object at `from` to `to` and resolves to `void`. ```ts lineNumbers await files.move("uploads/tmp-abc.png", "avatars/user-123.png"); ``` Both keys are validated, and on a client with a [prefix](/docs/prefixes) both `from` and `to` are resolved against it. The destination is overwritten if it already exists. A missing `from` throws [`FilesError`](/docs/api/errors) with `code: "NotFound"`. ## How it moves Where the provider exposes a native rename that's atomic or avoids re-transferring the body, `move` uses it directly: the local filesystem ([`fs`](/docs/adapters/fs)) renames in place, [FTP](/docs/adapters/ftp) and [SFTP](/docs/adapters/sftp) issue a native rename (`RNFR`/`RNTO` and the SFTP `RENAME` op — no body round-trip), and Cloudinary uses its server-side `rename` (same `asset_id`, no re-upload). Everywhere else it falls back to [`copy`](/docs/api/copy) then [`delete`](/docs/api/delete): the source is copied to the destination and the original is removed. Object stores (S3, R2, GCS, Azure, …) have no atomic move primitive, so they always take the copy + delete path. Two consequences follow from the fallback: - **It is not atomic.** A crash or failure between the copy and the delete can leave the object at _both_ keys. Re-running `move` recovers — the copy overwrites and the delete is idempotent — but there is no transactional guarantee in between. - **It inherits `copy`'s costs.** On providers whose `copy` is a read + write rather than server-side (UploadThing, Netlify Blobs, …) the bytes travel through your process once before the source is deleted. See [`copy`](/docs/api/copy#the-read--write-fallback) for the details. (FTP and SFTP avoid this for `move` — they rename natively — but their standalone `copy` is still a read + write.) ## Moving onto the same key Moving a key onto itself (`from === to`, after the client `prefix` is applied) is a **no-op** — nothing is copied or deleted. This guard matters: without it, the copy + delete fallback would copy the object onto itself and then delete it, destroying the file. ## Providers without move `move` works on every adapter except where its building blocks don't. Since the fallback is `copy` + `delete`, `move` throws wherever `copy` throws — notably [Convex](/docs/adapters/convex), which assigns immutable storage ids and has no rename. There, `download()` the source and `upload()` it under a new key, then track the new id. Each adapter's Compatibility section marks `copy` (and therefore `move`). ## Options `move` accepts the shared `OperationOptions` — `signal`, `timeout`, and `retries`. As elsewhere, only `Provider` failures are [retried](/docs/retries); deterministic errors like `NotFound` are returned immediately. ```ts lineNumbers await files.move("uploads/tmp-abc.png", "avatars/user-123.png", { signal: controller.signal, retries: 3, }); ``` ## On a `FileHandle` [`files.file(key)`](/docs/api/file) exposes the same operation bound to one key, as `moveTo` (the handle is the source) and `moveFrom` (the handle is the destination): ```ts lineNumbers const tmp = files.file("uploads/tmp-abc.png"); await tmp.moveTo("avatars/user-123.png"); // move(key, "avatars/user-123.png") const avatar = files.file("avatars/user-123.png"); await avatar.moveFrom("uploads/tmp-abc.png"); // move("uploads/tmp-abc.png", key) ``` --- # onAction Source: https://files-sdk.dev/docs/api/onaction A constructor [`hook`](/docs/usage#hooks) that runs once when a public call settles, on success and on failure - `status` says which. Single-key operations report `key`; the array form reports the caller's `keys` and emits one event for the whole call, carrying the aggregated `result` (any per-item failures live in that result's `errors`). `copy` and `move` report `from` / `to`. Reach for it for audit logs, activity feeds, and per-action metrics. ```ts lineNumbers const files = new Files({ adapter: s3({ bucket: "uploads" }), hooks: { onAction(event) { logger.info("files", { action: event.type, status: event.status, target: event.keys ?? event.from ?? event.key, ms: event.durationMs, }); }, }, }); ``` ## Identifying the call `type` is the method name - `"upload"`, `"download"`, `"copy"`, and so on. Which key field is set depends on the shape of that call: - **Single-key calls** (`upload(key, …)`, `download(key)`, `delete(key)`, …) set `key`. - **Array calls** (`upload([…])`, `delete([…])`, …) set `keys` and fire one event for the whole batch - the per-item failures live in `result.errors`, not here. - **`copy`** and **`move`** set `from` / `to` instead of `key`. On success, `result` is the call's resolved value (an `UploadResult`, a `StoredFile`, a `ListResult`, …); on failure, `error` is the same [`FilesError`](/docs/api/errors) delivered to [`onError`](/docs/api/onerror) and then thrown. Keys are always the ones you passed - the client [`prefix`](/docs/prefixes) is never leaked. ```ts lineNumbers import type { UploadResult } from "files-sdk"; hooks: { onAction(event) { if (event.type === "upload" && event.status === "success" && event.key) { const { size } = event.result as UploadResult; activity.push({ kind: "upload", key: event.key, size }); } }, }, ``` ## Per-action metrics Because it fires for every method on success and failure with a wall-clock `durationMs`, one `onAction` covers latency and throughput across the whole surface - no per-call wrapping. It fires once per call, so a bulk `upload([…])` of fifty objects emits a single timing for the batch, not fifty. ```ts lineNumbers hooks: { onAction({ type, status, durationMs }) { metrics.timing(`files.${type}.duration`, durationMs, { status }); metrics.increment(`files.${type}.${status}`); }, }, ``` ## Provenance receipts With the [`receipts`](/docs/receipts) option on, a successful mutating call (`upload`, `delete`, `copy`, `move`) also carries a `receipt` - a provenance record with the op, provider, key, byte size, etag, timing, and (when asked for) a SHA-256 of the upload body. It's an additive field: absent on reads, on failures, and whenever receipts are off. ```ts lineNumbers hooks: { onAction(event) { if (event.receipt) { provenance.record(event.receipt); } }, }, ``` Like every hook, `onAction` is **fire-and-forget**: the SDK calls it but never awaits it, and a hook that throws can't fail the operation it observes. --- # onError Source: https://files-sdk.dev/docs/api/onerror A constructor [`hook`](/docs/usage#hooks) that runs only when a public call **rejects** - validation failures, adapter failures, timeouts, and aborts - just before the matching [`onAction({ status: "error" })`](/docs/api/onaction). Partial failures collected inside a bulk result's `errors[]` are not rejections, so they don't fire it; this is the hook to wire to Sentry or Datadog when you want only true call failures. ```ts lineNumbers const files = new Files({ adapter: s3({ bucket: "uploads" }), hooks: { onError(event) { Sentry.captureException(event.error, { tags: { action: event.type, code: event.error.code }, extra: { key: event.key, durationMs: event.durationMs }, }); }, }, }); ``` The payload mirrors [`onAction`](/docs/api/onaction) - `type`, the caller-facing `key` / `keys` (or `from` / `to` for `copy` and `move`), and `durationMs` - but `error` is always present and typed as a [`FilesError`](/docs/api/errors), not optional. ## Filtering out aborts A [cancellation](/docs/cancellations) and a [timeout](/docs/timeouts) both reject, so both reach `onError` - usually as noise you don't want paging anyone. They carry `aborted: true`; branch on it before reporting. ```ts lineNumbers hooks: { onError(event) { if (event.error.aborted) { return; // caller cancelled, or a timeout fired - expected } reportError(event.error, { action: event.type, key: event.key }); }, }, ``` The `code` is one of the normalized [error codes](/docs/api/errors#codes), so you can route by class - swallow `NotFound`, handle `ReadOnly`, alert on `Provider`, and so on - rather than parsing messages. ## Not fired for bulk partial failures The array forms don't reject when only some keys fail - they resolve with the bad keys in `result.errors`. So `onError` stays quiet for a partial batch failure; the whole batch still settles through [`onAction`](/docs/api/onaction). Inspect the returned `errors[]` (or `onAction`'s `result`) to catch per-key failures. See [Bulk actions](/docs/bulk#retries-and-hooks). `onError` is **fire-and-forget**, like the other hooks: it runs, then the same `FilesError` is thrown to your `await`. The hook can't suppress, replace, or delay that rejection. --- # onProgress Source: https://files-sdk.dev/docs/api/onprogress Unlike the constructor [hooks](/docs/usage#hooks) ([`onAction`](/docs/api/onaction), [`onError`](/docs/api/onerror), [`onRetry`](/docs/api/onretry)), `onProgress` isn't set in the constructor `hooks` - it's a per-call option on [`upload`](/docs/api/upload) (both the single and [array forms](/docs/bulk)), since progress only makes sense for uploads. It's the original fire-and-forget callback the hooks are modeled on: called as bytes go out so you can drive a progress bar, never awaited, and safe to throw from. Granularity depends on the body and the adapter. A buffered body (`File`, `Blob`, `ArrayBuffer`, `Uint8Array`, `string`) reports `{ loaded: 0, total }` then `{ loaded: total, total }`; a `ReadableStream` is reported byte-by-byte, with `total` omitted when the length isn't known. S3 and the S3-compatible adapters report true byte-level progress for every body type (multipart included) through `@aws-sdk/lib-storage`, an optional peer dependency. It fires only while the upload is in flight and on success - a failed upload emits no final event, and progress restarts on retry. The [array form](/docs/bulk) adds the item's `key` to each report so you can attribute it when several files upload at once. ```ts lineNumbers await files.upload("report.pdf", body, { onProgress({ loaded, total }) { bar.update(total ? loaded / total : loaded); }, }); ``` ## When `total` is unknown `total` is present for buffered bodies and omitted for a `ReadableStream` of unknown length - there's no content length to divide by, so you only get `loaded`. Branch on it: show a percentage when you can, fall back to bytes-so-far when you can't. ```ts lineNumbers await files.upload("export.csv", stream, { onProgress({ loaded, total }) { setLabel( total ? `${Math.round((loaded / total) * 100)}%` : `${(loaded / 1_000_000).toFixed(1)} MB` ); }, }); ``` ## Many files at once The [array form of `upload`](/docs/bulk) takes the same callback, with the item's `key` added to every report - so you can attribute progress when several files upload concurrently and update the right row. ```ts lineNumbers await files.upload(items, { onProgress({ key, loaded, total }) { rows.get(key)?.update(total ? loaded / total : loaded); }, }); ``` --- # onRetry Source: https://files-sdk.dev/docs/api/onretry A constructor [`hook`](/docs/usage#hooks) that runs each time the SDK schedules a retry for a single-operation call, with the upcoming `attempt`, the `delayMs` before it, and the `error` that triggered it. It never fires on the first attempt, for non-retryable errors, or for stream uploads (which aren't retried). The array forms never fire it either — they settle as a single aggregated [`onAction`](/docs/api/onaction) event for the whole call. See [Retries](/docs/retries) for what counts as retryable. ```ts lineNumbers const files = new Files({ adapter: s3({ bucket: "uploads" }), retries: 3, hooks: { onRetry(event) { logger.warn("files retry", { action: event.type, key: event.key, attempt: `${event.attempt}/${event.maxRetries}`, delayMs: event.delayMs, code: event.error.code, }); }, }, }); ``` It fires _before_ the wait, so `delayMs` is the [backoff](/docs/retries#backoff) the SDK is about to sleep, not one already spent. `error` is the [`FilesError`](/docs/api/errors) from the attempt that just failed - always a retryable `Provider` failure, since the deterministic codes are never retried. ## Watching for exhaustion `attempt` counts up from `1`, and `maxRetries` is the ceiling for this call - so `attempt === maxRetries` is the last retry the SDK will schedule. If that one also fails, no further `onRetry` fires and the call settles through [`onError`](/docs/api/onerror) and [`onAction`](/docs/api/onaction). Use the comparison to count calls that burn through their whole retry budget. ```ts lineNumbers hooks: { onRetry(event) { metrics.increment("files.retry", { action: event.type }); if (event.attempt === event.maxRetries) { metrics.increment("files.retry.exhausted", { action: event.type }); } }, }, ``` A steady stream of retries on one `type` is usually a sign of a throttling or availability problem with that provider - surfacing `event.error.code` alongside the count makes the cause visible. Like the other hooks, `onRetry` is **fire-and-forget**: it can't change the backoff, cancel the retry, or fail the call - it only observes. --- # search Source: https://files-sdk.dev/docs/api/search `files.search(pattern, options?)` Find objects whose **key** matches `pattern`, walking every page like [`listAll`](/docs/api/list#walking-every-page-with-listall). It's a streaming async iterable of [`StoredFile`](/docs/api/stored-file), so it stays memory-bounded on large buckets and you can `break` to stop early. ```ts lineNumbers // Glob is the default: `*` stays within a path segment, `?` matches one char. for await (const file of files.search("avatars/*.png")) { console.log(file.key, file.size); } // Collect into an array when you want them all at once: const pdfs = await Array.fromAsync(files.search("invoices/2024/*.pdf")); ``` Matching is against the caller-facing key, so a [client `prefix`](/docs/prefixes) on the instance is already stripped before the pattern is tested. ## Glob syntax `"glob"` mode uses standard glob semantics, powered by [picomatch](https://github.com/micromatch/picomatch): - `*` — any run of characters **within** a path segment (does not cross `/`). - `**` — a globstar segment that **spans** path segments. Write it as its own segment: `photos/**/*.jpg` matches at any depth, including zero subfolders. - `?` — a single non-`/` character. - `[a-z]`, `{a,b}` — character classes and brace alternation. - `!pattern` — negation (matches everything except). The pattern is anchored to the **whole key**, and dotfiles are matched (object keys are opaque, not hidden files). A glob with no wildcards is an **exact** match, not a substring — `files.search("report.pdf")` matches the key `report.pdf` and nothing else. Use `match: "substring"` for "contains". ```ts lineNumbers // Every JPEG at any depth under photos/: for await (const file of files.search("photos/**/*.jpg")) { // photos/cover.jpg, photos/2024/spain/beach.jpg, ... } ``` ## Match modes Pass a `match` mode to change how a string `pattern` is interpreted, or pass a `RegExp` directly (which always matches by regex and ignores `match`): ```ts lineNumbers // Regular expression (string form): files.search("\\.(png|jpe?g)$", { match: "regex" }); // ...or a RegExp instance: files.search(/\.(png|jpe?g)$/); // Substring — key contains the text anywhere: files.search("report", { match: "substring" }); // Exact — key equals the text: files.search("invoices/2024/q1.pdf", { match: "exact" }); // Case-insensitive (any mode): files.search("*.PNG", { caseInsensitive: true }); ``` An invalid `regex` pattern throws a [`FilesError`](/docs/api/errors) before the walk begins. ## Scoping the walk with `prefix` `search` reads every page under a prefix, following the cursor. For a **glob**, the literal head of the pattern is pushed down automatically as that prefix, so `files.search("uploads/2024/*.pdf")` scopes the walk to the `uploads/2024` prefix rather than the whole bucket. Other modes carry no inferable prefix, so for a `regex`, `substring`, or `caseInsensitive` search over a large bucket, pass `prefix` yourself to bound the walk: ```ts lineNumbers // Only walk logs/ — then regex-match within it: files.search("error|panic", { match: "regex", prefix: "logs/" }); ``` A glob's auto push-down is disabled when `caseInsensitive` is set (a provider's prefix filter is case-sensitive), so combine `caseInsensitive` with an explicit `prefix` to scope it. ## Stopping early `maxResults` caps the number of matches yielded; because the walk is lazy, it also stops paging once the cap is hit. Equivalently, `break` out of the loop. ```ts lineNumbers // First 10 matches, then stop fetching pages: const recent = await Array.fromAsync( files.search("**/*.log", { maxResults: 10 }) ); ``` ## Provider support `search` runs on **every adapter**, since it's built on [`listAll`](/docs/api/list#walking-every-page-with-listall) — there's no per-provider search capability and nothing to gate. The two `listAll` caveats carry over: on [Netlify Blobs](/docs/adapters/netlify-blobs) omit `limit` to walk everything, and the non-recursive [Box](/docs/adapters/box) / [OneDrive](/docs/adapters/onedrive) / [SharePoint](/docs/adapters/sharepoint) adapters only see the immediate children of the root folder. An unbounded search with no `prefix` walks the whole bucket by design. ## Options The `match` mode is one of: --- # signedUploadUrl Source: https://files-sdk.dev/docs/api/signed-upload-url `files.signedUploadUrl(key, options)` Returns a discriminated PUT-or-POST contract so a client (typically a browser) can upload directly to the bucket without proxying bytes through your server. The flow is: your server calls `signedUploadUrl()`, returns the result to the browser, the browser uploads straight to the provider directly. Bandwidth and CPU stay off your server. Without `maxSize`, the adapter returns a presigned PUT URL - simpler, but with no server-side size cap. With `maxSize`, providers that support upload policies switch to a presigned POST form whose policy enforces the size at the bucket via `content-length-range`. In practice you should pass `maxSize` when the adapter supports it - without it, anyone with the URL can DoS your storage costs until `expiresIn` elapses. Vercel Blob, Bunny Storage, Appwrite, PocketBase, fs, and Convex throw here - Vercel's upload model goes through `handleUpload()` from `@vercel/blob/client` instead of presigned URLs, Bunny Storage writes require the Storage API `AccessKey` header, Appwrite/PocketBase have no presigned upload primitive at all, fs has no signer/verifier-backed upload server, and Convex upload URLs cannot bind the caller's SDK key or constraints. The R2 Workers binding throws unless you've configured hybrid mode (binding + HTTP credentials). Azure, Supabase, R2, Google Drive, OneDrive, SharePoint, Cloudinary, and UploadThing have no `content-length-range` equivalent and **throw if you pass unsupported size limits**; omit those options for a presigned PUT/session URL and enforce upload caps at your application gateway instead. Azure also throws if you pass `contentType`, because SAS does not bind Content-Type into the signature. ```ts lineNumbers // On your server: hand back an upload contract that lets the browser // PUT/POST the file directly to the bucket. Bytes never touch your server. const upload = await files.signedUploadUrl("avatars/abc.png", { expiresIn: 60, contentType: "image/png", maxSize: 5_000_000, }); // → { method: "PUT", url, headers? } // | { method: "POST", url, fields } // In the browser: PUT path (no maxSize) is a plain fetch. await fetch(upload.url, { method: "PUT", body: file, headers: upload.headers, }); // POST path (with maxSize) is multipart with the signed policy fields. const form = new FormData(); for (const [k, v] of Object.entries(upload.fields)) form.append(k, v); form.append("file", file); await fetch(upload.url, { method: "POST", body: form }); ``` ## Options --- # StoredFile Source: https://files-sdk.dev/docs/api/stored-file Native `File` covers `name`, `size`, `type`, and `lastModified`, but storage adds three things it doesn't carry: a full `key`, an `etag` for cache validation, and user-defined `metadata`. `StoredFile` mirrors `File`'s shape and adds those. ```ts lineNumbers interface StoredFile { // File-shaped: name: string; // = key size: number; type: string; // = contentType lastModified?: number; arrayBuffer(): Promise; text(): Promise; stream(): ReadableStream; blob(): Promise; // Storage-specific: key: string; etag?: string; metadata?: Record; } ``` `upload` accepts a native `File` as input. `download`, `head`, and `list` all return `StoredFile`. The body accessors on results from `head` and `list` lazy-fetch on call. --- # sync Source: https://files-sdk.dev/docs/api/sync [`transfer`](/docs/api/transfer) is a one-shot copy: it streams every object across, every time. `sync(source, dest, options?)` is the mirror. It reconciles the destination against the source — uploading only what's new or changed, optionally pruning what the source no longer has, and able to preview the whole plan before touching anything. It's what backup and incremental-migration workflows actually reach for. ```ts lineNumbers import { Files, sync } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { r2 } from "files-sdk/r2"; const from = new Files({ adapter: s3({ bucket: "live" }) }); const to = new Files({ adapter: r2({ bucket: "backup", accountId, accessKeyId, secretAccessKey }), }); // Incremental, pruning mirror — re-running only moves the delta. const { uploaded, deleted } = await sync(from, to, { prefix: "uploads/", prune: true, compare: "size", // cross-provider — see the caveat below }); ``` Both arguments are full [`Files`](/docs/api) instances, so each leg honors its own instance's `prefix`, retries, timeouts, and [hooks](/docs/api/onaction). Changed objects are streamed download-to-upload, exactly like `transfer`, so the destination never sees a buffered copy of a large file. Only the body, content type, and user metadata travel with each object. Both sides are walked in full before any work begins — `sync` runs two listings up front (the destination walk drives both the comparison and the prune). That's the cost of a two-sided reconcile; if you only want a cheap one-shot copy, use `transfer` instead. ## What counts as changed `compare` decides whether an object already at the destination is up to date: | `compare` | Skips when… | Use for | | --- | --- | --- | | `"etag"` | size **and** etag both match _(default)_ | same-provider mirrors (S3 → S3) | | `"size"` | byte length matches | cross-provider mirrors | | a function | `(source, dest) => boolean` returns `true` | custom rules (a checksum header, a timestamp) | **etags are only comparable within one scheme.** S3-to-S3 single-part uploads produce matching etags, but across heterogeneous backends (S3 → R2 / GCS / Azure) or for multipart objects, etags differ even for byte-identical content — so the default `"etag"` conservatively re-uploads them. For a cross-provider mirror, use `compare: "size"` (or a custom comparator that reads a checksum you control). `lastModified` is deliberately never used: the destination stamps its own upload time, so it would never match the source and every run would re-upload everything. ## Mirror mode With `prune: true`, after the uploads `sync` deletes every destination key (within the destination scope) that no source key maps onto — leaving the destination an exact mirror. Uploads run **before** prunes, so an interrupted run never leaves the destination missing data it was about to gain. > Prune is destructive. An **empty source** with `prune: true` deletes the entire destination scope. Scope it deliberately with `prefix` / `destPrefix`, and `dryRun` it first. When `transformKey` re-homes keys under a different namespace, set `destPrefix` so prune only ever considers the mirror's own keys (it defaults to `prefix`). ## Dry run `dryRun: true` lists both sides and returns the real reconciliation plan — what _would_ be uploaded, skipped, and deleted — without uploading or deleting anything. `onProgress` doesn't fire, because nothing settles. ```ts lineNumbers const plan = await sync(from, to, { prune: true, dryRun: true }); console.log( `${plan.uploaded.length} to upload, ${plan.deleted?.length} to prune` ); ``` ## Result shape Like the [bulk actions](/docs/bulk), `sync` does **not** throw on a partial failure. Successes, skips, and failures come back separated: ```ts lineNumbers const { uploaded, skipped, deleted, errors } = await sync(from, to, { prune: true, }); ``` | Field | Contents | | --- | --- | | `uploaded` | Source keys written to the destination (new or changed). | | `skipped` | Source keys left untouched because the destination copy was current. | | `deleted` | Destination keys pruned. Present only when `prune` is set. | | `errors` | Per-key `{ key, error }` failures (uploads and prunes). Omitted when none. | `error` is always a normalized [`FilesError`](/docs/api/errors). ## Options ```ts lineNumbers await sync(from, to, { prefix: "uploads/", // only mirror keys under this prefix (scopes the source walk) destPrefix: "uploads/", // scope the destination walk (compare + prune); defaults to prefix transformKey: (key) => `archive/${key}`, // remap each key for the destination prune: true, // delete destination keys the source no longer has compare: "size", // change detection: "etag" (default) | "size" | (s, d) => boolean dryRun: false, // compute the plan without mutating concurrency: 16, // uploads in flight at once (default 8) limit: 500, // page size for both walks stopOnError: true, // bail at the first upload failure (prune is then skipped) signal: controller.signal, // abort the sync onProgress: ({ done, total, key, status }) => {}, }); ``` `concurrency` bounds how many objects stream at once. Under `stopOnError` the run is sequential and a failed upload **skips the prune phase**, so the destination is never trimmed against a half-applied source. `signal` is forwarded to every `list` / `download` / `upload` (the bulk `delete` carries no signal). ## Progress `onProgress` fires once per key as it settles — skips first, then uploads as each streams through, then prunes — carrying a running `done` count, the `total` (uploads + skips + prunes), the `key`, and a `status` of `"uploaded"`, `"skipped"`, or `"deleted"`. It does not fire under `dryRun`. ```ts lineNumbers await sync(from, to, { prune: true, onProgress: ({ done, total, key, status }) => { console.log(`${done}/${total}: ${key} (${status})`); }, }); ``` --- # transfer Source: https://files-sdk.dev/docs/api/transfer `copy` and `move` live inside a single adapter. A migration spans two — and that's the one thing a unified surface uniquely enables. `transfer(source, dest, options?)` walks every object the `source` exposes and streams each one straight to the `dest`, whatever the backends are. ```ts lineNumbers import { Files, transfer } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { r2 } from "files-sdk/r2"; const from = new Files({ adapter: s3({ bucket: "old" }) }); const to = new Files({ adapter: r2({ bucket: "new", accountId, accessKeyId, secretAccessKey }), }); const { transferred, errors } = await transfer(from, to, { prefix: "uploads/", onProgress: ({ done, total, key }) => console.log(`${done}/${total} — ${key}`), }); ``` Both arguments are full [`Files`](/docs/api) instances, not raw adapters, so each leg honors its own instance's `prefix`, retries, timeouts, and [hooks](/docs/api/onaction). Each object is streamed download-to-upload, so the destination never sees a buffered copy of a large file. `transfer` is a one-shot copy. For an incremental, optionally-pruning mirror — skip-unchanged, delete extraneous keys, dry-run the plan — reach for [`sync`](/docs/api/sync) instead. ## What travels The body, content type, and user metadata move with each object. Destination-assigned fields (`etag`, `lastModified`) are fresh on the other side, and `Cache-Control` is **not** carried — a [`StoredFile`](/docs/api/download) doesn't expose it. Metadata is dropped for adapters with no metadata primitive; a metadata key a destination adapter rejects outright (Bunny, Appwrite, PocketBase) surfaces as a per-key error rather than failing the whole run. ## Result shape Like the [bulk actions](/docs/bulk), `transfer` does **not** throw on a partial failure. Successes, skips, and failures come back separated, in walk order: ```ts lineNumbers const { transferred, skipped, errors } = await transfer(from, to); ``` | Field | Contents | | --- | --- | | `transferred` | Source keys copied to the destination. | | `skipped` | Keys skipped because they already existed. Omitted when none. | | `errors` | Per-key `{ key, error }` failures. Omitted when every key wins. | `error` is always a normalized [`FilesError`](/docs/api/errors). ## Options ```ts lineNumbers await transfer(from, to, { prefix: "uploads/", // only walk keys under this prefix transformKey: (key) => `archive/${key}`, // remap each key for the destination overwrite: false, // skip keys already at the destination concurrency: 16, // keys in flight at once (default 8) limit: 500, // page size for the underlying walk stopOnError: true, // bail at the first failure signal: controller.signal, // abort the whole transfer onProgress: ({ done, key, status }) => {}, }); ``` `transformKey` maps the _logical_ key — each instance applies its own `prefix` independently — which makes re-homing under a new namespace (or moving between two prefixed instances) a one-liner. With `overwrite: false`, every key costs one extra `exists()` against the destination. `concurrency` bounds how many objects stream at once (and therefore memory, since each in-flight key holds one open stream). It's ignored under `stopOnError`, which runs sequentially and returns the keys transferred so far plus the first error. `signal` is forwarded to every `list` / `exists` / `download` / `upload` and stops new keys from being scheduled; keys already in flight may still finish or surface as errors. ## Progress `onProgress` fires once per key as it settles, carrying a running `done` count, the `total`, the `key`, and whether it was `transferred` or `skipped`. The source is walked in full before any transfer begins, so `total` is the final denominator from the very first event. ```ts lineNumbers await transfer(from, to, { onProgress: ({ done, total, key, status }) => { console.log(`${done}/${total}: ${key} (${status})`); }, }); ``` --- # upload Source: https://files-sdk.dev/docs/api/upload `files.upload(key, body, options?)` · `files.upload(items)` Writes a body to `key`. Accepts native `File`, `Blob`, `ReadableStream`, `ArrayBuffer`, or `string`. Content type is inferred from the input when possible. ```ts lineNumbers await files.upload("avatars/abc.png", file, { contentType: "image/png", cacheControl: "public, max-age=31536000", metadata: { userId: "123" }, }); // → { key, size, contentType, etag, lastModified } ``` ## Options ## Progress tracking Pass `onProgress` to drive a progress bar as bytes are sent: ```ts lineNumbers await files.upload("big.zip", stream, { onProgress: ({ loaded, total }) => { const pct = total ? Math.round((loaded / total) * 100) : null; console.log(pct === null ? `${loaded} bytes` : `${pct}%`); }, }); ``` Every adapter calls `onProgress`. How fine-grained it is depends on the body and the adapter: - A **`ReadableStream`** body is reported byte-by-byte on **every** adapter, as the bytes are consumed. Its length is unknown, so `total` is omitted — you get `loaded` only. - A **buffered** body (`File`, `Blob`, `ArrayBuffer`, `Uint8Array`, `string`) is handed to the provider whole, so by default it reports `{ loaded: 0, total }` then `{ loaded: total, total }`. - Some adapters report **true byte-level progress for every body type** (buffered included) by tapping their SDK's native upload-progress hook: **S3** and the **S3-compatible** adapters, **R2** (HTTP), **Azure Blob**, **Google Cloud Storage**, **Firebase Storage**, **Vercel Blob**, and **FTP**. Notes: - The S3 family (incl. R2 over HTTP) needs the optional [`@aws-sdk/lib-storage`](https://www.npmjs.com/package/@aws-sdk/lib-storage) package installed; it also enables multipart for large files. - GCS and Firebase Storage switch to a **resumable** upload when `onProgress` is set (only that path emits progress) — one extra round trip versus the default simple upload. - The remaining adapters (Supabase, Convex, Dropbox, Box, OneDrive, Google Drive, SharePoint, Cloudinary, Bunny, Appwrite, PocketBase, Netlify Blobs, UploadThing, SFTP) send buffered bodies in a single request with no progress signal, so those report only the start/finish pair above. Stream bodies still get byte-level. `onProgress` fires only while the upload is in flight and on success; a failed upload emits no final event, and a retry restarts progress. In the array form, each report also carries the item's `key`. ## Multipart uploads Pass `multipart` to upload a large body in parallel parts instead of a single request — the robust path for objects beyond the single-request limit (5 GB on S3) and for `ReadableStream` bodies of unknown length: ```ts lineNumbers // Defaults: 5 MiB parts, 4 in flight. await files.upload("backups/db.tar", stream, { multipart: true }); // Or tune it: await files.upload("backups/db.tar", stream, { multipart: { partSize: 16 * 1024 * 1024, concurrency: 8 }, }); ``` - **S3 and the S3-compatible adapters** (incl. R2 over HTTP) run multipart through the optional [`@aws-sdk/lib-storage`](https://www.npmjs.com/package/@aws-sdk/lib-storage) package, falling back to a single `PutObject` when the body fits in one part. Unknown-length streams use multipart **automatically**, even without the flag. - **OneDrive** uploads above 250 MB (and any `multipart` request) go through a chunked upload session — large files that previously failed now just work. - **GCS** and **Firebase Storage** switch to a resumable upload; `partSize` maps to the chunk size. - **Azure Blob** already splits large bodies into parallel blocks; `multipart` only tunes the block size and concurrency. - **Dropbox** streams `ReadableStream` bodies through its upload session chunk-by-chunk, so a large stream is never buffered whole; `partSize` (rounded to a 4 MiB multiple) tunes the chunk size. - Other adapters already stream natively or only accept a fully-buffered body, so they ignore the option. ## Pause and resume Pass a `control` ([`UploadControl`](/docs/resumable)) to pause, resume, or abort a large upload — and to resume it later, even in a new process, from a serializable session token. It's supported on every adapter whose provider exposes a resumable session — S3 and the S3-compatible adapters, GCS, Firebase Storage, Google Drive, Azure, OneDrive, Dropbox, Vercel Blob, the local filesystem, FTP/SFTP, Supabase, Appwrite, and Cloudinary (Box, bun-s3, and memory pause in-process only); the rest throw. See [Resumable uploads](/docs/resumable) for the full walkthrough. ```ts lineNumbers import { UploadControl } from "files-sdk"; const control = new UploadControl(); const result = files.upload("big.iso", file, { control }); control.pause(); control.resume(); await result; ``` ## Many items Pass an array of `{ key, body, ...options }` to upload many in one call. Each item carries its own `contentType` / `cacheControl` / `metadata` / `multipart`. The call returns a structured result instead of throwing on partial failure: successes land in `uploaded`, per-item failures (including invalid keys) in `errors`, both in the order supplied. It honors the client's `prefix` and fans out with bounded `concurrency` (default 8); `stopOnError: true` stops at the first failure. ```ts lineNumbers const result = await files.upload( [ { key: "avatars/a.png", body: a, contentType: "image/png" }, { key: "avatars/b.png", body: b }, ], { concurrency: 8, stopOnError: false } ); result.uploaded; // UploadResult[] — successes, in the order supplied result.errors; // undefined when every item succeeded ``` ### Item (array form) ### Options (array form) --- # url Source: https://files-sdk.dev/docs/api/url `files.url(key, options?)` Returns a URL the caller can use to fetch `key`. Every adapter returns the most direct URL it can produce. Signing adapters (S3 and the S3-compatible catalog — R2 over HTTP plus every regional / budget / decentralised wrapper — alongside Google Cloud Storage, Azure with shared key, Supabase, UploadThing in `private` mode, and the R2 binding when HTTP credentials are also configured) sign a read URL - defaulting to a 1-hour expiry, override per-call via `{ expiresIn }` or per-adapter via `defaultUrlExpiresIn`. If the adapter is constructed with a `publicBaseUrl` (CDN, custom domain, `r2.dev`, Bunny Pull Zone) or UploadThing's `public-read` ACL, that wins and the URL is built without signing. Three configurations have no URL primitive and throw: Vercel Blob in `access: "private"` mode, an R2 Workers binding without either `publicBaseUrl` or HTTP credentials, and Bunny Storage without `publicBaseUrl` because the Storage API URL requires an `AccessKey` header. ```ts lineNumbers // One call, every adapter. S3 and the S3-compatible catalog (R2 over HTTP // plus every regional / budget / decentralised wrapper) sign a GetObject (1h // default, override with { expiresIn }); Google Cloud Storage and Azure sign // a read URL natively with the same default; Supabase signs via createSignedUrl // (or returns the public URL when constructed with public:true); Vercel Blob // (public), UploadThing (public-read), and Bunny Storage with publicBaseUrl // return their CDN URLs. If you configured `publicBaseUrl` on the adapter, that // wins and signing is skipped. const url = await files.url("avatars/abc.png"); const short = await files.url("avatars/abc.png", { expiresIn: 60 }); // Force download (defeat stored XSS from user-uploaded HTML/SVG). // Forces signing even if `publicBaseUrl` is configured - a permanent // CDN URL has no signature to bind the override into, and silently // dropping a security ask would be a regression. const safe = await files.url("avatars/abc.png", { responseContentDisposition: "attachment", }); ``` ## Options --- # Bulk actions Source: https://files-sdk.dev/docs/bulk `upload`, `download`, `head`, and `exists` each take a single key or an array; `delete` takes one key or many. The array form fans out with bounded concurrency (8 by default) and returns a structured result that keeps successes and failures separate, in input order — so one bad key never sinks the whole batch. ```ts lineNumbers // Upload several objects in one call const { uploaded, errors } = await files.upload([ { key: "a.txt", body: "alpha" }, { key: "b.txt", body: "beta", contentType: "text/plain" }, ]); // exists() splits the keys into present and absent const { existing, missing } = await files.exists(["a.txt", "b.txt", "c.txt"]); // delete() reports what it removed const { deleted } = await files.delete(["a.txt", "b.txt"]); ``` Each method returns a result shaped for what it does, and every one carries an optional `errors` array — omitted entirely when every item succeeded: | Method | Array form returns | | ---------- | -------------------------------- | | `upload` | `{ uploaded, errors? }` | | `download` | `{ downloaded, errors? }` | | `head` | `{ files, errors? }` | | `exists` | `{ existing, missing, errors? }` | | `delete` | `{ deleted, errors? }` | The success arrays come back in the order you supplied the keys. Each entry in `errors` is `{ key, error }`, where `error` is a normalized [`FilesError`](/docs/api/errors). Invalid keys (empty, or containing null bytes) are reported there too, never thrown. ## Partial failure By default the array forms don't throw on a partial failure — every item is attempted and per-key failures collect in `errors`. Pass `stopOnError: true` to bail at the first failure and return the results gathered so far plus that error (this path runs sequentially), or `concurrency` to tune the fan-out. ```ts lineNumbers const result = await files.upload(items, { concurrency: 16, stopOnError: false, }); if (result.errors) { for (const { key, error } of result.errors) { logger.warn("upload failed", key, error.code); } } ``` ## Native bulk vs. fan-out `upload`, `download`, `head`, and `exists` have no provider batch primitive, so the SDK always fans out to per-key calls under the concurrency limit. `delete` is the exception: adapters with a native bulk primitive (S3 `DeleteObjects`, chunked into batches of 1000; Supabase; UploadThing) remove everything in a single request and ignore `concurrency`, while the rest fall back to bounded fan-out. When a native bulk provider only reports that the whole request failed, that error is mapped onto each affected key. ## Retries and hooks Bulk calls are **not** retried — `retries` applies to single-operation calls only — so [`onRetry`](/docs/api/onretry) never fires for them. [`onAction`](/docs/api/onaction) emits one event for the whole call, carrying the caller's `keys` and the aggregated result; the per-item failures inside `errors` are not rejections, so they don't fire [`onError`](/docs/api/onerror). The client's [`prefix`](/docs/prefixes) is honored throughout: keys are resolved under it on the way in and stripped back off on the way out, just as in the single-key forms. --- # Cancellation Source: https://files-sdk.dev/docs/cancellations Pass a `signal` to bind a call to an `AbortController`. The moment it aborts, the in-flight call rejects with a `FilesError` carrying `aborted: true` — for **every** adapter, whether or not the provider's SDK supports cancellation. ```ts lineNumbers const controller = new AbortController(); const upload = files.upload("avatars/abc.png", file, { signal: controller.signal, }); // Later, abort it — the call rejects immediately. controller.abort(); ``` ## Detecting an abort An aborted call rejects with a `Provider` [`FilesError`](/docs/api/errors) whose `aborted` flag is `true`. That flag, not the `code`, is what distinguishes a cancellation (or a [timeout](/docs/timeouts)) from a real provider failure. ```ts lineNumbers import { FilesError } from "files-sdk"; try { await files.download("big.zip", { signal: controller.signal }); } catch (err) { if (err instanceof FilesError && err.aborted) { return; // expected — the caller (or a timeout) aborted it } throw err; } ``` ## Constructor and per-call signals `signal` can also be set on the constructor, where it applies to every single-key operation the instance runs — handy for tearing down all in-flight work when a request or job ends. When both a constructor signal and a per-call signal are present, **either** one aborting cancels the call. ```ts lineNumbers const files = new Files({ adapter: s3({ bucket: "uploads" }), signal: req.signal, // bind every operation to the request lifecycle }); ``` The array forms (`upload([…])`, `delete([…])`, …) don't take a per-call `signal`; they manage work through `concurrency` / `stopOnError` instead. Cancellation is a single-key concern. ## What the adapter does downstream Failing fast at the `Files` layer is guaranteed; cancelling the _provider request_ underneath is not. Adapters whose SDK exposes cancellation forward the signal directly — the S3 adapter and the whole S3-compatible catalog (R2 over HTTP, MinIO, Spaces, and every regional / budget / decentralised wrapper), Vercel Blob, and UploadThing's fetch-backed reads. Adapters whose SDK has no cancellation primitive (for example UploadThing's `delete`) still reject at the `Files` layer the instant the signal aborts, but the provider request may run to completion in the background. Aborts are never retried — see [Retries](/docs/retries) — and a [`timeout`](/docs/timeouts) aborts the call the same way when it fires. --- # Capabilities Source: https://files-sdk.dev/docs/capabilities The unified surface is the common subset every adapter implements, but adapters differ at the edges: some honor byte-range reads, some can mint a signed URL, some copy server-side. The wrapper already gates on these per-adapter — pass a `range` to an adapter that has no range primitive and it throws _before_ any provider call. `files.capabilities` turns that implicit knowledge into a queryable surface, so you can branch up front instead of discovering a limit by catching an error. ```ts const files = new Files({ adapter: s3({ bucket: "uploads" }) }); if (files.capabilities.rangeRead) { // Safe to stream a byte range — the adapter has a range primitive. const clip = await files.download(key, { range: { start: 0, end: 1023 } }); } if (files.capabilities.signedUrl.supported) { return files.url(key, { expiresIn: 600 }); } // No signing primitive — stream the bytes through the SDK instead. return files.download(key); ``` ## The shape ```ts interface AdapterCapabilities { rangeRead: boolean; uploadProgress: boolean; delimiter: boolean; metadata: boolean; cacheControl: boolean; multipart: boolean; serverSideCopy: boolean; signedUrl: { supported: boolean; maxExpiresIn?: number }; } ``` | Field | `true` means | Maps to | | --- | --- | --- | | `rangeRead` | `download({ range })` returns only the requested bytes | `download` | | `uploadProgress` | `upload({ onProgress })` reports byte-level progress natively | `upload` | | `delimiter` | `list({ delimiter })` returns S3-style common prefixes | `list` | | `metadata` | `upload({ metadata })` persists arbitrary user metadata | `upload` | | `cacheControl` | `upload({ cacheControl })` stores a `Cache-Control` header | `upload` | | `multipart` | the adapter exposes a resumable / multipart upload primitive | `upload({ control })` | | `serverSideCopy` | `copy()` runs server-side, with no body re-transfer through your process | `copy` | | `signedUrl` | `url()` can mint a signed or tokenized URL — see below | `url` | Every field mirrors an operation the unified API actually has. There are deliberately no flags for `raw`-only territory (object versioning, checksums, conditional writes, POST policies) — advertising a flag for a non-operation would turn the matrix into a back-door spec, where a wrong flag is worse than no flag. ### `signedUrl` ```ts signedUrl: { supported: boolean; maxExpiresIn?: number } ``` `supported` is `true` when `url()` mints a signed or tokenized download URL that grants access without the caller's own credentials and is more than a permanent public link — an S3 SigV4 URL, an Azure SAS, a GCS signed URL, a Box or PocketBase access-token URL. It's `false` when the adapter has no signing primitive: it returns only a permanent public URL (Vercel Blob, Appwrite, Convex) or throws because it can't mint one at all (the filesystem, FTP/SFTP, OneDrive / Google Drive outside their public-link mode). When `false`, prefer `download()`. `maxExpiresIn` is set only when the provider enforces a hard ceiling on `expiresIn` **in code** — for example Dropbox temporary links cap at 4 hours and `url()` throws above that. It is deliberately _not_ set for soft or config-dependent limits: AWS SigV4's 604800-second ceiling is an infra limit the SDK passes through without checking, and Azure's 7-day cap only applies to user-delegation SAS (account-key SAS has no such limit). Those live in [Provider gaps](/docs/provider-gaps), not here. Whether a supported URL honors `expiresIn` exactly is also per-provider — some pin the lifetime server-side (Box, PocketBase) and ignore the request. ## How it's derived `capabilities` is computed live from the adapter on every read, so a plugin that swaps behavior is always reflected. The first six fields read the exact per-adapter flags and optional methods the wrapper already gates on (`supportsRange`, `reportsUploadProgress`, `supportsDelimiter`, `supportsMetadata`, `supportsCacheControl`, and the presence of `resumableUpload`), so they can never drift from runtime behavior. `serverSideCopy` and `signedUrl` are declared by each adapter and default to the conservative value (`false`) when an adapter declares nothing — a caller that doesn't advertise reads as "no", never a wrong "yes". If you're writing a custom adapter, set `supportsServerSideCopy` and `signedUrl` alongside the existing `supports*` flags to make your adapter introspectable; both are optional and advisory (they don't gate any operation). --- # CLI overview Source: https://files-sdk.dev/docs/cli The CLI wraps the same adapters as the SDK behind a single `files` binary - JSON-by-default output, stdin/stdout streaming, and a built-in MCP server. Install it, point it at a provider, and every SDK method is available as a command. ## Install The CLI ships with the `files-sdk` package — install it globally to get a `files` binary on your `PATH`, or invoke it via `npx` / `bunx` for one-off commands. ```package-install npm install -g files-sdk ``` One-shot, no install: ```package-install npx -p files-sdk files --provider fs --root ./uploads list ``` Adapter SDKs (AWS, GCP, Azure, Dropbox, etc.) are loaded lazily on first use, so cold-start cost matches whichever single provider you select — not the union of all of them. Those SDKs are optional peer dependencies of `files-sdk`, so install the one for the provider you intend to use alongside the CLI — for example `npm install -g files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner` for S3. See the per-adapter docs for the exact package list. ## Pick a provider Pass `--provider ` on every call, or set `FILES_SDK_PROVIDER` once. Provider-specific credentials come from the adapter's standard env vars (`AWS_ACCESS_KEY_ID`, `BLOB_READ_WRITE_TOKEN`, `GOOGLE_APPLICATION_CREDENTIALS`, etc.), so the same environment that works with the SDK works with the CLI.
s3 r2 gcs azure vercel-blob netlify-blobs supabase minio neon archil digitalocean-spaces backblaze-b2 wasabi scaleway ovhcloud hetzner tigris storj filebase akamai idrive-e2 vultr ibm-cos oracle-cloud exoscale alibaba tencent yandex uploadthing bunny-storage dropbox box google-drive onedrive sharepoint appwrite pocketbase firebase-storage cloudinary fs ftp sftp webdav
Common short flags cover the obvious fields (`--bucket`, `--region`, `--endpoint`, `--root`, `--container`, `--token`, etc.). For the long tail, `--config-json '{...}'` accepts the raw adapter options blob — anything the SDK factory accepts, the CLI can pass through. ## Next steps - [Commands](/docs/cli/commands) - every command, flag by flag. - [Output](/docs/cli/output) - the JSON-by-default output contract and `--pretty`. - [Streaming](/docs/cli/streaming) - piping bodies through stdin and stdout. - [Agents](/docs/cli/agents) - using the CLI from coding agents, plus the built-in MCP server. --- # Wiring agents Source: https://files-sdk.dev/docs/cli/agents Three patterns, ordered by how much trust you're extending to the agent. ```bash lineNumbers # 1. Quick exploration — let an agent inspect a bucket without writing code files --provider s3 --bucket uploads list --prefix invoices/ --limit 20 | jq '.items[].key' # 2. Programmatic loop — feed JSON straight to the next step cursor="" while :; do page=$(files --provider s3 --bucket uploads list --prefix logs/ --limit 100 \ ${cursor:+--cursor "$cursor"}) echo "$page" | jq -r '.items[].key' | while read key; do files --provider s3 --bucket uploads download "$key" --stdout | gunzip | grep ERROR done cursor=$(echo "$page" | jq -r '.cursor // empty') [ -z "$cursor" ] && break done # 3. Provider override via env — agents don't need to thread --provider everywhere export FILES_SDK_PROVIDER=fs files --root ./sandbox list ``` For read-only investigation, the JSON output piped through `jq` is usually enough. For multi-step workflows, the [MCP server](/docs/cli/mcp) keeps tool calls structured and avoids quoting bugs in shell composition. For everything in between, the plain CLI with `--dry-run` gates is the path of least surprise. --- # Commands Source: https://files-sdk.dev/docs/cli/commands Each command maps to a `Files` method. Same semantics, same `FilesError` codes, same `StoredFile` fields on the way out (emitted as flat JSON). | Command | SDK method | What it does | | --- | --- | --- | | `upload` | [`upload`](/docs/api/upload) | Write a file or piped stdin to a key | | `download` | [`download`](/docs/api/download) | Read a key to disk or stream it to stdout | | `head` | [`head`](/docs/api/head) | Fetch metadata without the body | | `exists` | [`exists`](/docs/api/exists) | Test a key — prints `{ exists, key }`, sets exit code | | `list` | [`list`](/docs/api/list) | Page through keys under a prefix | | `copy` | [`copy`](/docs/api/copy) | Copy a key to a new key | | `move` | [`move`](/docs/api/move) | Move (rename) a key | | `delete` | [`delete`](/docs/api/delete) | Delete one or more keys | | `url` | [`url`](/docs/api/url) | Get a read URL — presigned or public | | `sign-upload` | [`signedUploadUrl`](/docs/api/signed-upload-url) | Mint a browser-direct upload policy | | `transfer` | [`transfer`](/docs/api/transfer) | Stream every object to another provider | | `sync` | [`sync`](/docs/api/sync) | Mirror onto another provider (skip-unchanged, prune) | | `capabilities` | [`capabilities`](/docs/capabilities) | Print what the configured adapter can do, as JSON | All examples use `--provider s3 --bucket uploads`; swap in any [provider](/docs/providers) and its flags. ## Methods ### upload Write a body to a key. Read it from a file with `--file`, or pipe it through `--stdin`. `--content-type` is otherwise inferred from the key. ```bash lineNumbers files --provider s3 --bucket uploads \ upload reports/2026-q1.pdf --file ./report.pdf --content-type application/pdf cat report.pdf | files --provider s3 --bucket uploads \ upload reports/2026-q1.pdf --stdin --content-type application/pdf ``` For large objects see [multipart](#byte-ranges-and-multipart); to push a whole local tree see [directories](#directories). ### download Read a key back. `--out` writes it to a file; `--stdout` streams the raw bytes so you can pipe them onward. ```bash lineNumbers files --provider s3 --bucket uploads download reports/2026-q1.pdf --out ./report.pdf files --provider s3 --bucket uploads download reports/2026-q1.pdf --stdout > report.pdf ``` To pull a slice instead of the whole object see [byte ranges](#byte-ranges-and-multipart); to fetch many keys at once see [directories](#directories). ### head Fetch an object's metadata — size, content type, etag, last-modified — without downloading the body. ```bash lineNumbers files --provider s3 --bucket uploads head reports/2026-q1.pdf ``` Pass several keys to inspect them in one call; see [many keys at once](#many-keys-at-once). ### exists Test whether a key exists. It prints `{ exists, key }` and signals the result through the exit code too, so it drops straight into a shell conditional. ```bash lineNumbers files --provider s3 --bucket uploads exists reports/2026-q1.pdf # exit 0 = exists, 1 = missing ``` ### list Return one page of keys under a prefix. `--prefix` filters this call (distinct from the instance-wide [`--key-prefix`](#global-flags)) and `--limit` caps the page. ```bash lineNumbers files --provider s3 --bucket uploads list --prefix reports/ --limit 50 ``` The result carries a `cursor` for the next page. Pass `--all` to follow the cursor to the end and return every item in one result — mind the memory cost on huge buckets: ```bash lineNumbers files --provider s3 --bucket uploads list --prefix logs/ --all | jq '.items[].key' ``` Pass `--delimiter` to collapse keys into folders — the direct files come back in `items` and the subfolders in a `prefixes` array (full keys with the trailing delimiter). This is the building block for a file-browser view; it throws on adapters with no folder concept, and can't be combined with `--all`: ```bash lineNumbers files --provider s3 --bucket uploads list --prefix photos/ --delimiter / | jq '.prefixes' # ["photos/2023/", "photos/2024/"] ``` ### copy Copy a key to a new key, leaving the source in place. ```bash lineNumbers files --provider s3 --bucket uploads copy reports/2026-q1.pdf reports/archive/q1.pdf ``` ### move Move (rename) a key — a copy followed by a delete of the source. ```bash lineNumbers files --provider s3 --bucket uploads move uploads/tmp-q1.pdf reports/2026-q1.pdf ``` ### delete Delete a key. ```bash lineNumbers files --provider s3 --bucket uploads delete reports/archive/q1.pdf ``` Pass several keys to delete them in one fan-out; see [many keys at once](#many-keys-at-once). ### url Get a read URL for a key — presigned and short-lived on signing adapters, a public URL for CDN-backed providers. `--expires-in` sets the lifetime in seconds. ```bash lineNumbers files --provider s3 --bucket uploads url reports/2026-q1.pdf --expires-in 600 ``` ### sign-upload Mint a presigned POST policy for browser-direct uploads. `--max-size` is enforced server-side, so the client can't exceed it. ```bash lineNumbers files --provider s3 --bucket uploads sign-upload uploads/avatar.png \ --expires-in 600 --max-size 5242880 --content-type image/png ``` ### capabilities Print the configured adapter's [capability snapshot](/docs/capabilities) as JSON — range reads, native upload progress, list delimiters, metadata, cache-control, multipart, server-side copy, and signed URLs. Pure introspection; it makes no provider call. ```bash lineNumbers files --provider s3 --bucket uploads capabilities ``` ### transfer Stream every object from the configured (source) provider to another provider, given as a JSON config. The source uses the normal global flags (so `--key-prefix` scopes it); `--prefix` filters the walk, and `--no-overwrite` skips keys already present at the destination. ```bash lineNumbers # Migrate an S3 prefix to R2, skipping anything already copied files --provider s3 --bucket old --verbose \ transfer \ --to '{"provider":"r2","bucket":"new","accountId":"...","accessKeyId":"...","secretAccessKey":"..."}' \ --prefix uploads/ --no-overwrite --concurrency 16 ``` ### sync Mirror the source onto another provider: upload new or changed objects, skip the unchanged ones, and — with `--prune` — delete destination keys the source no longer has. `--compare` picks the change check (`etag`, the default, or `size` for cross-provider mirrors). Unlike every other command, `--dry-run` here lists both sides and prints the real reconciliation plan (`{ uploaded, skipped, deleted }`) without mutating anything — preview a `--prune` before you run it. ```bash lineNumbers # Back up an S3 prefix to R2 — only the delta moves, and the backup mirrors deletes files --provider s3 --bucket live --verbose \ sync \ --to '{"provider":"r2","bucket":"backup","accountId":"...","accessKeyId":"...","secretAccessKey":"..."}' \ --prefix uploads/ --prune --compare size --concurrency 16 # Preview what a pruning mirror would do, read-only files --provider s3 --bucket live \ sync --to '{"provider":"r2","bucket":"backup",...}' --prune --dry-run ``` ## Global flags These apply to every command and mirror the `Files` constructor and `OperationOptions`: ```bash lineNumbers # --key-prefix scopes every operation under a base path (the instance prefix, # distinct from `list --prefix`, which is a one-off filter). Listed/returned # keys come back relative to it. files --provider s3 --bucket uploads --key-prefix tenants/acme \ list # lists under tenants/acme/ # --timeout (per attempt, ms) and --retries (provider failures) apply to all commands files --provider s3 --bucket uploads --timeout 10000 --retries 3 \ head reports/2026-q1.pdf ``` ## Many keys at once `head`, `exists`, and `delete` take multiple keys and return a structured result instead of throwing on partial failure. `--concurrency` and `--stop-on-error` tune the fan-out: ```bash lineNumbers files --provider s3 --bucket uploads head a.txt b.txt c.txt files --provider s3 --bucket uploads delete a.txt b.txt --concurrency 16 files --provider s3 --bucket uploads exists a.txt b.txt --stop-on-error ``` ## Byte ranges and multipart ```bash lineNumbers # Download a byte range (0-based, inclusive) — for video seeking or resuming. # Range downloads throw on adapters with no native range primitive. files --provider s3 --bucket uploads download big.mp4 --out head.mp4 --range 0-1048575 # Upload in parallel parts (robust for large objects). --part-size / # --multipart-concurrency tune it and imply --multipart. files --provider s3 --bucket uploads \ upload big.iso --file ./big.iso --multipart --part-size 16777216 ``` ## Directories Upload a whole local tree, or download many keys into a directory. Each file is keyed by (or written to) its relative path; content types are inferred per file on upload. ```bash lineNumbers # Upload every file under ./build, keyed by relative path (composes with --key-prefix) files --provider s3 --bucket site --key-prefix assets upload --dir ./build # Download many keys into a directory, recreating their key paths underneath it files --provider s3 --bucket uploads \ download docs/a.pdf docs/b.pdf logos/c.png --out-dir ./pulled ``` --- # MCP server Source: https://files-sdk.dev/docs/cli/mcp `files ... mcp` boots a read-only [MCP server](https://modelcontextprotocol.io) on stdio. By default it exposes `download`, `head`, `exists`, `list`, `url`, and `capabilities`. The provider and credentials are bound at server startup; the agent only passes operation arguments, never secrets. Pass `--allow-writes` to also expose mutating tools: `upload`, `delete`, `copy`, `move`, `sign-upload`, `transfer`, and `sync`. The tools mirror the CLI surface: `download` accepts a byte `range`, `head`/`exists` take arrays of keys plus `concurrency`/`stopOnError`, `list` accepts `all` to walk every page or a `delimiter` to return one folder level (files in `items`, subfolders in `prefixes`), and `capabilities` takes no arguments and returns what the bound adapter can do so the agent can branch before calling. With `--allow-writes`, `upload` accepts `multipart`, `delete` takes arrays of keys plus `concurrency`/`stopOnError`, `transfer` takes a destination provider config (`to`) to copy objects across backends, and `sync` mirrors onto a destination provider with `prune`, `compare`, and `dryRun` (set `dryRun` to preview the reconciliation plan read-only). ```bash lineNumbers # Start the read-only MCP server on stdio files --provider s3 --bucket uploads mcp # Opt into mutation tools files --provider s3 --bucket uploads mcp --allow-writes ``` ```jsonc lineNumbers // Wire it into Claude Code (~/.claude.json or .claude/mcp.json) { "mcpServers": { "files-sdk": { "command": "files", "args": ["--provider", "s3", "--bucket", "uploads", "mcp"], "env": { "AWS_ACCESS_KEY_ID": "...", "AWS_SECRET_ACCESS_KEY": "...", }, }, }, } ``` Binary payloads are roundtripped as base64 over MCP, so binary downloads (`download`) and, when writes are enabled, uploads (`upload` with a `base64` body) survive intact. --- # JSON output Source: https://files-sdk.dev/docs/cli/output Every command emits one JSON line on success. Errors go to `stderr` with a stable `{ error: { code, message } }` envelope, never mixed with the success channel — so a JSON parser downstream sees either a clean record or nothing. ```bash lineNumbers # JSON output is the default — pipe straight to jq. $ files --provider fs --root /tmp/store head reports/q1.pdf {"key":"reports/q1.pdf","name":"q1.pdf","size":48213,"type":"application/pdf","lastModified":1778881504647,"etag":"\"9feb94ca37e5d155\""} # Errors go to stderr with a stable error code. Exit codes: # 0 ok # 1 NotFound (or exists → false) # 2 Provider / unknown error # 3 Unauthorized # 4 Conflict $ files --provider fs --root /tmp/store head nope.txt {"error":{"code":"NotFound","message":"ENOENT: no such file or directory, stat '/tmp/store/nope.txt'"}} $ echo $? 1 ``` Use `--pretty` for indented JSON when reading manually, or `--no-json` for plain-text output (still suitable for `grep`, just not for parsing). --- # Streaming & dry-run Source: https://files-sdk.dev/docs/cli/streaming `upload --stdin` reads the body from `stdin`; `download --stdout` writes it to `stdout`. No intermediate file, no extra copy. Metadata for stdout downloads is suppressed by default and only emitted to `stderr` when `--verbose` is set, so the byte stream stays clean. `--dry-run` resolves the provider and prints the operation it _would_ run, without making a network call. Handy as a sanity check inside an agent loop before letting it execute writes. ```bash lineNumbers # Stream binary in/out without temp files ffmpeg -i talk.mov -c copy -f mp4 - \ | files --provider r2 --bucket talks upload 2026/q1/keynote.mp4 --stdin --content-type video/mp4 files --provider r2 --bucket talks download 2026/q1/keynote.mp4 --stdout \ | ffprobe -i - 2>&1 # Plan before doing — useful as a sanity check inside an agent loop files --provider s3 --bucket uploads --dry-run delete reports/q1.pdf # → {"action":"delete","dryRun":true,"provider":"s3","keys":["reports/q1.pdf"]} # Verbose adds stack traces to error output files --provider s3 --bucket uploads --verbose head missing.txt ``` --- # Escape hatch Source: https://files-sdk.dev/docs/escape-hatch The unified surface is deliberately small — the methods every adapter can implement. When you need a provider feature that isn't part of it (S3 versioning, lifecycle rules, ACLs, object tags, anything), drop down to the native client instead of waiting for it to be wrapped. ## `files.raw` `raw` is the underlying provider client, typed per adapter — `S3Client` for `s3()`, `VercelBlobClient` for `vercelBlob()`, an `R2Bucket` (or `S3Client` in HTTP mode) for `r2()`, and so on. The type flows through from the adapter, so you keep full autocomplete on it. ```ts lineNumbers const files = new Files({ adapter: s3({ bucket: "uploads" }) }); // `files.raw` is typed as S3Client — no cast needed. await files.raw.send( new PutObjectAclCommand({ Bucket: "uploads", Key: "a.png", ACL: "public-read", }) ); ``` `files.adapter` is the adapter itself, if you need its `name` or want to pass it around; `files.adapter.raw` is the same client as `files.raw`. ## What `raw` bypasses The native client talks to the provider directly, so none of the `Files` wrapper's behavior applies to calls you make through it: - **No prefix scoping.** You pass the provider's full key yourself, including any `prefix` the `Files` instance was constructed with — `files.raw` doesn't know about it. - **No normalized errors.** A failure throws the provider's own error type, not a [`FilesError`](/docs/api/errors); catch and classify accordingly. - **No hooks, retries, timeouts, or cancellation.** [Hooks](/docs/usage#hooks) don't fire, and `signal` / `timeout` / `retries` aren't applied. Use the provider client's own equivalents. In short, `raw` is an unmanaged door straight to the provider. Reach for it for the one feature you need, and keep using the unified methods for everything else. ## Keeping the type The `raw` type is inferred from the adapter you pass in, so it stays typed as long as the instance keeps its concrete type. Widening to the bare `Files` (whose adapter is the default `Adapter`) erases it: ```ts lineNumbers const files = new Files({ adapter: s3({ bucket: "uploads" }) }); files.raw; // S3Client const widened: Files = files; widened.raw; // unknown ``` If you store a `Files` instance on a typed field, annotate it with the adapter's type (for example `Files`) rather than the bare `Files`, and `raw` stays typed. See the Compatibility section on each [adapter's docs page](/docs/adapters/s3) for what it supports through the unified surface before reaching for the escape hatch. --- # FAQ Source: https://files-sdk.dev/docs/faq ## Which providers are supported? 40+, behind one API: AWS S3 and the whole S3-compatible long tail (R2, MinIO, Backblaze, Wasabi, Scaleway, OVH, Hetzner, Tigris, Storj, Filebase, Akamai, IDrive, Vultr, IBM COS, Oracle, Exoscale, DigitalOcean Spaces), Vercel Blob, Google Cloud Storage, Azure, Supabase, the consumer providers (Dropbox, Box, Google Drive, OneDrive, SharePoint), upload-focused services (UploadThing, Cloudinary), the BaaS stack (Appwrite, PocketBase, Firebase Storage), and a local `fs` adapter for tests. See the full list under [Adapters](/docs/adapters/s3). ## Do I have to install every provider's SDK? No. Adapters are subpath exports (`files-sdk/s3`, `files-sdk/r2`, ...) and each provider SDK is an optional peer dependency, loaded lazily on first use. Install only the one(s) you wire up - the SDK you don't import is never bundled. See [Installation](/docs/installation). ## Why am I getting "Cannot find module '@aws-sdk/client-s3'"? Provider SDKs are peer dependencies - install only the ones for the adapters you use. The per-adapter docs list the exact packages. ## How do I switch providers? Swap the adapter you pass to `new Files({ adapter })`. Every call site below the constructor stays identical - the code that uploads to S3 is the code that uploads to Vercel Blob. See [Usage](/docs/usage). ## What happens when a provider doesn't support a feature? Every method normalizes to the same shape, but providers differ in what they offer natively. Where a provider lacks a primitive, the method throws a `FilesError` rather than silently misbehaving (for example, `signedUploadUrl` on Vercel Blob). Each [adapter's docs page](/docs/adapters/vercel-blob) has a Compatibility section listing its per-method support, and `files.raw` is the [typed escape hatch](/docs/escape-hatch) for anything outside the unified surface. ## Can I use it in the browser? The core `Files` class and the `StoredFile` type are isomorphic, but most adapters wrap server-only SDKs. For browser uploads, mint a contract with [`signedUploadUrl()`](/docs/api/signed-upload-url) on the server and have the client `fetch` directly to the provider. See [Troubleshooting](/docs/troubleshooting) for the full pattern. ## How are errors handled? Every method throws a single `FilesError` with a normalized `code` - `NotFound`, `Unauthorized`, `Conflict`, `ReadOnly`, or `Provider` - and the original provider error attached as `cause`. See [Errors](/docs/api/errors). ## Does `delete` throw on a missing key? Depends on the provider. S3, R2, and Vercel Blob treat delete as idempotent and resolve successfully; strict providers throw a `FilesError` with `code: "NotFound"`. If you need consistent behavior, gate on `await files.exists(key)` first. See [delete](/docs/api/delete). ## Why is `contentType` `application/octet-stream`? When `upload` can't infer the type from the input (a raw `ArrayBuffer`, a `ReadableStream`, or a string with no extension hint), it defaults to `application/octet-stream`. Pass `contentType` explicitly to override it. ## Why does the body come back empty from `head` / `list`? Body accessors on `head` and `list` results are lazy - they fetch on call. If you serialize a `StoredFile` without invoking an accessor, you get the metadata only. That's the intent; call `.arrayBuffer()` / `.text()` / `.blob()` / `.stream()` to materialize the bytes. See [Troubleshooting](/docs/troubleshooting) for the lazy-body gotcha. ## Is there a CLI? Yes - a `files` binary with JSON-by-default output, stdin/stdout streaming, and a built-in MCP server, with the same semantics as the SDK. See the [CLI](/docs/cli) docs. --- # Installation Source: https://files-sdk.dev/docs/installation `files-sdk` itself is small - one runtime dependency (`commander`, for the CLI) behind a tree of subpath exports. Install the package, then add the peer dependencies for the adapter you're wiring up. Anything you don't import is never bundled. ```package-install npm install files-sdk ``` The package ships its own TypeScript types, so there's no `@types/files-sdk` to chase down. It's published as ESM only (`"type": "module"`) and targets modern runtimes - Node 18+ and Bun. From a CommonJS file, reach it with a dynamic `import()`. ## Adapter peer dependencies Adapters are subpath exports - `files-sdk/s3`, `files-sdk/r2`, `files-sdk/vercel-blob`, and so on - and each one's provider SDK is an optional peer dependency, loaded lazily on first use. The SDK you don't import is never bundled, so both install size and cold-start cost stay proportional to the providers you actually wire up. Install the peer deps for your adapter alongside the SDK. For S3 - and every S3-compatible store, since R2 over HTTP, MinIO, Backblaze, Wasabi, DigitalOcean Spaces, and the rest all wrap the same client: ```package-install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner ``` The shape is the same for every adapter, only the packages change: Google Cloud Storage adds `@google-cloud/storage` and `google-auth-library`; Azure adds `@azure/storage-blob`, `@azure/core-auth`, and `@azure/identity`; Vercel Blob needs only `@vercel/blob`. A few adapters need no extra packages at all - `files-sdk/fs` and `files-sdk/bun-s3` use primitives the runtime already provides. The [per-adapter docs](/docs/adapters/s3) list the exact packages for each provider, and the [provider catalog](/docs/providers) exposes the same data programmatically (`peerDeps`) if you're generating install commands or building a config UI. ## Missing a peer dependency? Import an adapter without its peer installed and Node throws `ERR_MODULE_NOT_FOUND` naming the package that's missing - the SDK doesn't vendor or shim provider clients, so the failure is loud and specific rather than a silent fallback. Install the named package and the import resolves. Once your adapter's packages are in place, head to [Usage](/docs/usage) to construct a `Files` instance and run the core methods. --- # Multipart uploads Source: https://files-sdk.dev/docs/multipart Multipart splits a body into parts and uploads them in parallel, then stitches them back together server-side. It's the robust path for objects beyond the single-request limit (5 GB on S3) and for `ReadableStream` bodies of unknown length: a single PUT must either buffer the whole body or know its length up front, while multipart streams part-by-part with bounded memory. It's a per-call option on [`upload`](/docs/api/upload) — the only method that writes a body, and so the only one to which it applies. Pass `multipart: true` for sensible defaults, or an object to tune `partSize` and `concurrency`: ```ts lineNumbers // Defaults: 5 MiB parts, 4 in flight. await files.upload("backups/db.tar", stream, { multipart: true }); // Or tune it: await files.upload("backups/db.tar", stream, { multipart: { partSize: 16 * 1024 * 1024, concurrency: 8 }, }); ``` In the [array form](/docs/bulk), `multipart` is a per-item option — set it on each `{ key, body }` that needs it. ## What each adapter does The flag maps onto whatever chunking primitive the provider exposes, so the mechanics differ but the contract — a large body uploaded reliably — does not: - **S3 and the S3-compatible adapters** (incl. R2 over HTTP) run multipart through the optional [`@aws-sdk/lib-storage`](https://www.npmjs.com/package/@aws-sdk/lib-storage) package, falling back to a single `PutObject` when the body fits in one part. Unknown-length streams use multipart **automatically**, even without the flag. - **OneDrive** uploads above 250 MB (and any `multipart` request) go through a chunked upload session — large files that previously failed now just work. - **GCS** and **Firebase Storage** switch to a resumable upload; `partSize` maps to the chunk size. - **Azure Blob** already splits large bodies into parallel blocks; `multipart` only tunes the block size and concurrency. - **Dropbox** streams `ReadableStream` bodies through its upload session chunk-by-chunk, so a large stream is never buffered whole; `partSize` (rounded to a 4 MiB multiple) tunes the chunk size. - Other adapters already stream natively or only accept a fully-buffered body, so they ignore the option. Adapters that chunk natively round `partSize` to their own valid granularity — OneDrive to a 320-KiB multiple, GCS and Firebase to 256 KiB — and S3 enforces a 5 MiB minimum for every part except the last. ## Sizing the parts `partSize` and `concurrency` trade memory for throughput: up to `partSize × concurrency` bytes are buffered at once. The defaults (5 MiB × 4) keep that footprint small; raise them for fat pipes and large objects, lower them on memory-constrained workers. Because S3 caps an object at 10,000 parts, very large objects need a `partSize` big enough to fit under that ceiling. ## Progress and retries When [`onProgress`](/docs/api/onprogress) is set, the S3 family reports true byte-level progress across the whole multipart upload through the same `@aws-sdk/lib-storage` path. Multipart is still a single [`upload`](/docs/api/upload) call as far as [retries](/docs/retries), [timeouts](/docs/timeouts), and [cancellation](/docs/cancellations) are concerned — a failure retries the call, not an individual part, and an aborted [`signal`](/docs/cancellations) fails it fast. To pause and resume an upload — or resume one after a crash from a serializable token, retrying individual parts rather than the whole call — reach for a [resumable upload](/docs/resumable) instead. --- # Plugins overview Source: https://files-sdk.dev/docs/plugins A [`hook`](/docs/api/onaction) can only watch an operation go by. A **plugin** can change it. Plugins are an opt-in, ordered pipeline you pass to the constructor; each one wraps every operation on the instance and can transform the inputs, veto the call, observe the result - or add entirely new methods. ```ts lineNumbers import { createFiles, handlers } from "files-sdk"; import { s3 } from "files-sdk/s3"; const files = createFiles({ adapter: s3({ bucket: "uploads" }), plugins: [ { name: "uppercase", wrap: handlers({ upload: (op, next) => next({ ...op, body: (op.body as string).toUpperCase() }), }), }, ], }); await files.upload("a.txt", "hello"); // stored as "HELLO" ``` Reach for a plugin when you need to **change** behavior - envelope-encrypt bodies at rest, gate uploads through a virus scanner, meter bandwidth, mirror writes to a backup region. Keep [hooks](/docs/api/onaction) for lightweight, fire-and-forget observability; a plugin's `wrap` is the interceptable superset that can transform and veto where hooks only watch. ## Available plugins A set of plugins ships with the SDK, each as its own subpath export: - [`encryption()`](/docs/plugins/encryption) - envelope AES-256-GCM encryption at rest. - [`compression()`](/docs/plugins/compression) - gzip/deflate bodies via Compression Streams. - [`validation()`](/docs/plugins/validation) - fail-closed size, MIME, and key guards on writes. - [`versioning()`](/docs/plugins/versioning) - snapshot on overwrite/delete, with `versions()` and `restore()`. - [`contentType()`](/docs/plugins/content-type) - magic-byte sniffing that sets and guards `Content-Type`. - [`dedup()`](/docs/plugins/dedup) - content-addressed storage that shares identical blobs. - [`usage()`](/docs/plugins/usage) - meter operations and bytes up/down, optionally grouped. - [`tracing()`](/docs/plugins/tracing) - an OpenTelemetry span per operation. - [`softDelete()`](/docs/plugins/soft-delete) - a trash-prefix recycle bin with restore and purge. - [`audit()`](/docs/plugins/audit) - a durable who/what/when log of mutations. - [`cache()`](/docs/plugins/cache) - LRU/KV caching for `head()`, `url()`, and small downloads. - [`tiering()`](/docs/plugins/tiering) - route hot and cold keys to different backends. - [`failover()`](/docs/plugins/failover) - retry provider errors against replica backends. - [`signedUrlPolicy()`](/docs/plugins/signed-url-policy) - safe defaults for `url()` and `signedUploadUrl()`. - [`zip()`](/docs/plugins/zip) - bundle stored objects into ZIP archives and extract them back. The [plugin API reference](/docs/plugins/api) covers the `FilesPlugin` type, `handlers()`, and `createFiles`. ## The two capabilities A [`FilesPlugin`](/docs/plugins/api#filesplugin) is an object with a `name` and up to two optional capabilities. A plugin can use either or both. | Capability | What it does | Changes the instance type? | | --- | --- | --- | | `wrap` | Intercept every operation - transform the inputs, veto by throwing, or wrap the result. | No | | `extend` | Contribute new namespaced methods (e.g. `files.usage()`). | Yes - via [`createFiles`](#typing-extend-with-createfiles) | Because `wrap` doesn't touch the instance type, plugins that only wrap work with plain `new Files({ plugins })`. Only `extend` adds surface, and that's the one case [`createFiles`](/docs/plugins/api#createfiles) exists for. ## wrap: intercepting operations `wrap(op, next)` receives the current [operation](/docs/plugins/api#filesoperation) and a `next` function that continues inward. Call `next(op)` to run the rest of the pipeline (and ultimately the real call); pass a modified `op` to transform it, return a modified result to rewrite the output, or throw to veto. ```ts lineNumbers const plugin: FilesPlugin = { name: "logger", wrap: async (op, next) => { console.log("→", op.kind); const result = await next(op); // continue inward console.log("←", op.kind); return result; }, }; ``` Plugins compose as **ordered, nested layers**: `plugins[0]` is the outermost. With `[a, b]`, a write runs `a` → `b` → the real operation → `b` → `a`. A nice property falls out for free: because the innermost layer wraps the real read, **read-side inverses self-order**. Given `[validate, compress, encrypt]`, a download unwinds decrypt → decompress → validate automatically - you never hand-manage the symmetry. ### Where plugins sit Plugins run **inside** the [`onAction`](/docs/api/onaction) / [`onError`](/docs/api/onerror) hooks but **outside** [retries](/docs/retries) and [key prefixing](/docs/prefixes): - A `wrap` runs **once per logical operation**, not once per retry attempt. Encryption seals the body once; a retry resends the bytes the plugin already produced. - Plugins see **caller-facing keys** - never the internal [prefixed](/docs/prefixes) path. A key-rewriting plugin rewrites before prefixing. - The hooks still fire around the whole thing, so `onAction` reports the final, plugin-produced result. ### Bulk operations too `wrap` intercepts both single and bulk calls. The array forms of `upload`, `download`, `head`, `exists`, and `delete` fan out to **one operation per item**, each carrying `bulk: true` so a plugin can tell a batch element from a standalone call. This means an `encryption()` plugin encrypts `upload(key, body)` and every item of `upload([...])` - no silent plaintext footgun. > When any wrapping plugin is installed, `delete([...])` fans out to per-key deletes through each plugin instead of the adapter's native batch primitive, so every key is intercepted. Without plugins, the native batch path is unchanged. ### handlers(): per-verb wraps A raw `wrap` is right for cross-cutting plugins that touch every verb (logging, metering, tracing). For transforms that only care about one or two operations, [`handlers`](/docs/plugins/api#handlers) lets you write a per-verb map - each handler is typed to its own operation, and any verb you don't list passes straight through: ```ts lineNumbers import { handlers } from "files-sdk"; const encryption = (key: CryptoKey): FilesPlugin => ({ name: "encryption", wrap: handlers({ // typed as the upload op; `next` is typed to the upload result upload: (op, next) => seal(op.body, key).then(({ body, iv }) => next({ ...op, body, options: { ...op.options, metadata: { ...op.options?.metadata, iv } }, }) ), download: (op, next) => next(op).then((file) => unseal(file, key)), // head, exists, delete, copy, move, list, url, signedUploadUrl: untouched }), }); ``` :::note You don't have to write this plugin yourself - we ship [`encryption()`](/docs/plugins/encryption) out of the box. ::: ## extend: new methods `extend(files)` returns an object of methods grafted onto the instance. It runs once at construction against the **fully-wrapped** instance, so an extension method that calls back into `files.upload(...)` also passes through every plugin. ```ts lineNumbers const usage = (): FilesPlugin<{ usage: () => number }> => { let bytes = 0; return { name: "usage", wrap: async (op, next) => { const result = await next(op); if (op.kind === "upload") { bytes += result.size; } return result; }, extend: () => ({ usage: () => bytes }), }; }; ``` An extension key that collides with an existing `Files` method, a getter, or another plugin's extension **throws at construction** rather than silently shadowing it - so a plugin can never quietly break `upload` or make the instance un-`await`able. ### Typing extend with createFiles `new Files({ plugins })` works at runtime regardless, but a class constructor can't return `this & Ext` keyed off its arguments - so the extra methods won't show up on the **type**. [`createFiles`](/docs/plugins/api#createfiles) is the seam that surfaces them. It's identical to `new Files()` at runtime; it just carries the plugins' `extend` return types onto the result. ```ts lineNumbers import { createFiles } from "files-sdk"; const files = createFiles({ adapter: s3({ bucket: "uploads" }), plugins: [usage()], }); files.usage(); // ✅ typed ``` The built-in [`versioning()`](/docs/plugins/versioning) plugin is a real example of this - it uses `extend` to add `files.versions()` and `files.restore()`, so you construct it with `createFiles`. ## Things to keep in mind - **Order is the contract.** `[compress, encrypt]` compresses then encrypts (encrypted bytes don't compress); `[validate, scan, transform]` fails fast before doing work. Document each plugin's place even though reads self-order. - **Buffering transforms break streaming.** Encrypt / compress / scan need the whole body in memory, which is incompatible with unknown-length streams and [resumable uploads](/docs/resumable) (which re-read the original body). Gate those plugins the way the core already gates streams. - **Metadata-stashing needs adapter support.** A plugin that round-trips state through `options.metadata` (an encryption IV, say) only works on adapters that [support metadata](/docs/api/upload) - the same gate a direct `metadata` upload hits. - **`wrap` runs outside the timeout.** Per-attempt [timeouts](/docs/timeouts) bound the adapter call, not a slow plugin. The caller's `options` (including `signal`) ride on the operation, so a plugin can opt into cancellation itself. --- # API Source: https://files-sdk.dev/docs/plugins/api The types and helpers that make up the plugin system. See the [overview](/docs/plugins) for how `wrap`, `extend`, and the pipeline fit together. ## FilesPlugin The plugin object you pass to `plugins`. A `name` plus up to two optional capabilities - `wrap` and `extend`. ## FilesOperation The discriminated union handed to `wrap` - one variant per public verb, carrying the caller-facing inputs. The array forms of `upload` / `download` / `head` / `exists` / `delete` set `bulk: true` on each fanned-out item. ## handlers ```ts function handlers(map: PluginHandlers): FilesPlugin["wrap"]; ``` Builds a [`wrap`](/docs/plugins#wrap-intercepting-operations) from a per-verb map. Each handler is typed to its own operation and a same-kind `next`; verbs absent from the map pass through untouched. ## createFiles ```ts function createFiles( opts: FilesOptions & { plugins?: FilesPlugin[] } ): Files & ExtensionsOf; ``` Constructs a `Files` instance whose type includes every plugin's [`extend`](/docs/plugins#extend-new-methods) surface. Runtime-identical to `new Files(opts)` - it exists only to surface the added methods on the type. --- # audit Source: https://files-sdk.dev/docs/plugins/audit The built-in `audit()` plugin writes a structured **who / what / when** record of every mutation to a sink you provide. Unlike the fire-and-forget [`onAction`](/docs/api/onaction) hook, the sink is **awaited** - the operation doesn't resolve until the record is written, so you get ordering, back-pressure, and a write failure you can actually see. ```ts lineNumbers import { createFiles } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { audit } from "files-sdk/audit"; const files = createFiles({ adapter: s3({ bucket: "uploads" }), plugins: [ audit({ actor: () => currentUser()?.id, // read from your request context sink: (record) => db.insert("audit_log", record), // awaited }), ], }); await files.delete("notes.txt"); // → sink({ action: "delete", key: "notes.txt", actor: "u_42", // at: 1717…, durationMs: 12, status: "success" }) ``` ## The record Each audited operation produces one [`AuditRecord`](#the-record): | Field | Always? | What it is | | --- | --- | --- | | `action` | yes | The verb (`upload`, `delete`, `copy`, `move`, `signedUploadUrl`, …). | | `key` | — | Caller-facing key, for every verb except `copy` / `move` / `list`. | | `from`, `to` | — | Source / destination, for `copy` and `move`. | | `actor` | — | Who performed it, from the [`actor`](#options) resolver. | | `at` | yes | When the operation started (ms since epoch). | | `durationMs` | yes | Wall-clock duration of the logical operation. | | `status` | yes | `"success"` or `"error"`. | | `size` | — | Stored byte size, on a successful `upload`. | | `bulk` | — | `true` when the record is one item of a bulk (`[...]`) call. | | `error` | — | `{ code, message }`, on `status: "error"`. | Keys are always the caller-facing ones, never the internal [prefixed](/docs/prefixes) path. ## Awaited, not fire-and-forget A [`hook`](/docs/api/onaction) is called but never awaited; a hook that's slow or throws can't affect the operation. `audit()` is the opposite by design: - **The operation waits for the sink.** `await files.delete(...)` doesn't resolve until your sink resolves, so records land in order and a slow sink applies back-pressure. - **On success, a rejecting sink fails the call.** The mutation already happened but wasn't recorded - rather than silently drop the entry, the call rejects so you decide what to do (retry, alert). Fail closed. - **On failure, the operation's error always wins.** When the operation itself throws, the record is written best-effort; a sink that _also_ rejects while recording the failure is suppressed so it can never mask why the call failed. If you'd rather audit best-effort, `catch` inside your own sink - then it never rejects and never fails a call. ## Options | Option | Default | What it does | | --- | --- | --- | | `sink` | _(required)_ | `(record) => void \| Promise`, **awaited**. Where each record is written. | | `actor` | — | `(op) => string \| undefined`. Resolve **who** - typically read synchronously from an `AsyncLocalStorage`. | | `events` | `"writes"` | Which verbs to record: `"writes"`, `"all"` (reads included), or an explicit list like `["upload", "delete"]`. | | `clock` | `Date.now` | The clock used for `at` and `durationMs`. Inject a fake for deterministic tests or a trusted time source. | ### Which operations are recorded By default `audit()` records the **mutating** verbs - `upload`, `delete`, `copy`, `move`, and `signedUploadUrl` (minting an upload capability is a write worth logging). Pass `events: "all"` to also record reads (`download`, `head`, `exists`, `list`, `url`), or an explicit list to record exactly the verbs you name: ```ts audit({ sink, events: ["upload", "delete"] }); // only these two ``` ### Attributing the actor The `actor` resolver receives the full [operation](/docs/plugins/api#filesoperation), so you can read it from request context or derive it from the key: ```ts lineNumbers audit({ sink, actor: (op) => { const user = requestContext.get()?.user; // e.g. an AsyncLocalStorage return user?.id; }, }); ``` Return `undefined` to leave `actor` off a record; omit the option to never set one. ## Ordering Put `audit()` **first** (outermost) so it records the caller's **logical intent**. A `delete` that an inner [`softDelete()`](/docs/plugins/soft-delete) turns into a `move` is still audited as the `delete` the caller asked for; a body an inner [`encryption()`](/docs/plugins/encryption) seals is still recorded at its logical size. ```ts plugins: [audit({ sink }), softDelete(), encryption(key)]; ``` Placed **last** (innermost) it instead records the physical operations the pipeline above expands into - the `move` soft-delete actually issued, the encrypted byte size. Both are valid; pick the layer whose history you want. ## Things to keep in mind - **One record per logical operation.** Plugins run [outside retries](/docs/retries), so a call that retries three times is still **one** record - its `durationMs` spans the retries. - **Bulk fans out to one record per item.** `upload([...])` / `delete([...])` record each key individually, flagged `bulk: true`, with per-item success/error - exactly the granularity an audit log wants. - **Body-transparent, works on any adapter.** It never buffers, transforms, or reads the body (`size` comes from the upload result's declared metadata, not the bytes), so streaming, range downloads, `url()`, and `signedUploadUrl()` all keep working. It writes no object metadata and has no native dependencies. - **Not a security boundary.** It records operations made **through the instance**; a direct presigned `PUT` to the bucket bypasses it. Pair it with [`signedUrlPolicy()`](/docs/plugins/signed-url-policy) to keep the URLs you mint tight. - **`wrap`-only.** It adds no methods, so plain `new Files({ plugins })` works - though `createFiles` is fine too and keeps you consistent with the [extend](/docs/plugins/api#createfiles)-based plugins. --- # cache Source: https://files-sdk.dev/docs/plugins/cache The built-in `cache()` plugin puts an LRU (or your own KV) in front of the cheap read verbs. A repeat [`head()`](/docs/api/head) or [`url()`](/docs/api/url) - and, opt-in, a small [`download()`](/docs/api/download) - for an unchanged key is served from memory instead of round-tripping to the provider. Any write **through the instance** ([`upload`](/docs/api/upload), [`delete`](/docs/api/delete), [`copy`](/docs/api/copy), [`move`](/docs/api/move)) invalidates the affected key, so the next read re-fetches. It writes **no object metadata** and has **no native dependencies**, so it works on any adapter. Like the other plugins it runs **outside** [retries](/docs/retries) - a cache hit skips the retry loop entirely. ```ts lineNumbers import { createFiles } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { cache } from "files-sdk/cache"; const files = createFiles({ adapter: s3({ bucket: "uploads" }), plugins: [cache()], }); await files.head("a.png"); // miss → provider await files.head("a.png"); // hit → memory await files.upload("a.png", body); // invalidates "a.png" await files.head("a.png"); // miss → provider again ``` :::note `invalidateCache()`, `cacheStats()`, and `resetCacheStats()` are contributed by the plugin's `extend`, so they only appear on the **type** when you construct with [`createFiles`](/docs/plugins/api#createfiles) (identical to `new Files()` at runtime). ::: ## What gets cached By default `cache()` caches the two cheap, body-free verbs - `head` and `url`. Pass `operations` to change the set: ```ts cache({ operations: ["head", "url", "download"] }); ``` - **`head`** caches the metadata only. A hit returns a [`StoredFile`](/docs/api/stored-file) whose body still **lazy-fetches on access** - exactly the contract an uncached `head` has - so nothing is buffered up front. - **`url`** caches the returned string, keyed per url-options signature (so a plain `url()` and a `url({ expiresIn })` cache apart). Each entry is **additionally capped at its own `expiresIn`**, so a presigned URL is never handed out past its signature. - **`download`** is **off by default**. With `"download"` enabled, only **known-length bodies at or under `maxBytes`** (default 1 MiB) are buffered and cached; anything larger - or of unknown length - streams straight through **uncached**, so streaming and [range downloads](/docs/api/download) keep working. A cached small body is re-served as a fresh, re-readable `StoredFile`. ```ts cache({ operations: ["head", "url", "download"], maxBytes: 256 * 1024, // only cache downloads ≤ 256 KiB }); ``` ## Invalidation Caching is only safe because writes evict. Every mutation **through the instance** drops the affected key's entire record (all of its cached verbs) once the write lands: | Write | Invalidates | | ---------------- | ------------------- | | `upload(key)` | `key` | | `delete(key)` | `key` | | `copy(from, to)` | `to` (destination) | | `move(from, to)` | `from` **and** `to` | Invalidation is keyed by the **caller-facing** key - never the internal [prefixed](/docs/prefixes) path - so it lines up with the keys reads are cached under. ### Writes the cache can't see A change the plugin never observes won't invalidate: an upload made through a [presigned URL](/docs/api/signed-upload-url), or a mutation straight against the provider. Treat the cache as **eventually-consistent** and evict by hand when that happens: ```ts lineNumbers await files.invalidateCache("a.png"); // drop one key await files.invalidateCache(); // drop everything ``` ## Stats `cacheStats()` returns a fresh `{ hits, misses }` snapshot for tuning your TTL and entry budget; `resetCacheStats()` starts a new window: ```ts lineNumbers files.cacheStats(); // { hits: 41, misses: 9 } files.resetCacheStats(); ``` ## The store By default the cache is a **bounded in-memory LRU** keyed by object key, holding `maxEntries` keys (default 1000) before evicting the least-recently-used. Each key's record bundles every cached verb together, which is what makes invalidation a single delete. :::warning Worst-case memory is roughly `maxEntries * maxBytes` once `download` caching is on. The default set (`head` + `url`) stores only small metadata, so the entry count is the only thing to size. ::: Pass your own `store` to share the cache across instances or processes - e.g. a Redis-backed `CacheStore` that serializes each record: ```ts lineNumbers const files = createFiles({ adapter: s3({ bucket: "uploads" }), plugins: [cache({ store: myRedisStore })], }); ``` A `CacheStore` is four methods - `get`, `set`, `delete`, `clear` - each of which may be sync or async. A distributed store has an inherent read-modify-write race when two different verbs for the same key are first cached at the exact same instant; it's harmless - it just costs a re-fetch next time. ## TTL Every entry honors a `ttl` (default `60_000` ms). Set `0` to disable time-based expiry entirely (entries then live until evicted or invalidated): ```ts cache({ ttl: 30_000 }); ``` For `url`, keep `ttl` **comfortably below your signed-URL expiry**. The per-entry `expiresIn` cap guarantees you never serve a dead URL, but a short `ttl` keeps the URLs you hand out fresh with plenty of life left. ## Ordering Place `cache()` **first** (outermost) so a hit short-circuits before the rest of the pipeline does any work: ```ts plugins: [cache(), encryption(key)]; ``` Put it **after** a body-transforming plugin only if you deliberately want to cache the transformed bytes (e.g. caching post-`compression()` output). ## Things to keep in mind - **A cache is eventually-consistent.** Out-of-band writes (presigned uploads, direct provider changes) won't invalidate - call `invalidateCache()`, and keep the `ttl` honest. - **`download` caching buffers bodies.** It's gated to small, known-length objects for exactly this reason; large and unknown-length downloads always stream through untouched. - **Bound your memory.** With `download` enabled, size `maxEntries` and `maxBytes` together - the product is your ceiling. - **It's per-instance by default.** The in-memory store lives with the `Files` instance. Reach for a shared `store` to cache across processes. --- # compression Source: https://files-sdk.dev/docs/plugins/compression The built-in `compression()` plugin compresses every body **at rest** and decompresses it on the way back out - transparently, for single and [bulk](/docs/bulk) calls alike. It's a textbook [wrap plugin](/docs/plugins#wrap-intercepting-operations): it transforms the body on `upload`, reverses it on `download`, and round-trips its bookkeeping through the object's `metadata`. It uses only the [Compression Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Compression_Streams_API), so it has **no native dependencies** and runs anywhere the SDK does - Node, Bun, Deno, edge runtimes, and the browser. It works on any adapter that [supports metadata](/docs/api/upload). ```ts lineNumbers import { createFiles } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { compression } from "files-sdk/compression"; const files = createFiles({ adapter: s3({ bucket: "uploads" }), plugins: [compression()], }); await files.upload("notes.txt", "a".repeat(10_000)); // stored gzipped await (await files.download("notes.txt")).text(); // the original 10k string ``` ## How it works 1. On `upload`, the body is compressed with the configured algorithm (gzip by default). 2. If the compressed form is **smaller**, it's stored, and the algorithm plus the original byte length are recorded in the object's `metadata`. If it **wouldn't shrink** - already-compressed inputs like JPEG, ZIP, or encrypted blobs - the original bytes are stored verbatim and marked `identity`, so the plugin never inflates your storage. 3. On `download`, the recorded algorithm decompresses the body back to the original bytes and hands you a normal [`StoredFile`](/docs/api/stored-file) - with its `size` reporting the **original** length and the internal metadata fields hidden. Because the algorithm is stored per object, reads always decompress with the right one. Changing the `format` option later never breaks objects written under the old format. ## Choosing a format `compression()` defaults to gzip. Pass `format` to pick another: ```ts lineNumbers import { compression } from "files-sdk/compression"; compression(); // gzip (default) compression({ format: "deflate" }); // zlib-wrapped deflate compression({ format: "deflate-raw" }); // bare deflate, no framing ``` :::note Brotli is intentionally not offered. It isn't part of the Compression Streams standard, so supporting it would mean a native dependency and break the plugin's run-anywhere promise. The three formats above are the ones every platform ships. ::: ## Ordering Compression should run **before** encryption, so it sees plaintext - encrypted bytes are effectively random and don't compress. Put it **earlier** in the array: ```ts plugins: [compression(), encryption(key)]; ``` Because reads unwind the [onion](/docs/plugins#wrap-intercepting-operations) in reverse, a download automatically runs decrypt → decompress. You never hand-manage the symmetry. ## Things to keep in mind :::warning The plugin **buffers the entire body in memory** to compare the compressed and original sizes. It's unsuitable for unknown-length streams and [resumable uploads](/docs/resumable), which re-read the original body. ::: - **Range downloads throw.** A byte range of the original maps to no fixed slice of the compressed bytes, so a [`download`](/docs/api/download) with a `range` is refused. - **`url()` and `signedUploadUrl()` throw.** A presigned GET hands out compressed bytes with no `Content-Encoding`, so a client receives them as-is and can't read them; a presigned PUT would silently bypass compression and store uncompressed bytes. Both fail closed - upload and download through the instance instead. - **`copy` and `move` just work.** They operate on the stored bytes server-side, and the algorithm marker rides along in the object's metadata, so the copy still decompresses. - **Mixed buckets are safe.** On read, objects without this plugin's marker (pre-existing data, or anything written elsewhere) pass straight through unchanged, so you can enable it on a bucket that already holds plain objects. - **It needs metadata support.** The algorithm and original size are stored as object metadata, so the adapter must [support metadata](/docs/api/upload) - an `upload` to one that doesn't throws before any bytes move. ## What it stores in metadata Each object this plugin writes carries two `fscmp_`-prefixed metadata fields: `fscmp_alg` (the algorithm, or `identity` when stored verbatim) and `fscmp_size` (the original, uncompressed byte length). They're stripped from the `StoredFile` you get back on `download`, `head`, and `list`, so your own metadata is all you see. --- # contentType Source: https://files-sdk.dev/docs/plugins/content-type The built-in `contentType()` plugin sets an upload's `Content-Type` from what the bytes **actually are**, not what the client said they were. On `upload` it magic-byte-sniffs the body and either corrects the stored type to match (the default) or rejects a mismatch — so a `.png` whose bytes are really HTML or SVG can't be stored under an image type and later served inline. It's the [wrap plugin](/docs/plugins#wrap-intercepting-operations) counterpart to [`validation()`](/docs/plugins/validation): validation vets the _declared_ type, `contentType()` verifies the _real_ one. ```ts lineNumbers import { createFiles } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { contentType } from "files-sdk/content-type"; const files = createFiles({ adapter: s3({ bucket: "uploads" }), plugins: [contentType({ onMismatch: "reject" })], }); await files.upload("avatar.png", pngBytes); // ok — bytes are a PNG await files.upload("avatar.png", htmlBytes); // throws — bytes are HTML ``` ## What it recognizes The sniffer is deliberately scoped to where a verdict is unambiguous and useful: - **Images** — PNG, JPEG, GIF, BMP, WebP, TIFF, and ICO, by their magic bytes. - **PDF** — the `%PDF` signature. - **HTML, SVG, and XML** — the security-relevant part. These are text, so they have no fixed magic bytes; a leading text scan (skipping a BOM and whitespace) catches ``, ``, ` ``` **Svelte** ```svelte {#if $isUploading}{/if} ``` The browser never holds storage credentials. Calls go to **your** endpoint, which runs the SDK against whatever adapter you configured (S3, R2, GCS, Vercel Blob, …) and streams or signs as needed. ## The two halves | | Package | What it is | | --- | --- | --- | | **Client** | [`files-sdk/react`](/docs/ui/client/react), [`files-sdk/vue`](/docs/ui/client/vue), [`files-sdk/svelte`](/docs/ui/client/svelte) | A `useFiles` binding for your framework — every verb (imperative, with upload progress) plus optional reactive `useList` / `useFile` / `useSearch`. | | **Server** | `files-sdk/api` + a framework adapter ([Next.js](/docs/ui/server/next), [Hono](/docs/ui/server/hono), [Express](/docs/ui/server/express)) | A [gateway](/docs/ui/server/gateway) you mount at `/api/files` that exposes the `Files` API over HTTP, gated by an [`authorize`](/docs/ui/server/authorization) hook. | The same gateway backs all three bindings. A framework-agnostic core, `createFilesClient` from `files-sdk/client`, sits under them for non-framework (Node, worker) callers. :::warning The gateway proxies `download`, `list`, `delete`, and `move` to the browser — it is effectively a remote storage console. It is **deny-by-default**: nothing is exposed until you configure [`authorize`](/docs/ui/server/authorization) or `operations`. Read that page before shipping. ::: ## Quick start **1. Mount the gateway.** Expose the `Files` API at an endpoint and scope every key to the signed-in user. This example uses Next.js; [Hono](/docs/ui/server/hono) and [Express](/docs/ui/server/express) are a one-liner too: ```ts title="app/api/files/route.ts" lineNumbers import { createFiles } from "files-sdk"; import { s3 } from "files-sdk/s3"; import { createFilesRouter } from "files-sdk/api"; import { createRouteHandler } from "files-sdk/next"; const router = createFilesRouter({ files: createFiles({ adapter: s3({ bucket: "uploads" }) }), allowedOrigins: ["https://app.example.com"], authorize: async ({ req }) => { const session = await auth(req); // throw → 401 return { keyPrefix: `users/${session.id}/`, maxExpiresIn: 300 }; }, }); export const { GET, POST, PUT } = createRouteHandler(router); ``` **2. Use your binding.** Drop the component above into your app — that's the whole loop: uploads stream directly to storage (or proxy through your endpoint for adapters that can't presign), and reads run against your gateway. Keys the client sends are relative to the authorized prefix, so the browser can never address another user's files. For a file browser, the reactive reads (`useList` / `useFile` / `useSearch`) wrap the read verbs with `data` / `isLoading` / `refetch`. Next: pick your binding — [React](/docs/ui/client/react), [Vue](/docs/ui/client/vue), or [Svelte](/docs/ui/client/svelte) — then set up the [gateway](/docs/ui/server/gateway) and its [authorization](/docs/ui/server/authorization). ## Server adapters The gateway mounts on any of these with a one-line adapter: [Next.js](/docs/ui/server/next), [Hono](/docs/ui/server/hono), [Express](/docs/ui/server/express), [Fastify](/docs/ui/server/fastify), [Koa](/docs/ui/server/koa), [NestJS](/docs/ui/server/nestjs), [Elysia](/docs/ui/server/elysia), [Nitro](/docs/ui/server/nitro), [SvelteKit](/docs/ui/server/sveltekit), [Astro](/docs/ui/server/astro), [TanStack Start](/docs/ui/server/tanstack-start), [Bun](/docs/ui/server/bun), and [Deno](/docs/ui/server/deno). ## Prebuilt components On top of the React hook there's a [shadcn](https://ui.shadcn.com) registry of SDK-wired components you can `npx shadcn add` and own: - [File browser](/docs/ui/components/file-browser) - list, navigate, and manage a prefix. - [File list](/docs/ui/components/file-list) - a lightweight table of stored files. - [Dropzone](/docs/ui/components/dropzone) - drag-and-drop uploads. - [Upload progress](/docs/ui/components/upload-progress) - per-file progress UI. - [Multipart uploader](/docs/ui/components/multipart-uploader) - large-file uploads in parts. - [File preview](/docs/ui/components/file-preview) - inline previews for stored objects. - [File search](/docs/ui/components/file-search) - key search over the gateway. - [File actions](/docs/ui/components/file-actions) - a per-file dropdown of verbs. - [Share dialog](/docs/ui/components/share-dialog) - mint signed URLs with expiry presets. - [Capabilities badges](/docs/ui/components/capabilities-badges) - show what the backend supports. - [Version history](/docs/ui/components/version-history) - browse and restore versions. - [Trash bin](/docs/ui/components/trash-bin) - soft-deleted files with restore and purge. Each page has its own install command, or grab all of them at once with the `all` bundle: --- # React Source: https://files-sdk.dev/docs/ui/client/react `files-sdk/react` brings the full Files API to the browser as a React hook. `useFiles` returns one method per `Files` verb against the [gateway](/docs/ui/server/gateway) you mounted, plus ambient upload/error state. Keys are plain strings (relative to your [authorized prefix](/docs/ui/server/authorization)). ```tsx lineNumbers const files = useFiles({ endpoint: "/api/files", // default "/api/files" headers: () => ({ authorization: token }), // sent on every call (static or lazy) }); ``` ## The verbs ```ts lineNumbers // Reads await files.head("docs/a.txt"); // → StoredFile (lazy body) await files.exists("docs/a.txt"); // → boolean await files.list({ prefix: "docs/" }); // → { items, prefixes?, cursor? } await files.url("img.png"); // → string (for ) await files.capabilities(); // → AdapterCapabilities // Writes await files.upload(file); // → { key, size, type, etag } await files.delete("old.txt"); await files.copy("a.txt", "b.txt"); await files.move("a.txt", "c.txt"); // Streaming iterators for await (const f of files.listAll()) { /* every object */ } for await (const f of files.search("docs/*.pdf")) { /* glob/regex */ } ``` Every verb mirrors the SDK's signature, including the bulk array forms — `files.delete([a, b])`, `files.head([a, b])`, `files.download([a, b])` — which return the same partial-result shapes (`{ deleted, errors? }`, …) and never throw on a single bad key. ## Uploading `upload` has three forms, resolved by argument shape: ```ts lineNumbers // Keyless - the server mints the key (the common case). const { key } = await files.upload(file); // Explicit key - you choose it (subject to the authorized prefix). await files.upload("avatars/me.png", file, { contentType: "image/png" }); // Bulk - pooled at the configured concurrency. await files.upload([ { key: "a.txt", body: "one" }, { key: "b.txt", body: "two" }, ]); ``` Track progress per call, or read the hook's ambient state: ```tsx lineNumbers await files.upload(file, { onProgress: (p) => console.log(p.fraction), // 0 → 1 }); // or, ambient across any in-flight upload this hook started: files.isUploading; // boolean files.progress; // { loaded, total, fraction } files.uploads; // per-file FileUploadState[] ``` ## Downloading vs linking Two tools, two jobs: ```ts lineNumbers // download(key) → StoredFile: the bytes flow through your gateway. Works on // every adapter. Use it when you need the data programmatically. const file = await files.download("report.pdf"); const blob = await file.blob(); // lazy - only fetched on access const text = await file.text(); const chunk = await files.download("video.mp4", { range: { start: 0, end: 1023 }, }); // url(key) → string: a direct link. Use it for / /