Write once. Store anywhere.
A unified SDK for S3, R2, GCS, Azure, and every other object or blob store. One small API, web standards, and an escape hatch when you need the native client.
and 31 more — see every adapter →
Live snippet
The exact same code. Any backend.
Switch the adapter, keep every call site. Here's the same upload, download, head, list, and delete sequence across five providers.
import { Files } from "files-sdk";
import { s3 } from "files-sdk/s3";
const files = new Files({
adapter: s3({ bucket: "uploads", region: "us-east-1" }),
});
await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");import { Files } from "files-sdk";
import { r2 } from "files-sdk/r2";
const files = new Files({
adapter: r2({ bucket: "uploads", accountId: "..." }),
});
await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");import { Files } from "files-sdk";
import { vercelBlob } from "files-sdk/vercel-blob";
const files = new Files({
adapter: vercelBlob(),
});
await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");import { Files } from "files-sdk";
import { netlifyBlobs } from "files-sdk/netlify-blobs";
const files = new Files({
adapter: netlifyBlobs({ name: "uploads" }),
});
await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");import { Files } from "files-sdk";
import { minio } from "files-sdk/minio";
const files = new Files({
adapter: minio({ bucket: "uploads", endpoint: "http://localhost:9000" }),
});
await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");Capabilities
Everything you do with files.
A complete set of operations, 40+ adapters behind one interface, ready-made AI tools, and a CLI — plus multipart, live progress, byte ranges, and lifecycle hooks. The same one-line ergonomics on every backend.
Every operation, one interface
upload, download, head, exists, copy, move, list, delete — the same calls on every adapter. Hand any of them an array to batch with bounded concurrency, or walk a listing as a plain async iterable.
await files.upload("report.pdf", body);
const file = await files.download("report.pdf");
await files.copy("a.png", "b.png");
await files.move("tmp/x.png", "img/x.png");
// walk every page as a plain async iterable
for await (const f of files.listAll({ prefix: "img/" })) {
console.log(f.key, f.size);
}
// pass an array to batch with bounded concurrency
await files.delete(["old/1.png", "old/2.png"]);Find files by name, glob, or regex
files.search() finds objects by key — a glob by default, or a regex, substring, or exact match. Matches stream back as an async iterable, and a glob's prefix scopes the walk to skip the rest of the bucket.
// glob by default — ** spans folders
for await (const file of files.search("invoices/**/*.pdf")) {
console.log(file.key, file.size);
}
// or a regex, substring, or exact match
const errors = files.search(/error|panic/, { prefix: "logs/" });
// collect into an array, capped
const recent = await Array.fromAsync(
files.search("*.png", { maxResults: 20 }),
);File tools for your agents
Generate ready-made file tools for the Vercel AI SDK, OpenAI Agents, or Claude and MCP. Hand your agent list, read, upload, and delete — with read-only mode and per-tool approval gates built in.
import { createFileTools } from "files-sdk/ai-sdk";
import { generateText } from "ai";
const tools = createFileTools({
files,
requireApproval: { deleteFile: true },
});
await generateText({
model,
tools, // listFiles, downloadFile, uploadFile, …
prompt: "Archive last month's invoices to /archive.",
});Archive last month's invoices to /archive.
The same SDK, from your shell
Every method is also a command. Stream with stdin and stdout, switch backends with --provider, and get JSON by default — handy for scripts, CI, and one-off ops.
# upload from a pipe, switch providers with a flag
cat q1.pdf | files --provider s3 upload q1.pdf --stdin
# list as JSON — the default
files --provider r2 list --prefix reports/
# stream a download straight to disk
files --provider gcs download q1.pdf --stdout > out.pdfMultipart, in parallel
Hand off a large body or an unbounded stream and files-sdk splits it into parts, uploading them with bounded concurrency. Tune the part size and parallelism, or just say multipart: true.
// split a large body into parallel parts
await files.upload("db.tar", stream, {
multipart: true,
});
// or tune the part size & concurrency
await files.upload("db.tar", stream, {
multipart: {
partSize: 16 * 1024 * 1024,
concurrency: 4,
},
});Live upload progress
Pass one callback and get byte-level progress for every file — buffered or streamed, single or bulk. Drive a progress bar per key without ever touching the transport.
const items = [
{ key: "hero.jpg", body: hero },
{ key: "promo.mp4", body: promo },
// …two more
];
await files.upload(items, {
onProgress({ key, loaded, total }) {
bars.get(key)?.set(loaded / total);
},
});Byte-range downloads
Ask for exactly the bytes you need. Ranged reads map straight to HTTP 206, so you can seek video, resume a download, or read a file header without pulling the whole object.
// download just a byte range — end is inclusive
const head = await files.download("video.mp4", {
range: { start: 0, end: 1023 },
});
// stream the next chunk as the player seeks
const chunk = await files.download("video.mp4", {
as: "stream",
range: { start: offset, end: offset + CHUNK },
});Lifecycle hooks
Wire metrics, logging, and error reporting once at the constructor. onAction, onRetry, and onError fire for every operation across every adapter — fire-and-forget, never in your way.
const files = new Files({
adapter: s3({ bucket: "uploads" }),
hooks: {
onAction({ type, status, durationMs }) {
metrics.timing("files." + type, durationMs);
},
onRetry({ type, attempt }) {
log.warn("retry " + attempt + ": " + type);
},
onError({ error }) {
if (!error.aborted) Sentry.captureException(error);
},
},
});Mirror across backends
sync() reconciles one backend onto another — uploading only what changed, skipping what's identical, and pruning what's gone. Back up or migrate in a line, and dry-run the plan first.
const live = new Files({ adapter: s3({ bucket: "live" }) });
const backup = new Files({
adapter: r2({
bucket: "backup",
accountId,
accessKeyId,
secretAccessKey,
}),
});
// back up S3 to R2 — only the delta moves
const {
uploaded,
skipped,
deleted,
} = await sync(live, backup, {
prune: true,
compare: "size",
});Two steps
Your first upload in under a minute.
Install files-sdk and your provider's native client, then construct one Files instance and start calling it.
1. Install
npm install files-sdkpnpm add files-sdkbun add files-sdkyarn add files-sdkEach adapter's native SDK is an optional peer dependency — install only the ones you actually use.
2. Make your first call
import { Files } from "files-sdk";
import { s3 } from "files-sdk/s3";
const files = new Files({
adapter: s3({ bucket: "uploads", region: "us-east-1" }),
});
await files.upload("hello.txt", "world");Construct a Files instance with your provider's adapter, then call upload, download, list, delete on it.
Ship the storage layer once.
Open source, MIT licensed, built around web standards. Drop in an adapter and forget the difference.