usage
Meter storage, bandwidth, and operation counts across a Files instance, and read the running totals back with files.usage(). Counts the bytes you actually read out of a download lazily, optionally bucketed per tenant or prefix. No native dependencies; works on any adapter.
The built-in usage() plugin tallies every operation on a Files instance and surfaces the running totals as files.usage(). Each call counts as one operation, upload adds its size to bytesUp, and download / head wrap the returned body so the bytes you actually read add to bytesDown — the lazy, stream-level accounting a fire-and-forget hook can't do.
import { createFiles } from "files-sdk";
import { s3 } from "files-sdk/s3";
import { usage } from "files-sdk/usage";
const files = createFiles({
adapter: s3({ bucket: "uploads" }),
plugins: [usage()],
});
await files.upload("a.txt", "hello");
await (await files.download("a.txt")).text();
files.usage();
// { operations: 2, bytesUp: 5, bytesDown: 5, operationsByKind: { upload: 1, download: 1, … } }Because usage() adds files.usage(), construct the instance with createFiles so the method shows up on the type — the same as versioning().
How it works
uploadadds its result's reported size tobytesUp. Nothing is buffered — the body still streams to the adapter.download/headreturn a body wrapped so the bytes are counted as they flow:stream()is metered chunk-by-chunk (an aborted read counts only what was consumed), andarrayBuffer()/blob()/text()count the body's length the first time one resolves. An unread body costs nothing — and a body read twice counts once.- Every verb increments
operationsand itsoperationsByKindentry. A call that throws (a missing key, avalidation()veto) isn't counted, and because plugins run outside retries a call counts once no matter how many attempts it takes.
Bulk upload([...]) / download([...]) count per item.
Reading the totals
extend adds three methods:
| Method | Returns |
|---|---|
usage() | A UsageStats snapshot aggregated across every group. |
usageByGroup() | A Record<string, UsageStats> keyed by the group label. |
resetUsage() | Zeroes every counter, starting a fresh accounting window. |
Each snapshot is a fresh copy, so mutating it never touches the running totals.
Grouping by tenant or prefix
Pass group to bucket usage by a label derived from each operation — a tenant id, a key prefix, anything. It receives the full operation, so branch on op.kind to reach op.key, op.from / op.to, etc.
const files = createFiles({
adapter: s3({ bucket: "uploads" }),
plugins: [
usage({ group: (op) => ("key" in op ? op.key.split("/")[0] : "shared") }),
],
});
await files.upload("acme/logo.png", bytes);
await files.upload("globex/logo.png", bytes);
files.usageByGroup(); // { acme: { bytesUp: … }, globex: { bytesUp: … } }
files.usage(); // the two buckets summedOrdering
Put usage() first (outermost) to meter what your application sees: a later body-transforming plugin like compression() or encryption() reports the logical size up the chain, and the internal sub-operations a plugin like dedup() issues stay below it, unmetered.
plugins: [usage(), compression(), encryption(key)];Placed last (innermost) it instead meters the bytes-on-the-wire to the provider and the provider operations those plugins expand into. Both are valid — pick the layer whose numbers you want to bill against.
Things to keep in mind
bytesUpcomes from each upload's reported result size. The rare adapter that doesn't report a size contributes0tobytesUpfor that upload.bytesDownis only the body you read. Reading metadata withhead()and never touching the body costs nothing; the moment you call a body accessor, the bytes are tallied.- It counts logical operations, not provider requests. Retries happen below the plugin, so a call that retries three times still counts once.
- No metadata, no native deps. Unlike the transforming plugins it never reads or rewrites the body, so streaming, range downloads,
url(), andsignedUploadUrl()all keep working on any adapter.
tracing
Open an OpenTelemetry span around every operation on a Files instance - one span per call, named files.<verb>, with the key, size, and outcome as attributes and errors recorded with an ERROR status. Spans nest under your active request span. Uses the optional @opentelemetry/api peer dependency.
validation
A fail-closed guard that vets every upload before any bytes move - enforce a max/min size, an allowed-MIME-type list, and a key-naming rule. Provider-agnostic, no native dependencies, no metadata.