compression
Transparently compress object bodies at rest with gzip, deflate, or deflate-raw. The original size and algorithm ride along in metadata, and reads decompress automatically - provider-agnostic, no native dependencies.
The built-in compression() plugin compresses every body at rest and decompresses it on the way back out - transparently, for single and bulk calls alike. It's a textbook wrap plugin: it transforms the body on upload, reverses it on download, and round-trips its bookkeeping through the object's metadata.
It uses only the Compression Streams API, so it has no native dependencies and runs anywhere the SDK does - Node, Bun, Deno, edge runtimes, and the browser. It works on any adapter that supports metadata.
import { createFiles } from "files-sdk";
import { s3 } from "files-sdk/s3";
import { compression } from "files-sdk/compression";
const files = createFiles({
adapter: s3({ bucket: "uploads" }),
plugins: [compression()],
});
await files.upload("notes.txt", "a".repeat(10_000)); // stored gzipped
await (await files.download("notes.txt")).text(); // the original 10k stringHow it works
- On
upload, the body is compressed with the configured algorithm (gzip by default). - If the compressed form is smaller, it's stored, and the algorithm plus the original byte length are recorded in the object's
metadata. If it wouldn't shrink - already-compressed inputs like JPEG, ZIP, or encrypted blobs - the original bytes are stored verbatim and markedidentity, so the plugin never inflates your storage. - On
download, the recorded algorithm decompresses the body back to the original bytes and hands you a normalStoredFile- with itssizereporting the original length and the internal metadata fields hidden.
Because the algorithm is stored per object, reads always decompress with the right one. Changing the format option later never breaks objects written under the old format.
Choosing a format
compression() defaults to gzip. Pass format to pick another:
import { compression } from "files-sdk/compression";
compression(); // gzip (default)
compression({ format: "deflate" }); // zlib-wrapped deflate
compression({ format: "deflate-raw" }); // bare deflate, no framingBrotli is intentionally not offered. It isn't part of the Compression Streams standard, so supporting it would mean a native dependency and break the plugin's run-anywhere promise. The three formats above are the ones every platform ships.
Ordering
Compression should run before encryption, so it sees plaintext - encrypted bytes are effectively random and don't compress. Put it earlier in the array:
plugins: [compression(), encryption(key)];Because reads unwind the onion in reverse, a download automatically runs decrypt → decompress. You never hand-manage the symmetry.
Things to keep in mind
The plugin buffers the entire body in memory to compare the compressed and original sizes. It's unsuitable for unknown-length streams and resumable uploads, which re-read the original body.
- Range downloads throw. A byte range of the original maps to no fixed slice of the compressed bytes, so a
downloadwith arangeis refused. url()andsignedUploadUrl()throw. A presigned GET hands out compressed bytes with noContent-Encoding, so a client receives them as-is and can't read them; a presigned PUT would silently bypass compression and store uncompressed bytes. Both fail closed - upload and download through the instance instead.copyandmovejust work. They operate on the stored bytes server-side, and the algorithm marker rides along in the object's metadata, so the copy still decompresses.- Mixed buckets are safe. On read, objects without this plugin's marker (pre-existing data, or anything written elsewhere) pass straight through unchanged, so you can enable it on a bucket that already holds plain objects.
- It needs metadata support. The algorithm and original size are stored as object metadata, so the adapter must support metadata - an
uploadto one that doesn't throws before any bytes move.
What it stores in metadata
Each object this plugin writes carries two fscmp_-prefixed metadata fields: fscmp_alg (the algorithm, or identity when stored verbatim) and fscmp_size (the original, uncompressed byte length). They're stripped from the StoredFile you get back on download, head, and list, so your own metadata is all you see.
API
The plugin contract types and helpers - FilesPlugin, FilesOperation, handlers, and createFiles. See the overview for how they compose.
contentType
A security guard that decides each upload's Content-Type from its bytes, not the client's claim. Magic-byte sniffing stops a mislabeled .png that's really HTML or SVG from being stored as an image and served inline. No metadata, no native dependencies, and it never buffers the whole body.