Skip to main content

toolcraft_s3_kit/
bucket_client.rs

1use std::sync::Arc;
2
3use bytes::Bytes;
4use toolcraft_utils::{presign_get_object, presign_put_object, sign_request};
5
6use crate::client::S3Client;
7use crate::error::Result;
8use crate::util::{check_status, parse_object_list, url_encode, ObjectInfo};
9
10// ── Types ─────────────────────────────────────────────────────────────────────
11
12/// Object operations scoped to a specific bucket.
13///
14/// Constructed from a shared [`S3Client`]:
15/// ```rust,ignore
16/// let client = Arc::new(S3Client::new(endpoint, ak, sk, None)?);
17/// let bucket = BucketClient::new(Arc::clone(&client), "my-bucket");
18/// ```
19#[derive(Clone)]
20pub struct BucketClient {
21    inner: Arc<S3Client>,
22    bucket: String,
23}
24
25// ── Init ──────────────────────────────────────────────────────────────────────
26
27impl BucketClient {
28    pub fn new(client: Arc<S3Client>, bucket: impl Into<String>) -> Self {
29        Self { inner: client, bucket: bucket.into() }
30    }
31}
32
33// ── Object operations ─────────────────────────────────────────────────────────
34
35impl BucketClient {
36    pub async fn list_objects(&self, prefix: Option<&str>) -> Result<Vec<ObjectInfo>> {
37        let c = &self.inner;
38        let path = format!("/{}", self.bucket);
39        let query = match prefix {
40            Some(p) => format!("list-type=2&prefix={}", url_encode(p)),
41            None => "list-type=2".to_string(),
42        };
43        let auth = sign_request(
44            "GET",
45            &c.access_key,
46            &c.secret_key,
47            &c.host(),
48            &path,
49            &query,
50            Some(&c.region),
51        );
52
53        let resp = c
54            .http
55            .get(format!("{}?{}", c.url(&path), query))
56            .header("host", c.host())
57            .header("x-amz-date", &auth.x_amz_date)
58            .header("x-amz-content-sha256", &auth.x_amz_content_sha256)
59            .header("authorization", &auth.authorization)
60            .send()
61            .await?;
62
63        let xml = check_status(resp).await?.text().await?;
64        parse_object_list(&xml)
65    }
66
67    pub async fn upload_file(
68        &self,
69        key: &str,
70        data: Bytes,
71        content_type: Option<&str>,
72    ) -> Result<()> {
73        let c = &self.inner;
74        let url = presign_put_object(
75            &c.access_key,
76            &c.secret_key,
77            &self.bucket,
78            key,
79            Some(&c.region),
80            c.base_url.as_str(),
81            None,
82        );
83
84        let mut req = c.http.put(&url).body(data);
85        if let Some(ct) = content_type {
86            req = req.header("content-type", ct);
87        }
88        check_status(req.send().await?).await.map(|_| ())
89    }
90
91    pub async fn download_object(&self, key: &str) -> Result<Bytes> {
92        let c = &self.inner;
93        let url = presign_get_object(
94            &c.access_key,
95            &c.secret_key,
96            &self.bucket,
97            key,
98            Some(&c.region),
99            c.base_url.as_str(),
100            None,
101        );
102
103        let resp = check_status(c.http.get(&url).send().await?).await?;
104        Ok(resp.bytes().await?)
105    }
106
107    pub async fn delete_object(&self, key: &str) -> Result<()> {
108        let c = &self.inner;
109        let path = format!("/{}/{}", self.bucket, key.trim_start_matches('/'));
110        let auth = sign_request(
111            "DELETE",
112            &c.access_key,
113            &c.secret_key,
114            &c.host(),
115            &path,
116            "",
117            Some(&c.region),
118        );
119
120        let resp = c
121            .http
122            .delete(c.url(&path))
123            .header("host", c.host())
124            .header("x-amz-date", &auth.x_amz_date)
125            .header("x-amz-content-sha256", &auth.x_amz_content_sha256)
126            .header("authorization", &auth.authorization)
127            .send()
128            .await?;
129
130        check_status(resp).await.map(|_| ())
131    }
132
133    /// Generate a presigned PUT URL for direct client-side upload.
134    pub fn presign_upload(&self, key: &str, expires_secs: Option<u64>) -> String {
135        let c = &self.inner;
136        presign_put_object(
137            &c.access_key,
138            &c.secret_key,
139            &self.bucket,
140            key,
141            Some(&c.region),
142            c.base_url.as_str(),
143            expires_secs,
144        )
145    }
146}