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 summed
Ordering
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.