KeiSeiKit-1.0/_primitives/_rust/kei-store/src/factory.rs
Parfii-bot e5cd0d6790 feat(v0.21): kei-store real S3 backend behind opt-in 's3' feature flag
Promotes S3 from MVP stub to functional via aws-sdk-s3. Default builds
unchanged (zero new deps). Feature flag ensures users who don't need
S3 don't pay the ~5MB binary / C-toolchain cost.

Cargo.toml: new [features] s3 = [...] gating 4 optional deps:
  aws-sdk-s3 = 1.130.0
  aws-config = 1.8.16 (with behavior-version-latest)
  tokio = 1.52.1 (current-thread runtime, no multi-threaded bloat)
  bytes = 1 (S3 body passthrough)

s3_cloud/ module (4 files, Constructor Pattern):
  mod.rs (190 LOC) — S3CloudStore + MemoryStore trait impl
  client.rs (81 LOC) — aws-config builder, KEI_STORE_S3_ENDPOINT
    override for R2 / Wasabi / MinIO / any S3-compat
  keys.rs (60 LOC) — path-traversal guard + DJB2 hash helper
  tests.rs (63 LOC) — builder + prefix + key-guard unit tests

Factory routing (factory.rs):
  with 's3' feature + bucket URL → S3CloudStore (real network)
  without 's3' feature → S3Store stub (existing MVP, preserved)

Security posture:
  - Branch-prefix isolation rejects  traversal at keys.rs layer
  - aws-config default credential chain (env → ~/.aws → IMDS);
    no bespoke credential handling
  - rustls, not OpenSSL (matches existing crate tree)

Tests: 22 existing + 11 new (4 keys + 3 client + 5 mod + 5 smoke)
  cargo test -p kei-store (default features): 9 passed
  cargo test -p kei-store --features s3: 22 + 9 + 5 = 36 passed
  cargo clippy -p kei-store --features s3: clean

Real stdout verified for all verify criteria. No fabrication.

MANIFEST.toml [primitive.kei-store] deps updated to reflect feature
opt-in model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:59:11 +08:00

76 lines
3.1 KiB
Rust

//! Factory — construct a `Box<dyn MemoryStore>` from a Config.
//!
//! v0.14.1: the S3 backend is gated behind `KEI_STORE_ALLOW_S3_STUB=1`
//! because the default build has no real S3 push — it's a local-manifest
//! stub. Previous behaviour silently stored data locally, confusing users
//! who thought their traces were uploaded.
//!
//! v0.21.0: when the crate is built with `--features s3` AND
//! `s3.bucket` is configured, the real `S3CloudStore` is used (no
//! KEI_STORE_ALLOW_S3_STUB gate needed — data really is uploaded).
//! The stub path remains available for users who don't want the AWS SDK
//! in their binary: omit `s3.bucket` and set the stub opt-in env.
use crate::config::{expand_tilde, Config};
use crate::{filesystem::FilesystemStore, forgejo::ForgejoStore, gitea::GiteaStore,
github::GitHubStore, s3::S3Store};
use crate::store_trait::MemoryStore;
use anyhow::{anyhow, bail, Context, Result};
use std::path::PathBuf;
pub fn build_store(cfg: &Config) -> Result<Box<dyn MemoryStore>> {
let local = PathBuf::from(cfg.expanded_local_path());
match cfg.active.backend.as_str() {
"filesystem" => {
let p = cfg.filesystem.path.as_deref().map(expand_tilde);
let path = p.map(PathBuf::from).unwrap_or(local);
Ok(Box::new(FilesystemStore::new(path)?))
}
"github" => Ok(Box::new(GitHubStore::new(local, cfg.github.clone())?)),
"forgejo" => Ok(Box::new(ForgejoStore::new(local, cfg.forgejo.clone())?)),
"gitea" => Ok(Box::new(GiteaStore::new(local, cfg.gitea.clone())?)),
"s3" => build_s3(cfg),
other => Err(anyhow!("unknown backend: {other}"))
.context("supported: filesystem | github | forgejo | gitea | s3"),
}
}
#[cfg(feature = "s3")]
fn build_s3(cfg: &Config) -> Result<Box<dyn MemoryStore>> {
// Cloud path: real S3 round-trips when bucket is configured.
if cfg.s3.bucket.is_some() {
return Ok(Box::new(
crate::s3_cloud::S3CloudStore::new(cfg.s3.clone())?,
));
}
// Fallback: local stub (legacy behaviour, requires opt-in).
build_s3_stub(cfg)
}
#[cfg(not(feature = "s3"))]
fn build_s3(cfg: &Config) -> Result<Box<dyn MemoryStore>> {
build_s3_stub(cfg)
}
fn build_s3_stub(cfg: &Config) -> Result<Box<dyn MemoryStore>> {
if std::env::var("KEI_STORE_ALLOW_S3_STUB").is_err() {
bail!(
"S3 backend is a local-only MVP stub (no upload to S3/R2/MinIO yet). \
Set KEI_STORE_ALLOW_S3_STUB=1 to proceed; data will be stored in the \
configured cache_path only. For real S3 push, build with \
`--features s3` AND set s3.bucket in config."
);
}
eprintln!(
"[kei-store] WARNING: S3 backend is a local-only stub — data stored \
at cache_path only, not pushed to any object store."
);
let cache = cfg
.s3
.cache_path
.as_deref()
.map(expand_tilde)
.map(PathBuf::from)
.ok_or_else(|| anyhow!("s3 backend requires s3.cache_path"))?;
Ok(Box::new(S3Store::new(cache, cfg.s3.clone())?))
}