Skip to main content
BunnyStorage is the Storage component’s Bunny backend. It implements the IStorage interface directly against the Bunny Storage SDK, uploading and downloading files to a regional storage zone over HTTPS — a good fit for edge file delivery paired with Bunny’s CDN. Because it shares the IStorage contract with the filesystem and S3-compatible backends, you can switch to or from Bunny without changing a single call site.

Why Bunny

  • Edge delivery. Files live in a Bunny Storage zone close to your users and pair naturally with Bunny’s CDN.
  • Same IStorage interface. put, getFile, getAsJson, delete, list, and more — identical to every other storage backend.
  • Regional zones. Pick from nine storage regions with a short region code.
  • Flexible content. put() accepts strings, ArrayBuffer, Blob, Request/Response, and BunFile/S3File, converting each to a stream for upload.
  • Container-managed. Registered with @decorator.storage() and resolved from the container.

Installation

BunnyStorage ships with @ooneex/storage and depends on the Bunny Storage SDK.
bun add @ooneex/storage @bunny.net/storage-sdk

Environment variables

VariableRequiredPurpose
STORAGE_BUNNY_ACCESS_KEYYesBunny storage access key. Missing throws StorageException (API_KEY_REQUIRED).
STORAGE_BUNNY_STORAGE_ZONEYesBunny storage zone name. Missing throws StorageException (STORAGE_ZONE_REQUIRED).
STORAGE_BUNNY_REGIONNoRegion code. Defaults to de.
STORAGE_BUNNY_ACCESS_KEY=your_access_key
STORAGE_BUNNY_STORAGE_ZONE=your-zone
STORAGE_BUNNY_REGION=de
Credentials are validated when BunnyStorage is constructed, so a missing key or zone fails fast at startup.

Regions

STORAGE_BUNNY_REGION accepts a short code that maps to a Bunny storage region:
CodeRegion
deFalkenstein (default)
ukLondon
nyNew York
laLos Angeles
sgSingapore
seStockholm
brSão Paulo
jhJohannesburg
sydSydney

How it works

BunnyStorage connects to your storage zone with the access key and chosen region, then organizes files under a bucket. The active bucket becomes a path prefix: with bucket uploads, a key avatars/1.png is stored at /uploads/avatars/1.png. Content passed to put() is converted to a ReadableStream and uploaded through the Bunny SDK; reads stream back and are materialized as JSON, an ArrayBuffer, a stream, or written to disk.
MethodPurpose
getBucket() / setBucket(name)Read or switch the active bucket (path prefix); setBucket chains.
list()List the file object names in the current bucket.
clearBucket()Delete every file in the current bucket.
exists(key)Whether an object exists.
delete(key)Remove one object (no-op if absent).
put(key, content)Upload content; returns the number of bytes written.
putFile(key, localPath)Upload a local file.
putDir(bucket, options)Upload a local directory recursively with an optional filter regex.
getFile(key, options)Download an object to options.outputDir (optionally renamed).
getAsJson<T>(key)Parse the object as JSON.
getAsArrayBuffer(key)Read the object as an ArrayBuffer.
getAsStream(key)Read the object as a ReadableStream.

Usage

The API is the same as every other storage backend.
import { container } from "@ooneex/container";
import { BunnyStorage } from "@ooneex/storage";

const storage = container.get(BunnyStorage).setBucket("uploads");

// Write content — returns the number of bytes written
await storage.put("avatars/123.png", file); // file: Blob | BunFile | ArrayBuffer | ...
await storage.put("config.json", JSON.stringify({ theme: "dark" }));

// Read it back
const config = await storage.getAsJson<{ theme: string }>("config.json");
const buffer = await storage.getAsArrayBuffer("avatars/123.png");

// Existence, listing, and removal
await storage.exists("avatars/123.png"); // true
const keys = await storage.list();
await storage.delete("avatars/123.png");
Upload a local file or a whole directory, and stream large files instead of buffering them:
// Upload one local file, or a directory recursively (images only)
await storage.putFile("avatars/123.png", "/tmp/avatar.png");
await storage.putDir("avatars", {
  path: "/tmp/avatars",
  filter: /\.(png|jpe?g)$/,
});

// Stream a large object straight to the response
const stream = storage.getAsStream("videos/intro.mp4");
return new Response(stream);

Use in the app

In an @ooneex/app application, storage isn’t a dedicated App config slot — BunnyStorage registers itself with the container through @decorator.storage() as soon as the class is imported, and you inject it wherever you read or write files.
bun add @ooneex/app @ooneex/storage @bunny.net/storage-sdk
Inject it into a service or controller, set the bucket, and use the IStorage methods:
import { inject } from "@ooneex/container";
import { BunnyStorage } from "@ooneex/storage";

export class MediaService {
  constructor(@inject(BunnyStorage) private readonly storage: BunnyStorage) {}

  public async uploadAvatar(userId: string, file: Blob): Promise<void> {
    await this.storage.setBucket("uploads").put(`avatars/${userId}.png`, file);
  }
}
To bind a dedicated bucket to its own injectable type, register a subclass with @decorator.storage():
import { decorator } from "@ooneex/storage";
import { BunnyStorage } from "@ooneex/storage";

@decorator.storage()
export class AvatarStorage extends BunnyStorage {}
Set STORAGE_BUNNY_ACCESS_KEY, STORAGE_BUNNY_STORAGE_ZONE, and (optionally) STORAGE_BUNNY_REGION in your .env.yml (or environment) so the backend can connect.

Exceptions

BunnyStorage throws StorageException on misconfiguration or a failed operation, carrying a machine-readable key.
KeyWhen
API_KEY_REQUIREDConstructed without STORAGE_BUNNY_ACCESS_KEY.
STORAGE_ZONE_REQUIREDConstructed without STORAGE_BUNNY_STORAGE_ZONE.
STORAGE_UPLOAD_FAILEDAn upload to the storage zone failed.
STORAGE_DOWNLOAD_FAILEDA download or read failed.
STORAGE_LIST_FAILEDListing the zone failed.
UNSUPPORTED_CONTENT_TYPEput() received content it cannot convert to a stream.
import { StorageException } from "@ooneex/storage";

try {
  await storage.put("avatars/123.png", file);
} catch (error) {
  if (error instanceof StorageException) {
    logger.error(`Storage error [${error.key}]: ${error.message}`, error.data);
  }
  throw error;
}

Best practices

  • Set the bucket first. Call setBucket() before reads or writes so keys land under the right path prefix.
  • Pick the nearest region. Set STORAGE_BUNNY_REGION to the zone closest to your users for lower latency.
  • Use stable, namespaced keys. Group objects under prefixes like avatars/123.png so list() and clearBucket() stay predictable.
  • Stream large files. Prefer getAsStream() and putFile/putDir over loading whole files into memory.
  • Filter directory uploads. Pass a filter regex to putDir() so you only ship the files you intend to.
  • Keep credentials in the environment. Load the access key and zone from .env; never hard-code them.
See the Storage component for the full interface and the other backends, including Cloudflare Storage.