Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: cargo test
run: cargo test --workspace
# The `s3` and `sftp` disks sit behind `rustasea-storage`'s opt-in `aws`
# and `sftp` features, so the default workspace run never compiles them.
# Their Docker-backed suites (RustFS, SFTP) stay `#[ignore]`d.
- name: cargo test (rustasea-storage, aws + sftp)
run: cargo test -p rustasea-storage --features aws,sftp

# Supply-chain gate: license allow-list, advisory database, banned crates and
# source provenance (configuration in deny.toml).
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ Per the same section, each tag is expected to pass `cargo xtask ci` and

### Added

- 2026-09-24 - `storage-s3` umbrella feature forwarding to
`rustasea-storage/aws` (the `aws` feature needs rustc 1.89+ through
`object_store`'s `crc-fast` dependency), and a Docker-backed RustFS suite
(`crates/rustasea-storage/tests/rustfs_container.rs`, run with
`--features aws -- --ignored`) that round-trips an `s3` disk over a
plain-HTTP endpoint. `cargo xtask ci` and the CI test job now also cover
`rustasea-storage` with its opt-in `aws` and `sftp` drivers (M6;
`feat(rustasea): add storage-s3 umbrella feature`; `ci: lint and test
rustasea-storage with the aws and sftp features`).
- 2026-09-17 - Svelte starter-kit variant with full auth/settings page parity: the
`svelte` variant scaffolds the same 11 auth/settings pages as the react/vue
kits on the shared Inertia contract
Expand Down Expand Up @@ -120,6 +129,21 @@ Per the same section, each tag is expected to pass `cargo xtask ci` and

### Changed

- 2026-09-24 - `s3` disks now work with S3-compatible services such as RustFS
and MinIO: an explicit `http://` endpoint enables `object_store`'s
`allow_http` (previously every request failed with a reqwest builder error,
including the documented `endpoint = "http://localhost:9000"` example).
The `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`,
`AWS_BUCKET`, and `AWS_ENDPOINT` variables documented in `.env.example`,
previously never read, now override the matching keys of every `s3` disk
in `config/storage.toml`; note that the stock `.env.example` sets
`AWS_DEFAULT_REGION=us-east-1`. `bucket` may be omitted when `AWS_BUCKET`
is set, and a blank bucket is rejected with `StorageError::Config`.
`S3DiskConfig` moved to `rustasea_storage::s3` (still re-exported from the
crate root and `facade`) and redacts `secret_access_key` from `Debug`
output (M6; `refactor(storage): move S3DiskConfig into its own module and
share env helpers`; `fix(storage): support S3-compatible http endpoints
and AWS_* env for s3 disks`).
- 2026-09-17 - `cargo install rustasea` now installs the application scaffolder:
the `cargo-rustasea` binary moved into the `rustasea` facade package (ships
behind the `scaffold` feature, enabled by default) so `cargo rustasea new`
Expand Down
12 changes: 10 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ The integration suite requires a running Docker daemon and is opt-in:
cargo test -p rustasea --features integration -- --ignored
```

The storage drivers have their own Docker-backed suites (RustFS for the `s3`
disk, an SFTP server for the `sftp` disk):

```bash
cargo test -p rustasea-storage --features aws --test rustfs_container -- --ignored
cargo test -p rustasea-storage --features sftp --test sftp_container -- --ignored
```

## Branch and commit conventions

- **Branch from `master`.** Use a short, descriptive branch name such as
Expand Down Expand Up @@ -125,8 +133,8 @@ cargo test -p rustasea --features integration -- --ignored

| Job | What it runs |
|---|---|
| `quality` | `cargo xtask ci` (fmt, clippy with `-D warnings`, `deps:check`, `lines:check`, cycle check) |
| `test` | `cargo test --workspace` |
| `quality` | `cargo xtask ci` (fmt, clippy with `-D warnings` on the workspace and on `rustasea-storage --features aws,sftp`, `deps:check`, `lines:check`, cycle check) |
| `test` | `cargo test --workspace`, then `cargo test -p rustasea-storage --features aws,sftp` |
| `deny` | `cargo deny check` |
| `audit` | `cargo audit` |
| `msrv` | `cargo check --workspace` on Rust 1.88.0 |
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ The `rustasea` umbrella crate re-exports the whole framework, but several crates
| `action` | `dep:rustasea-action` | Action pattern adapters for HTTP/queue/CLI/events, `make:action` (ADOPT-028) |
| `google` | `dep:rustasea-google` | Service-account auth with cached OAuth2 access tokens (ADOPT-026) |
| `storage-sftp` | `rustasea-storage/sftp` | Pure-Rust `russh`/`russh-sftp` SFTP disk (ADOPT-025) |
| `storage-s3` | `rustasea-storage/aws` | S3 disk for AWS or S3-compatible services (RustFS, MinIO, R2), with the `AWS_*` env overlay and plain-HTTP local endpoints (needs rustc 1.89+, via `crc-fast`) |
| `excel` | `dep:rustasea-excel` | Excel/CSV import-export with queued jobs and signed links (ADOPT-023) |
| `image` | `dep:rustasea-image` | Image transform pipeline with EXIF auto-orient (ADOPT-024) |
| `debugbar` | `dep:rustasea-debugbar` | Dev request profiler / debug toolbar (ADOPT-009) |
Expand Down Expand Up @@ -759,7 +760,8 @@ cargo xtask migrate # run migrations
```

CI runs the same gate — see [`.github/workflows/ci.yml`](.github/workflows/ci.yml):
`quality` (`cargo xtask ci`), `test` (`cargo test --workspace`), `deny`
`quality` (`cargo xtask ci`), `test` (`cargo test --workspace`, plus
`rustasea-storage` with its opt-in `aws`/`sftp` drivers), `deny`
(`cargo deny check`), `audit` (`cargo audit`), and an `msrv` job that checks the
workspace builds on the 1.88 floor (ADR-0001). **Formatting violations fail the
build**: run `cargo fmt --all` before pushing, or `cargo xtask fmt` to check.
Expand Down
18 changes: 13 additions & 5 deletions config/storage.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
# contract (read-through with optional copy-back to the primary).
#
# Cloud drivers are opt-in: enable the matching crate feature
# (`rustasea-storage/{aws,gcp,azure}`) before referencing `s3`, `gcs`, or
# `azure` disks. The `sftp` driver is likewise opt-in
# (`rustasea-storage/sftp`, or `storage-sftp` on the `rustasea` facade crate).
# (`rustasea-storage/{aws,gcp,azure}`, or `storage-s3` on the `rustasea` facade
# crate for `s3`) before referencing `s3`, `gcs`, or `azure` disks. The `sftp`
# driver is likewise opt-in (`rustasea-storage/sftp`, or `storage-sftp` on the
# `rustasea` facade crate).

[storage]
default = "local"
Expand Down Expand Up @@ -60,10 +61,17 @@ report = false
# visibility = "public"
# throw = false
# report = false
# # Prefer the environment / a secret manager over inline credentials.
# # Prefer the environment over inline credentials: AWS_ACCESS_KEY_ID,
# # AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, and AWS_ENDPOINT
# # override the matching keys on every `s3` disk (blank values are ignored),
# # and `bucket` may be left out when AWS_BUCKET is set. `.env.example` sets
# # AWS_DEFAULT_REGION, so change the region there. With more than one `s3`
# # disk, leave AWS_BUCKET and AWS_ENDPOINT blank and set them per disk here.
# # access_key_id = "..."
# # secret_access_key = "..."
# # endpoint = "http://localhost:9000" # S3-compatible (MinIO, R2)
# # S3-compatible services (RustFS, MinIO, R2). An `http://` endpoint enables
# # plain HTTP for local development; any other endpoint stays HTTPS-only.
# # endpoint = "http://localhost:9000"
#
# [storage.disks.gcs]
# driver = "gcs"
Expand Down
5 changes: 3 additions & 2 deletions crates/rustasea-storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ azure = ["object_store/azure"]
sftp = ["dep:russh", "dep:russh-sftp"]

[dev-dependencies]
# Docker-backed SFTP integration tests (`tests/sftp_container.rs`), run only
# with `--features sftp -- --ignored`.
# Docker-backed integration tests, run only with `-- --ignored`: SFTP
# (`tests/sftp_container.rs`, `--features sftp`) and RustFS-backed S3
# (`tests/rustfs_container.rs`, `--features aws`).
testcontainers = { workspace = true }
18 changes: 18 additions & 0 deletions crates/rustasea-storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ Part of the [RustaSea framework](https://github.com/rustasea/framework) - a Lara
rustasea-storage = "0.1"
```

## S3 and S3-compatible disks

Enable the `aws` feature (or `storage-s3` on the `rustasea` facade crate) to
build `driver = "s3"` disks from `config/storage.toml`. `AWS_ACCESS_KEY_ID`,
`AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, `AWS_BUCKET`, and `AWS_ENDPOINT`
override the matching keys on every `s3` disk (blank values are ignored). An
`http://` endpoint enables plain HTTP for local S3-compatible services such as
RustFS or MinIO; any other endpoint stays HTTPS-only.

The `aws` feature needs rustc 1.89 or newer: `object_store`'s AWS backend
depends on `crc-fast` 1.10, which raised its MSRV above the workspace's 1.88.

The Docker-backed RustFS suite runs with:

```text
cargo test -p rustasea-storage --features aws --test rustfs_container -- --ignored
```

## License

MIT - see [LICENSE-MIT](https://github.com/rustasea/framework/blob/master/LICENSE-MIT).
57 changes: 21 additions & 36 deletions crates/rustasea-storage/src/facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use crate::disk::LocalDisk;
use crate::error::{Result, StorageError};
use crate::manager::{ManagedDisk, StorageConfig, StorageManager};

pub use crate::s3::S3DiskConfig;

/// Top-level `[storage]` configuration document.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)]
pub struct StorageFacadeConfig {
Expand Down Expand Up @@ -143,28 +145,6 @@ pub struct LocalDiskConfig {
pub settings: DiskSettings,
}

/// Configuration for an S3 disk.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct S3DiskConfig {
/// Bucket name.
pub bucket: String,
/// AWS region (falls back to the SDK default when absent).
#[serde(default)]
pub region: Option<String>,
/// Explicit access key id (prefer environment/secret manager).
#[serde(default)]
pub access_key_id: Option<String>,
/// Explicit secret access key (prefer environment/secret manager).
#[serde(default)]
pub secret_access_key: Option<String>,
/// Custom endpoint for S3-compatible services (MinIO, R2).
#[serde(default)]
pub endpoint: Option<String>,
/// Shared Laravel-parity disk settings.
#[serde(flatten, default)]
pub settings: DiskSettings,
}

/// Configuration for a Google Cloud Storage disk.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct GcsDiskConfig {
Expand Down Expand Up @@ -240,23 +220,18 @@ impl StorageManager {
}

/// Build an S3-backed disk.
///
/// The `AWS_*` environment overlay is applied and the config validated before
/// the `object_store` builder is configured (see [`S3DiskConfig::apply_env`]).
#[cfg(feature = "aws")]
fn build_s3(config: &S3DiskConfig, name: &str) -> Result<Arc<dyn ManagedDisk>> {
let mut builder =
object_store::aws::AmazonS3Builder::new().with_bucket_name(config.bucket.clone());
if let Some(region) = &config.region {
builder = builder.with_region(region.clone());
}
if let Some(key_id) = &config.access_key_id {
builder = builder.with_access_key_id(key_id.clone());
}
if let Some(secret) = &config.secret_access_key {
builder = builder.with_secret_access_key(secret.clone());
}
if let Some(endpoint) = &config.endpoint {
builder = builder.with_endpoint(endpoint.clone());
let mut config = config.clone();
config.apply_env();
if let Err(error) = config.validate() {
return Err(StorageError::Config(format!("disk {name}: {error}")));
}
let store = builder
let store = config
.builder()
.build()
.map_err(|e| StorageError::StoreUnavailable(format!("disk {name}: {e}")))?;
Ok(Arc::new(crate::ObjectDisk::new(
Expand Down Expand Up @@ -346,3 +321,13 @@ fn build_sftp(_config: &crate::sftp::SftpDiskConfig, name: &str) -> Result<Arc<d
"disk {name}: the `sftp` driver requires the `sftp` feature"
)))
}

/// Read an environment variable, treating unset or blank values as absent.
///
/// Shared by the per-driver environment overlays (`S3DiskConfig::apply_env`,
/// `SftpDiskConfig::apply_env`).
pub(crate) fn env_non_empty(key: &str) -> Option<String> {
std::env::var(key)
.ok()
.filter(|value| !value.trim().is_empty())
}
4 changes: 4 additions & 0 deletions crates/rustasea-storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ pub mod error;
pub mod facade;
pub mod manager;
pub mod path;
pub mod s3;
pub mod sftp;
pub mod storage;

#[cfg(test)]
mod test_support;

pub use crate::disk::{DiskKind, LocalDisk, ReadThrough, ReadThroughDisk};
pub use crate::error::{PathError, Result, StorageError};
pub use crate::facade::{
Expand Down
Loading
Loading