Skip to main content

toolcraft_s3_kit/
config.rs

1use std::{env, sync::Arc};
2
3use crate::{
4    bucket_client::BucketClient,
5    client::S3Client,
6    error::{Error, Result},
7};
8
9/// S3 configuration for building [`S3Client`] and [`BucketClient`].
10///
11/// Supported environment variables:
12/// - `TOOLCRAFT_S3_ENDPOINT` or `S3_ENDPOINT`
13/// - `TOOLCRAFT_S3_ACCESS_KEY` or `S3_ACCESS_KEY`
14/// - `TOOLCRAFT_S3_SECRET_KEY` or `S3_SECRET_KEY`
15/// - `TOOLCRAFT_S3_BUCKET` or `S3_BUCKET`
16/// - `TOOLCRAFT_S3_REGION` or `S3_REGION` (optional)
17#[derive(Debug, Clone)]
18pub struct S3BucketConfig {
19    pub endpoint: String,
20    pub access_key: String,
21    pub secret_key: String,
22    pub bucket: String,
23    pub region: Option<String>,
24}
25
26impl S3BucketConfig {
27    pub fn new(
28        endpoint: impl Into<String>,
29        access_key: impl Into<String>,
30        secret_key: impl Into<String>,
31        bucket: impl Into<String>,
32        region: Option<String>,
33    ) -> Self {
34        Self {
35            endpoint: endpoint.into(),
36            access_key: access_key.into(),
37            secret_key: secret_key.into(),
38            bucket: bucket.into(),
39            region,
40        }
41    }
42
43    /// Load config from environment variables.
44    pub fn from_env() -> Result<Self> {
45        let endpoint = read_required(&["TOOLCRAFT_S3_ENDPOINT", "S3_ENDPOINT"])?;
46        let access_key = read_required(&["TOOLCRAFT_S3_ACCESS_KEY", "S3_ACCESS_KEY"])?;
47        let secret_key = read_required(&["TOOLCRAFT_S3_SECRET_KEY", "S3_SECRET_KEY"])?;
48        let bucket = read_required(&["TOOLCRAFT_S3_BUCKET", "S3_BUCKET"])?;
49        let region = read_optional(&["TOOLCRAFT_S3_REGION", "S3_REGION"]);
50
51        Ok(Self {
52            endpoint,
53            access_key,
54            secret_key,
55            bucket,
56            region,
57        })
58    }
59
60    pub fn build_s3_client(&self) -> Result<S3Client> {
61        S3Client::new(
62            &self.endpoint,
63            &self.access_key,
64            &self.secret_key,
65            self.region.as_deref(),
66        )
67    }
68
69    pub fn build_bucket_client(&self) -> Result<BucketClient> {
70        let s3_client = Arc::new(self.build_s3_client()?);
71        Ok(BucketClient::new(s3_client, self.bucket.clone()))
72    }
73}
74
75fn read_required(names: &[&str]) -> Result<String> {
76    for name in names {
77        if let Ok(value) = env::var(name) {
78            let trimmed = value.trim();
79            if !trimmed.is_empty() {
80                return Ok(trimmed.to_string());
81            }
82        }
83    }
84
85    let joined = names.join(" / ");
86    Err(Error::Message(
87        format!("missing required environment variable: {joined}").into(),
88    ))
89}
90
91fn read_optional(names: &[&str]) -> Option<String> {
92    for name in names {
93        if let Ok(value) = env::var(name) {
94            let trimmed = value.trim();
95            if !trimmed.is_empty() {
96                return Some(trimmed.to_string());
97            }
98        }
99    }
100    None
101}