arcature 0.1.0

Arcature: an opinionated full-stack Rust web framework. One package, batteries included.
Documentation
# Storage

Object and file storage over OpenDAL, behind a named-disk registry.

`Storage` is a value, not a namespace. `Storage::disk("s3")` is an instance
method on a handle you got from state — there is no static `Storage::disk`.

## Configuring

Single backend:

```rust,ignore
use arcature::storage::{Storage, StorageConfig, S3Config};

let storage = Storage::connect(StorageConfig::fs("storage/app")?).await?;
```

`Storage::connect` registers the backend as a disk named `"default"`.

Several disks:

```rust,ignore
let storage = Storage::builder()
    .disk("local", StorageConfig::fs("storage/app")?)
    .disk("s3", StorageConfig::s3(
        S3Config::new("acme-uploads")?
            .region("eu-west-1")
            .access_key_id(std::env::var("AWS_ACCESS_KEY_ID")?)
            .secret_access_key(std::env::var("AWS_SECRET_ACCESS_KEY")?),
    ))
    .default_disk("local")
    .connect()
    .await?;
```

`S3Config` redacts the access key id and the secret in its `Debug` impl.

`Application::storage(config)` wires the single-backend path at startup.

## Disks

| Call | Behaviour |
| --- | --- |
| `storage.disk("s3")` | the named disk; **panics** if it was never registered |
| `storage.try_disk("s3")` | `Option<Disk>` |
| `storage.default_disk()` | the disk named by `default_disk`, or `"default"` |
| `storage.disk_names()` | what is registered |

`disk` panics deliberately: a disk name is a deployment constant, and a typo
should stop the process at the first use rather than return an error every
handler forgets to check. `try_disk` is there when the name really is dynamic.

`Disk` is cheap to clone — the OpenDAL `Operator` inside it is `Arc`-backed.

## Paths

Every data-path method takes a `&StoragePath`, not a `&str`. Constructing one
is where the validation happens:

```rust,ignore
use arcature::storage::StoragePath;

let path = StoragePath::new("avatars/1.png")?;
storage.disk("s3").put(&path, &bytes).await?;
```

Rejected: empty keys, absolute paths (`/etc/passwd`), any `..` segment,
backslashes, ASCII control characters, and empty segments (`a//b`).

Allowed: trailing slashes, because they are meaningful as list prefixes;
Unicode of all kinds; dots inside a segment.

The check runs before any storage work does, so a traversal attempt fails at
the type boundary rather than at the backend.

## Operations

All on `Disk`:

```rust,ignore
let disk = storage.disk("local");

disk.put(&path, &bytes).await?;
let bytes: Bytes = disk.get(&path).await?;
let present: bool = disk.exists(&path).await?;
let meta = disk.stat(&path).await?;
let entries = disk.list(&prefix).await?;
disk.copy(&from, &to).await?;
disk.rename(&from, &to).await?;
disk.delete(&path).await?;
```

For large objects, `disk.reader(&path)` and `disk.writer(&path)` return the
OpenDAL `Reader` and `Writer` and stream rather than buffering.

`Disk::from_operator(operator)` is the escape hatch when you want to
configure the OpenDAL operator yourself; `disk.operator()` borrows it back.

## Public files

`arc storage:link` links `storage/app/public` into `public/storage`, the
Laravel convention, so files written to the local disk under `public/` are
served as static assets.

## What this module does not own

No object-storage protocol implementation, no S3 signing, no AWS credential
machinery, no multipart engine, no TLS. OpenDAL owns the protocol layer, the
certified rustls plus aws-lc-rs stack owns TLS, Tokio owns the runtime. The
crates are re-exported as `arcature::storage::opendal` and
`arcature::storage::bytes`.