Skip to main content

toolcraft_s3_kit/
client.rs

1use toolcraft_request::{HeaderMap, Request};
2use toolcraft_utils::{DEFAULT_REGION, sign_request};
3use url::Url;
4
5use crate::{
6    error::Result,
7    util::{check_status, parse_bucket_names},
8};
9
10// ── Types ─────────────────────────────────────────────────────────────────────
11
12pub struct S3Client {
13    pub(crate) access_key: String,
14    pub(crate) secret_key: String,
15    pub(crate) base_url: Url,
16    pub(crate) region: String,
17    pub(crate) http: Request,
18}
19
20// ── Init ──────────────────────────────────────────────────────────────────────
21
22impl S3Client {
23    pub fn new(
24        endpoint: &str,
25        access_key: &str,
26        secret_key: &str,
27        region: Option<&str>,
28    ) -> Result<Self> {
29        let base_url = Url::parse(endpoint)?;
30        let http = Request::new()?;
31        Ok(Self {
32            access_key: access_key.to_string(),
33            secret_key: secret_key.to_string(),
34            base_url,
35            region: region.unwrap_or(DEFAULT_REGION).to_string(),
36            http,
37        })
38    }
39}
40
41// ── Bucket management ─────────────────────────────────────────────────────────
42
43impl S3Client {
44    pub async fn create_bucket(&self, bucket: &str) -> Result<()> {
45        let path = format!("/{bucket}");
46        let auth = sign_request(
47            "PUT",
48            &self.access_key,
49            &self.secret_key,
50            &self.host(),
51            &path,
52            "",
53            Some(&self.region),
54        );
55
56        let body = if self.region != "us-east-1" {
57            format!(
58                "<CreateBucketConfiguration><LocationConstraint>{}</LocationConstraint></\
59                 CreateBucketConfiguration>",
60                self.region,
61            )
62        } else {
63            String::new()
64        };
65
66        let resp = self
67            .http
68            .put_bytes(&self.url(&path), body, Some(self.signed_headers(&auth)?))
69            .await?;
70
71        check_status(resp).await.map(|_| ())
72    }
73
74    pub async fn delete_bucket(&self, bucket: &str) -> Result<()> {
75        let path = format!("/{bucket}");
76        let auth = sign_request(
77            "DELETE",
78            &self.access_key,
79            &self.secret_key,
80            &self.host(),
81            &path,
82            "",
83            Some(&self.region),
84        );
85
86        let resp = self
87            .http
88            .delete(&self.url(&path), Some(self.signed_headers(&auth)?))
89            .await?;
90
91        check_status(resp).await.map(|_| ())
92    }
93
94    pub async fn list_buckets(&self) -> Result<Vec<String>> {
95        let auth = sign_request(
96            "GET",
97            &self.access_key,
98            &self.secret_key,
99            &self.host(),
100            "/",
101            "",
102            Some(&self.region),
103        );
104
105        let resp = self
106            .http
107            .get(&self.url("/"), None, Some(self.signed_headers(&auth)?))
108            .await?;
109
110        let xml = check_status(resp).await?.text().await?;
111        parse_bucket_names(&xml)
112    }
113}
114
115// ── Private helpers ───────────────────────────────────────────────────────────
116
117impl S3Client {
118    pub(crate) fn host(&self) -> String {
119        let host = self.base_url.host_str().unwrap_or_default();
120        match self.base_url.port() {
121            Some(port) => format!("{host}:{port}"),
122            None => host.to_string(),
123        }
124    }
125
126    pub(crate) fn url(&self, path: &str) -> String {
127        format!("{}://{}{}", self.base_url.scheme(), self.host(), path)
128    }
129
130    pub(crate) fn signed_headers(
131        &self,
132        auth: &toolcraft_utils::S3AuthHeaders,
133    ) -> Result<HeaderMap> {
134        let mut headers = HeaderMap::new();
135        headers.insert("host", self.host())?;
136        headers.insert("x-amz-date", auth.x_amz_date.clone())?;
137        headers.insert("x-amz-content-sha256", auth.x_amz_content_sha256.clone())?;
138        headers.insert("authorization", auth.authorization.clone())?;
139        Ok(headers)
140    }
141}