toolcraft_s3_kit/
client.rs1use 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
10pub 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
20impl 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
41impl 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
115impl 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}