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

  • upload adds its result's reported size to bytesUp. Nothing is buffered — the body still streams to the adapter.
  • download / head return 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), and arrayBuffer() / 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 operations and its operationsByKind entry. A call that throws (a missing key, a validation() 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:

MethodReturns
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

  • bytesUp comes from each upload's reported result size. The rare adapter that doesn't report a size contributes 0 to bytesUp for that upload.
  • bytesDown is only the body you read. Reading metadata with head() 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(), and signedUploadUrl() all keep working on any adapter.

On this page