Skip to main content

spring_batch_rs/tasklet/s3/
mod.rs

1//! # S3 Tasklet
2//!
3//! This module provides tasklets for Amazon S3 file transfer operations (put and get).
4//! It is designed to be similar to Spring Batch's S3 capabilities for batch file transfers.
5//!
6//! ## Features
7//!
8//! - S3 PUT operations (upload local files to S3)
9//! - S3 GET operations (download S3 objects to local files, streaming)
10//! - S3 PUT FOLDER operations (upload entire local folder to an S3 prefix)
11//! - S3 GET FOLDER operations (download all objects under an S3 prefix to a local folder)
12//! - Explicit credential configuration (access key / secret key)
13//! - AWS default credential chain (environment variables, `~/.aws/credentials`, IAM role)
14//! - Custom endpoint URL for S3-compatible services (MinIO, LocalStack)
15//! - Configurable multipart upload chunk size
16//!
17//! ## Examples
18//!
19//! ### S3 PUT Operation
20//!
21//! ```rust,no_run
22//! use spring_batch_rs::tasklet::s3::put::S3PutTaskletBuilder;
23//!
24//! # fn example() -> Result<(), spring_batch_rs::BatchError> {
25//! let tasklet = S3PutTaskletBuilder::new()
26//!     .bucket("my-bucket")
27//!     .key("exports/file.csv")
28//!     .local_file("./output/file.csv")
29//!     .region("eu-west-1")
30//!     .build()?;
31//! # Ok(())
32//! # }
33//! ```
34//!
35//! ### S3 GET Operation
36//!
37//! ```rust,no_run
38//! use spring_batch_rs::tasklet::s3::get::S3GetTaskletBuilder;
39//!
40//! # fn example() -> Result<(), spring_batch_rs::BatchError> {
41//! let tasklet = S3GetTaskletBuilder::new()
42//!     .bucket("my-bucket")
43//!     .key("imports/file.csv")
44//!     .local_file("./input/file.csv")
45//!     .region("eu-west-1")
46//!     .build()?;
47//! # Ok(())
48//! # }
49//! ```
50//!
51//! ### S3 with MinIO (custom endpoint)
52//!
53//! ```rust,no_run
54//! use spring_batch_rs::tasklet::s3::put::S3PutTaskletBuilder;
55//!
56//! # fn example() -> Result<(), spring_batch_rs::BatchError> {
57//! let tasklet = S3PutTaskletBuilder::new()
58//!     .bucket("my-bucket")
59//!     .key("file.csv")
60//!     .local_file("./output/file.csv")
61//!     .endpoint_url("http://localhost:9000")
62//!     .access_key_id("minioadmin")
63//!     .secret_access_key("minioadmin")
64//!     .build()?;
65//! # Ok(())
66//! # }
67//! ```
68
69pub mod get;
70pub mod put;
71
72use crate::BatchError;
73use aws_config::BehaviorVersion;
74use aws_config::meta::region::RegionProviderChain;
75use aws_sdk_s3::config::Builder as S3ConfigBuilder;
76use aws_sdk_s3::config::Credentials;
77
78/// Configuration for connecting to an S3-compatible service.
79///
80/// All fields are optional. When `access_key_id` and `secret_access_key` are both
81/// `None`, the AWS default credential chain is used (environment variables,
82/// `~/.aws/credentials`, IAM instance role).
83///
84/// Set `endpoint_url` to connect to S3-compatible services such as MinIO or LocalStack.
85///
86/// # Examples
87///
88/// ```
89/// use spring_batch_rs::tasklet::s3::S3ClientConfig;
90///
91/// let config = S3ClientConfig {
92///     region: Some("eu-west-1".to_string()),
93///     endpoint_url: None,
94///     access_key_id: None,
95///     secret_access_key: None,
96/// };
97/// assert_eq!(config.region.as_deref(), Some("eu-west-1"));
98/// ```
99#[derive(Debug, Clone, Default)]
100pub struct S3ClientConfig {
101    /// AWS region (e.g. `"eu-west-1"`). Falls back to `AWS_DEFAULT_REGION` env var when `None`.
102    pub region: Option<String>,
103    /// Custom endpoint URL for S3-compatible services (e.g. `"http://localhost:9000"` for MinIO).
104    pub endpoint_url: Option<String>,
105    /// AWS access key ID. Uses default credential chain when `None`.
106    pub access_key_id: Option<String>,
107    /// AWS secret access key. Uses default credential chain when `None`.
108    pub secret_access_key: Option<String>,
109}
110
111/// Builds an [`aws_sdk_s3::Client`] from the given [`S3ClientConfig`].
112///
113/// When both `access_key_id` and `secret_access_key` are set, explicit static
114/// credentials are used. Otherwise the AWS default credential chain applies.
115///
116/// # Errors
117///
118/// Returns [`BatchError::Configuration`] if the AWS SDK configuration cannot be loaded.
119pub(crate) async fn build_s3_client(
120    config: &S3ClientConfig,
121) -> Result<aws_sdk_s3::Client, BatchError> {
122    let region_provider =
123        RegionProviderChain::first_try(config.region.clone().map(aws_sdk_s3::config::Region::new))
124            .or_default_provider();
125
126    let sdk_config = aws_config::defaults(BehaviorVersion::latest())
127        .region(region_provider)
128        .load()
129        .await;
130
131    let mut builder = S3ConfigBuilder::from(&sdk_config);
132
133    if let Some(url) = &config.endpoint_url {
134        builder = builder.endpoint_url(url).force_path_style(true);
135    }
136
137    if let (Some(key_id), Some(secret)) = (&config.access_key_id, &config.secret_access_key) {
138        let creds = Credentials::new(key_id, secret, None, None, "static");
139        builder = builder.credentials_provider(creds);
140    }
141
142    Ok(aws_sdk_s3::Client::from_conf(builder.build()))
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn should_default_to_none_fields() {
151        let config = S3ClientConfig::default();
152        assert!(config.region.is_none(), "region should default to None");
153        assert!(
154            config.endpoint_url.is_none(),
155            "endpoint_url should default to None"
156        );
157        assert!(
158            config.access_key_id.is_none(),
159            "access_key_id should default to None"
160        );
161        assert!(
162            config.secret_access_key.is_none(),
163            "secret_access_key should default to None"
164        );
165    }
166
167    #[test]
168    fn should_store_region() {
169        let config = S3ClientConfig {
170            region: Some("us-east-1".to_string()),
171            ..Default::default()
172        };
173        assert_eq!(config.region.as_deref(), Some("us-east-1"));
174    }
175
176    #[test]
177    fn should_store_endpoint_url() {
178        let config = S3ClientConfig {
179            endpoint_url: Some("http://localhost:9000".to_string()),
180            ..Default::default()
181        };
182        assert_eq!(
183            config.endpoint_url.as_deref(),
184            Some("http://localhost:9000")
185        );
186    }
187
188    #[test]
189    fn should_store_explicit_credentials() {
190        let config = S3ClientConfig {
191            access_key_id: Some("AKID".to_string()),
192            secret_access_key: Some("SECRET".to_string()),
193            ..Default::default()
194        };
195        assert_eq!(config.access_key_id.as_deref(), Some("AKID"));
196        assert_eq!(config.secret_access_key.as_deref(), Some("SECRET"));
197    }
198}