1use std::{path::Path, sync::Arc};
2
3use bytes::Bytes;
4use toolcraft_request::HeaderMap;
5use toolcraft_utils::{presign_get_object, presign_put_object, sign_request};
6
7use crate::{
8 client::S3Client,
9 error::Result,
10 util::{ObjectInfo, check_status, parse_object_list, url_encode},
11};
12
13#[derive(Clone)]
23pub struct BucketClient {
24 inner: Arc<S3Client>,
25 bucket: String,
26}
27
28impl BucketClient {
31 pub fn new(client: Arc<S3Client>, bucket: impl Into<String>) -> Self {
32 Self {
33 inner: client,
34 bucket: bucket.into(),
35 }
36 }
37}
38
39impl BucketClient {
42 pub async fn list_objects(&self, prefix: Option<&str>) -> Result<Vec<ObjectInfo>> {
43 let c = &self.inner;
44 let path = format!("/{}", self.bucket);
45 let query = match prefix {
46 Some(p) => format!("list-type=2&prefix={}", url_encode(p)),
47 None => "list-type=2".to_string(),
48 };
49 let auth = sign_request(
50 "GET",
51 &c.access_key,
52 &c.secret_key,
53 &c.host(),
54 &path,
55 &query,
56 Some(&c.region),
57 );
58
59 let resp = c
60 .http
61 .get(
62 &format!("{}?{}", c.url(&path), query),
63 None,
64 Some(c.signed_headers(&auth)?),
65 )
66 .await?;
67
68 let xml = check_status(resp).await?.text().await?;
69 parse_object_list(&xml)
70 }
71
72 pub async fn upload_bytes(
74 &self,
75 key: &str,
76 data: Bytes,
77 content_type: Option<&str>,
78 ) -> Result<()> {
79 let c = &self.inner;
80 let url = presign_put_object(
81 &c.access_key,
82 &c.secret_key,
83 &self.bucket,
84 key,
85 Some(&c.region),
86 c.base_url.as_str(),
87 None,
88 );
89
90 let mut headers = HeaderMap::new();
91 if let Some(ct) = content_type {
92 headers.insert("content-type", ct.to_string())?;
93 }
94 let headers = if headers.inner().is_empty() {
95 None
96 } else {
97 Some(headers)
98 };
99 check_status(c.http.put_bytes(&url, data, headers).await?)
100 .await
101 .map(|_| ())
102 }
103
104 pub async fn upload_local_file<P: AsRef<Path>>(
106 &self,
107 key: &str,
108 local_path: P,
109 content_type: Option<&str>,
110 ) -> Result<u64> {
111 let bytes = tokio::fs::read(local_path.as_ref()).await?;
112 let size = bytes.len() as u64;
113 self.upload_bytes(key, Bytes::from(bytes), content_type)
114 .await?;
115 Ok(size)
116 }
117
118 pub async fn upload_file(
120 &self,
121 key: &str,
122 data: Bytes,
123 content_type: Option<&str>,
124 ) -> Result<()> {
125 self.upload_bytes(key, data, content_type).await
126 }
127
128 pub async fn download_object(&self, key: &str) -> Result<Bytes> {
129 let c = &self.inner;
130 let url = presign_get_object(
131 &c.access_key,
132 &c.secret_key,
133 &self.bucket,
134 key,
135 Some(&c.region),
136 c.base_url.as_str(),
137 None,
138 );
139
140 let resp = check_status(c.http.get(&url, None, None).await?).await?;
141 Ok(resp.bytes().await?)
142 }
143
144 pub async fn delete_object(&self, key: &str) -> Result<()> {
145 let c = &self.inner;
146 let path = format!("/{}/{}", self.bucket, key.trim_start_matches('/'));
147 let auth = sign_request(
148 "DELETE",
149 &c.access_key,
150 &c.secret_key,
151 &c.host(),
152 &path,
153 "",
154 Some(&c.region),
155 );
156
157 let resp = c
158 .http
159 .delete(&c.url(&path), Some(c.signed_headers(&auth)?))
160 .await?;
161
162 check_status(resp).await.map(|_| ())
163 }
164
165 pub fn presign_upload(&self, key: &str, expires_secs: Option<u64>) -> String {
167 let c = &self.inner;
168 presign_put_object(
169 &c.access_key,
170 &c.secret_key,
171 &self.bucket,
172 key,
173 Some(&c.region),
174 c.base_url.as_str(),
175 expires_secs,
176 )
177 }
178}