Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Filesystem

Local filesystem - the dev/test adapter. Uses node:fs/promises with a sidecar .meta.json per file. Not for production.

Installation

This adapter has no extra peer dependencies - the runtime (Node or Bun) provides everything it needs.

npm install files-sdk
pnpm add files-sdk
yarn add files-sdk
bun add files-sdk
nub add files-sdk
aube add 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.

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

PropType
rootstring

Absolute or relative directory the adapter manages. Created on first upload if it doesn't exist. All operations are scoped to this root — keys that resolve outside it (e.g. `../etc/passwd`) throw `Invalid`.

Typestring
urlBaseUrl?string

Optional URL prefix for `url()`. When set, `url(key)` returns `${urlBaseUrl}/${key}` — useful when a dev server (Next.js `/public` mount, `serve-static`, etc.) is exposing the same root. When unset, `url()` returns a `file://` URL — appropriate for CLIs/tests, not for browsers.

Typestring

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.

The separate body and sidecar layout cannot provide a cross-process atomic ETag compare-and-set boundary. The adapter therefore advertises no conditional operations and rejects them before filesystem I/O; it does not emulate CAS with a read followed by a rename.

Directories aren’t objects. head, exists, download, copy, and move on a key that names a directory report NotFound, the way a cloud store treats a prefix with no object, and delete on one is a no-op. A key that ends in / (or in a . or .. segment) names a folder outright, so every method rejects it with Invalid instead of resolving it onto the file it would otherwise alias (dir/ → dir). A write that would need a directory to be a file (EISDIR, ENOTEMPTY) fails with Conflict. If root itself isn’t a directory, delete fails with Invalid instead of looking like an empty store.

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 <urlBaseUrl>/<key> 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. Only urlBaseUrl makes capabilities.publicUrl true: a file:// link isn’t a public URL, so without it the gateway proxies downloads instead of redirecting.
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.
resumable uploads ✅ upload({ control }) writes chunks to a <key>.fls-part partial next to the body (hidden from list(), reserved for writes) and renames it into place on completion. The partial is never opened through a symlink: a link at that path is replaced when the upload begins, and a chunk or completion that finds one fails with Conflict.
conditional operations ❌ Body and metadata are separate files, so there is no honest cross-process native CAS primitive.

Last updated on

Was this page helpful?