use anyhow::{Context, Result};
use tracing::info;
use aws_sdk_s3::types::TransitionDefaultMinimumObjectSize;
use s3util_rs::config::ClientConfig;
use s3util_rs::config::args::put_bucket_lifecycle_configuration::PutBucketLifecycleConfigurationArgs;
use s3util_rs::input::json::LifecycleConfigurationJson;
use s3util_rs::storage::s3::api;
pub async fn run_put_bucket_lifecycle_configuration(
args: PutBucketLifecycleConfigurationArgs,
client_config: ClientConfig,
) -> Result<()> {
let bucket = args
.bucket_name()
.map_err(|e| anyhow::anyhow!("{}", e.trim_end()))?;
let config_arg = args
.lifecycle_configuration
.as_deref()
.ok_or_else(|| anyhow::anyhow!("lifecycle-configuration source required"))?;
let body = if config_arg == "-" {
let mut buf = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
buf
} else {
std::fs::read_to_string(config_arg)
.with_context(|| format!("reading lifecycle configuration from {config_arg}"))?
};
let parsed = parse_lifecycle_configuration(&body)
.with_context(|| format!("parsing JSON from {config_arg}"))?;
let cfg = parsed.into_sdk()?;
let client = client_config.create_client().await;
if args.dry_run {
info!(bucket = %bucket, "[dry-run] would put bucket lifecycle configuration.");
return Ok(());
}
let transition_default_minimum_object_size = args
.transition_default_minimum_object_size
.as_deref()
.map(TransitionDefaultMinimumObjectSize::from);
api::put_bucket_lifecycle_configuration(
&client,
&bucket,
cfg,
transition_default_minimum_object_size,
)
.await?;
info!(bucket = %bucket, "Bucket lifecycle configuration set.");
Ok(())
}
fn parse_lifecycle_configuration(body: &str) -> Result<LifecycleConfigurationJson> {
serde_json::from_str(body).map_err(|e| {
if e.to_string()
.contains("unknown field `TransitionDefaultMinimumObjectSize`")
{
anyhow::anyhow!(e).context(
"`TransitionDefaultMinimumObjectSize` is a request parameter, not part of the \
lifecycle configuration document: remove it from the JSON and pass it with \
--transition-default-minimum-object-size instead",
)
} else {
anyhow::anyhow!(e)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_rules_document_parses() {
let body = r#"{"Rules": [{"ID": "r1", "Status": "Enabled",
"Expiration": {"Days": 30}, "Filter": {"Prefix": ""}}]}"#;
assert!(parse_lifecycle_configuration(body).is_ok());
}
#[test]
fn get_output_fed_back_into_put_hints_at_the_flag() {
let body = r#"{"Rules": [{"ID": "r1", "Status": "Enabled",
"Expiration": {"Days": 30}, "Filter": {"Prefix": ""}}],
"TransitionDefaultMinimumObjectSize": "all_storage_classes_128K"}"#;
let err = parse_lifecycle_configuration(body).unwrap_err();
assert!(
format!("{err:#}").contains("--transition-default-minimum-object-size"),
"error must point at the flag; got: {err:#}"
);
}
#[test]
fn other_unknown_fields_do_not_get_the_hint() {
let body = r#"{"Rules": [], "Bogus": 1}"#;
let err = parse_lifecycle_configuration(body).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("unknown field `Bogus`"), "got: {msg}");
assert!(
!msg.contains("--transition-default-minimum-object-size"),
"hint must only fire for TransitionDefaultMinimumObjectSize; got: {msg}"
);
}
}