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

# Storage

> Store and retrieve files behind a single IStorage interface with filesystem, Cloudflare R2, or Bunny backends.

The `@ooneex/storage` component is a multi-backend file storage layer. Every backend implements the same `IStorage` interface — `put`, `getFile`, `getAsJson`, `delete`, `list`, and more — so you can start on the local filesystem and move to Cloudflare R2 or Bunny without changing a single call site. Files are organized into buckets, and the S3-compatible backends share one abstract `Storage` base class.

## Why this component

* **One interface, many backends.** Filesystem, Cloudflare R2, and Bunny all implement `IStorage` — swap backends without touching callers.
* **Bucket-based.** Group files under a bucket with `setBucket()` / `getBucket()`; list or clear a whole bucket in one call.
* **Flexible content.** `put()` accepts strings, `ArrayBuffer`, `Blob`, `Request`/`Response`, and `BunFile`/`S3File`.
* **Read it your way.** Pull a file back as JSON, an `ArrayBuffer`, a stream, or write it straight to disk with `getFile()`.
* **Directory uploads.** `putDir()` walks a local directory recursively with an optional regex filter.
* **Container-managed.** Register a storage class with a decorator and resolve it from the container.

## How it works

You pick (or implement) a backend and register it. Reads and writes go through the `IStorage` methods; the backend handles the transport, bucketing, and content conversion.

| Method                    | Purpose                                                                                |
| ------------------------- | -------------------------------------------------------------------------------------- |
| `getBucket()`             | Return the active bucket name.                                                         |
| `setBucket(name)`         | Switch the active bucket; returns the storage for chaining.                            |
| `list()`                  | List the keys in the current bucket.                                                   |
| `clearBucket()`           | Delete every object in the current bucket.                                             |
| `exists(key)`             | Whether an object exists.                                                              |
| `delete(key)`             | Remove one object.                                                                     |
| `put(key, content)`       | Write 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`.                        |
| `getFile(key, options)`   | Download an object to `options.outputDir` (optionally renamed via `options.filename`). |
| `getAsJson<T>(key)`       | Parse the object as JSON.                                                              |
| `getAsArrayBuffer(key)`   | Read the object as an `ArrayBuffer`.                                                   |
| `getAsStream(key)`        | Read the object as a `ReadableStream`.                                                 |

The built-in backends differ in where files live, not in how you call them:

| Backend             | Best for                                   | Transport                 |
| ------------------- | ------------------------------------------ | ------------------------- |
| `FilesystemStorage` | Local development, single-instance storage | Local disk                |
| `CloudflareStorage` | Production object storage on Cloudflare R2 | S3-compatible (HTTPS)     |
| `BunnyStorage`      | Edge file delivery via Bunny Storage zones | Bunny Storage SDK (HTTPS) |

`FilesystemStorage` and `CloudflareStorage` extend the shared `Storage` base class and talk to an S3 client under the hood. `BunnyStorage` implements `IStorage` directly against the Bunny Storage SDK.

## Environment variables

| Variable                        | Backend             | Required | Purpose                                                                                           |
| ------------------------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `FILESYSTEM_STORAGE_PATH`       | `FilesystemStorage` | Yes      | Base directory for stored files. Missing throws `StorageException` (`STORAGE_ROOT_DIR_REQUIRED`). |
| `STORAGE_CLOUDFLARE_ACCESS_KEY` | `CloudflareStorage` | Yes      | R2 access key. Missing throws `StorageException` (`CONFIG_REQUIRED`).                             |
| `STORAGE_CLOUDFLARE_SECRET_KEY` | `CloudflareStorage` | Yes      | R2 secret key. Missing throws `StorageException` (`CONFIG_REQUIRED`).                             |
| `STORAGE_CLOUDFLARE_ENDPOINT`   | `CloudflareStorage` | Yes      | R2 S3 endpoint URL. Missing throws `StorageException` (`CONFIG_REQUIRED`).                        |
| `STORAGE_CLOUDFLARE_REGION`     | `CloudflareStorage` | No       | R2 region. Defaults to `EEUR`.                                                                    |
| `STORAGE_BUNNY_ACCESS_KEY`      | `BunnyStorage`      | Yes      | Bunny storage access key. Missing throws `StorageException` (`API_KEY_REQUIRED`).                 |
| `STORAGE_BUNNY_STORAGE_ZONE`    | `BunnyStorage`      | Yes      | Bunny storage zone name. Missing throws `StorageException` (`STORAGE_ZONE_REQUIRED`).             |
| `STORAGE_BUNNY_REGION`          | `BunnyStorage`      | No       | Bunny region code (`de`, `uk`, `ny`, `la`, `sg`, `se`, `br`, `jh`, `syd`). Defaults to `de`.      |

```bash theme={null}
FILESYSTEM_STORAGE_PATH=./storage
STORAGE_CLOUDFLARE_ACCESS_KEY=your_access_key
STORAGE_CLOUDFLARE_SECRET_KEY=your_secret_key
STORAGE_CLOUDFLARE_ENDPOINT=https://your-account.r2.cloudflarestorage.com
STORAGE_BUNNY_ACCESS_KEY=your_access_key
STORAGE_BUNNY_STORAGE_ZONE=your-zone
```

## Usage

The API is the same regardless of backend.

```typescript theme={null}
import { CloudflareStorage } from "@ooneex/storage";
import { container } from "@ooneex/container";

const storage = container.get(CloudflareStorage).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 an entire directory, and download objects to disk:

```typescript theme={null}
// Upload one local file
await storage.putFile("avatars/123.png", "/tmp/avatar.png");

// Upload a directory recursively, keeping only images
await storage.putDir("avatars", {
  path: "/tmp/avatars",
  filter: /\.(png|jpe?g)$/,
});

// Download an object to a local directory
await storage.getFile("avatars/123.png", {
  outputDir: "/tmp/downloads",
  filename: "user-avatar.png",
});
```

For large files, stream instead of buffering the whole object in memory:

```typescript theme={null}
const stream = storage.getAsStream("videos/intro.mp4");
return new Response(stream);
```

## Decorator and usage

### `@decorator.storage()`

Registers a storage class with the container. It accepts an optional scope (defaults to singleton). Use it to register a built-in backend under your own name, or a custom backend that extends `Storage` or implements `IStorage`.

```typescript theme={null}
import { decorator } from "@ooneex/storage";
import { CloudflareStorage } from "@ooneex/storage";

@decorator.storage()
export class UploadStorage extends CloudflareStorage {}
```

Resolve it from the container and inject it where needed:

```typescript theme={null}
import { inject } from "@ooneex/container";

export class UploadService {
  constructor(@inject(UploadStorage) private readonly storage: UploadStorage) {}
}
```

## Exceptions

The component throws `StorageException` when a backend is misconfigured or an operation fails. It carries a machine-readable `key`, a human-readable `message`, and a `data` object.

| Key                         | When                                                                    |
| --------------------------- | ----------------------------------------------------------------------- |
| `STORAGE_ROOT_DIR_REQUIRED` | `FilesystemStorage` is created without `FILESYSTEM_STORAGE_PATH`.       |
| `CONFIG_REQUIRED`           | `CloudflareStorage` is missing its access key, secret key, or endpoint. |
| `API_KEY_REQUIRED`          | `BunnyStorage` is missing its access key.                               |
| `STORAGE_ZONE_REQUIRED`     | `BunnyStorage` is missing its storage zone.                             |
| `STORAGE_BUCKET_REQUIRED`   | A `FilesystemStorage` operation runs before `setBucket()`.              |
| `STORAGE_UPLOAD_FAILED`     | A write or directory creation failed.                                   |
| `STORAGE_DOWNLOAD_FAILED`   | A read or download failed.                                              |
| `STORAGE_LIST_FAILED`       | Listing a bucket failed.                                                |
| `STORAGE_DELETE_FAILED`     | Deleting an object failed.                                              |
| `STORAGE_CLEAR_FAILED`      | Clearing a bucket failed.                                               |
| `FILE_NOT_FOUND`            | A read targeted an object that does not exist.                          |
| `UNSUPPORTED_CONTENT_TYPE`  | `BunnyStorage.put()` received content it cannot convert.                |

```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);
  } else {
    throw error;
  }
}
```

## Best practices

* **Set the bucket first.** Call `setBucket()` before reads or writes — `FilesystemStorage` throws `STORAGE_BUCKET_REQUIRED` otherwise.
* **Use stable, namespaced keys.** Group related objects under prefixes like `avatars/123.png` so `list()` and `clearBucket()` stay predictable.
* **Stream large files.** Prefer `getAsStream()` (and `putDir`/`putFile` for local sources) over loading whole files into memory.
* **Filter directory uploads.** Pass a `filter` regex to `putDir()` so you only ship the files you intend to.
* **Pick the backend per environment.** Filesystem for local/dev, Cloudflare R2 or Bunny for production — the code stays identical.
* **Handle the missing case.** Reads throw `FILE_NOT_FOUND` (or `STORAGE_DOWNLOAD_FAILED`) when an object is absent; catch `StorageException` and branch on `error.key`.
* **Throw `StorageException` with a stable `key`.** In custom backends, keep keys constant and put variable detail in `data`.

## CLI command

Scaffold a storage adapter and its test file with the generator. It writes the class under `modules/<module>/src/storage/<Name>Storage.ts` and installs `@ooneex/storage` if it is missing.

```bash theme={null}
# Interactive: prompts for the name
ooneex storage:create

# Provide the name
ooneex storage:create --name=Upload

# Target a module and overwrite
ooneex storage:create --name=Upload --module=media --override
```

| Option       | Description                                                         | Default             |
| ------------ | ------------------------------------------------------------------- | ------------------- |
| `--name`     | Storage class name. The `Storage` suffix is appended automatically. | Prompted if omitted |
| `--module`   | Target module the class is generated into.                          | `shared`            |
| `--override` | Overwrite an existing class without prompting.                      | `false`             |

The generated class extends `Storage` as an S3-compatible adapter, ready for you to set the bucket and confirm its credentials:

```typescript theme={null}
import { Storage, decorator, StorageException } from "@ooneex/storage";
import { inject } from "@ooneex/container";
import { AppEnv } from "@ooneex/app-env";
import type { S3Options } from "bun";

@decorator.storage()
export class UploadStorage extends Storage {
  protected bucket: string;
  private readonly accessKey: string;
  private readonly secretKey: string;
  private readonly endpoint: string;
  private readonly region: string;

  constructor(
    @inject(AppEnv) private readonly env: AppEnv,
    options?: {
      accessKey?: string;
      secretKey?: string;
      endpoint?: string;
      region?: string;
    },
  ) {
    super();

    const accessKey = options?.accessKey || this.env.STORAGE_UPLOAD_ACCESS_KEY;
    const secretKey = options?.secretKey || this.env.STORAGE_UPLOAD_SECRET_KEY;
    const endpoint = options?.endpoint || this.env.STORAGE_UPLOAD_ENDPOINT;

    if (!accessKey) {
      throw new StorageException(
        "Upload access key is required. Please provide an access key either through the constructor options or set the STORAGE_UPLOAD_ACCESS_KEY environment variable.",
      );
    }
    if (!secretKey) {
      throw new StorageException(
        "Upload secret key is required. Please provide a secret key either through the constructor options or set the STORAGE_UPLOAD_SECRET_KEY environment variable.",
      );
    }
    if (!endpoint) {
      throw new StorageException(
        "Upload endpoint is required. Please provide an endpoint either through the constructor options or set the STORAGE_UPLOAD_ENDPOINT environment variable.",
      );
    }

    this.accessKey = accessKey;
    this.secretKey = secretKey;
    this.endpoint = endpoint;
    this.region = options?.region || this.env.STORAGE_UPLOAD_REGION || "auto";
  }

  public getOptions(): S3Options {
    return {
      accessKeyId: this.accessKey,
      secretAccessKey: this.secretKey,
      endpoint: this.endpoint,
      bucket: this.bucket,
      region: this.region,
    };
  }
}
```

See [storage:create](/cli/commands/storage-create) for the full command reference.

## Use with Claude and Codex

The generator ships a matching `storage:create` skill. It runs the scaffold and then guides your AI agent through completing the adapter — setting the bucket, wiring the credential env vars, and finishing the test file. Initialize the skills once for your agent:

<Tabs>
  <Tab title="Claude">
    ```bash theme={null}
    ooneex claude:init
    ```

    Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create a storage adapter for user uploads backed by S3.
    ```
  </Tab>

  <Tab title="Codex">
    ```bash theme={null}
    ooneex codex:init
    ```

    Then ask Codex in natural language — it maps the request to the generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create a storage adapter for user uploads backed by S3.
    ```
  </Tab>
</Tabs>

For example, the prompt above maps to `storage:create --name=Upload`, then sets the bucket and credential env vars on the generated `Storage` subclass.
