> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bunny Storage

> Edge file storage on Bunny Storage zones behind the framework's IStorage interface.

`BunnyStorage` is the [Storage](/components/storage) component's [Bunny](https://bunny.net) 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.

```bash theme={null}
bun add @ooneex/storage @bunny.net/storage-sdk
```

## Environment variables

| Variable                     | Required | Purpose                                                                               |
| ---------------------------- | -------- | ------------------------------------------------------------------------------------- |
| `STORAGE_BUNNY_ACCESS_KEY`   | Yes      | Bunny storage access key. Missing throws `StorageException` (`API_KEY_REQUIRED`).     |
| `STORAGE_BUNNY_STORAGE_ZONE` | Yes      | Bunny storage zone name. Missing throws `StorageException` (`STORAGE_ZONE_REQUIRED`). |
| `STORAGE_BUNNY_REGION`       | No       | Region code. Defaults to `de`.                                                        |

```bash theme={null}
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:

| Code  | Region                |
| ----- | --------------------- |
| `de`  | Falkenstein (default) |
| `uk`  | London                |
| `ny`  | New York              |
| `la`  | Los Angeles           |
| `sg`  | Singapore             |
| `se`  | Stockholm             |
| `br`  | São Paulo             |
| `jh`  | Johannesburg          |
| `syd` | Sydney                |

## 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.

| Method                            | Purpose                                                               |
| --------------------------------- | --------------------------------------------------------------------- |
| `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.

```typescript theme={null}
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:

```typescript theme={null}
// 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.

```bash theme={null}
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:

```typescript theme={null}
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()`:

```typescript theme={null}
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`.

| Key                        | When                                                    |
| -------------------------- | ------------------------------------------------------- |
| `API_KEY_REQUIRED`         | Constructed without `STORAGE_BUNNY_ACCESS_KEY`.         |
| `STORAGE_ZONE_REQUIRED`    | Constructed without `STORAGE_BUNNY_STORAGE_ZONE`.       |
| `STORAGE_UPLOAD_FAILED`    | An upload to the storage zone failed.                   |
| `STORAGE_DOWNLOAD_FAILED`  | A download or read failed.                              |
| `STORAGE_LIST_FAILED`      | Listing the zone failed.                                |
| `UNSUPPORTED_CONTENT_TYPE` | `put()` received content it cannot convert to a stream. |

```typescript theme={null}
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](/components/storage) for the full interface and the other backends, including [Cloudflare Storage](/integrations/cloudflare-storage).
