use super::s3::S3Executor;
use super::{ExecutorByteStream, ObjectExecutor, ResourceAdminExecutor};
#[cfg(feature = "s3")]
pub(crate) fn s3_executor_from_env() -> S3Executor {
use aws_sdk_s3::config::{Credentials, Region};
fn var_chain(keys: &[&str], default: &str) -> String {
for key in keys {
if let Ok(value) = std::env::var(key) {
if !value.trim().is_empty() {
return value;
}
}
}
default.to_string()
}
let endpoint = var_chain(
&["UDB_LIVE_S3_ENDPOINT", "UDB_INTEGRATION_MINIO_ENDPOINT"],
"http://127.0.0.1:59000",
);
let access = var_chain(
&["UDB_LIVE_S3_ACCESS_KEY", "UDB_INTEGRATION_MINIO_ACCESS_KEY"],
"minio",
);
let secret = var_chain(
&["UDB_LIVE_S3_SECRET_KEY", "UDB_INTEGRATION_MINIO_SECRET_KEY"],
"minio123",
);
let region = var_chain(&["UDB_LIVE_S3_REGION"], "us-east-1");
let path_style = !matches!(
std::env::var("UDB_LIVE_S3_PATH_STYLE")
.unwrap_or_default()
.to_ascii_lowercase()
.as_str(),
"0" | "false" | "no" | "off"
);
let creds = Credentials::new(access, secret, None, None, "udb-a6-test");
let conf = aws_sdk_s3::Config::builder()
.behavior_version(aws_config::BehaviorVersion::latest())
.credentials_provider(creds)
.region(Region::new(region))
.endpoint_url(endpoint)
.force_path_style(path_style)
.build();
S3Executor(aws_sdk_s3::Client::from_conf(conf))
}
fn payload_of(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
#[cfg(feature = "s3")]
fn byte_stream(payload: &[u8], chunk: usize) -> ExecutorByteStream {
let chunks: Vec<Result<bytes::Bytes, tonic::Status>> = payload
.chunks(chunk.max(1))
.map(|c| Ok(bytes::Bytes::copy_from_slice(c)))
.collect();
Box::pin(tokio_stream::iter(chunks))
}
#[tokio::test]
#[ignore = "requires a live S3-compatible store: set UDB_LIVE_OBJECT_TESTS=1"]
async fn s3_object_stream_roundtrip_all_sizes_live() {
use tokio_stream::StreamExt as _;
if std::env::var("UDB_LIVE_OBJECT_TESTS").is_err() {
eprintln!("skipping: set UDB_LIVE_OBJECT_TESTS=1 to run against a live S3 store");
return;
}
let executor = s3_executor_from_env();
let bucket = format!("udb-a6-{}", uuid::Uuid::new_v4().simple());
executor
.ensure_resource(&bucket, "{}")
.await
.expect("create bucket");
const MIB: usize = 1024 * 1024;
let cases: &[(&str, usize)] = &[
("empty", 0),
("tiny-1k", 1024),
("sub-part-5mib", 5 * MIB),
("exact-part-8mib", 8 * MIB),
("multipart-20mib", 20 * MIB),
];
for (label, len) in cases {
let key = format!("a6/{label}.bin");
let payload = payload_of(*len);
let request_json = serde_json::json!({ "bucket": bucket, "object_key": key }).to_string();
executor
.put_object_stream(&request_json, byte_stream(&payload, MIB))
.await
.unwrap_or_else(|e| panic!("[{label}] streaming upload failed: {e}"));
let mut download = executor
.get_object_stream(&request_json)
.await
.unwrap_or_else(|e| panic!("[{label}] streaming download failed: {e}"));
let mut got: Vec<u8> = Vec::with_capacity(*len);
while let Some(chunk) = download.next().await {
got.extend_from_slice(
&chunk.unwrap_or_else(|e| panic!("[{label}] download chunk error: {e}")),
);
}
assert_eq!(got.len(), payload.len(), "[{label}] round-trip byte count");
assert!(got == payload, "[{label}] round-trip content matches");
let _ = executor.delete_object(&request_json).await;
}
let _ = executor.drop_resource(&bucket).await;
}