toolcraft_s3_kit/
client.rs1use reqwest::Client;
2use toolcraft_utils::{DEFAULT_REGION, sign_request};
3use url::Url;
4
5use crate::{
6 error::{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: Client,
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 = Client::builder()
31 .build()
32 .map_err(|e| Error::Message(e.to_string().into()))?;
33 Ok(Self {
34 access_key: access_key.to_string(),
35 secret_key: secret_key.to_string(),
36 base_url,
37 region: region.unwrap_or(DEFAULT_REGION).to_string(),
38 http,
39 })
40 }
41}
42
43impl S3Client {
46 pub async fn create_bucket(&self, bucket: &str) -> Result<()> {
47 let path = format!("/{bucket}");
48 let auth = sign_request(
49 "PUT",
50 &self.access_key,
51 &self.secret_key,
52 &self.host(),
53 &path,
54 "",
55 Some(&self.region),
56 );
57
58 let body = if self.region != "us-east-1" {
59 format!(
60 "<CreateBucketConfiguration><LocationConstraint>{}</LocationConstraint></\
61 CreateBucketConfiguration>",
62 self.region,
63 )
64 } else {
65 String::new()
66 };
67
68 let resp = self
69 .http
70 .put(self.url(&path))
71 .header("host", self.host())
72 .header("x-amz-date", &auth.x_amz_date)
73 .header("x-amz-content-sha256", &auth.x_amz_content_sha256)
74 .header("authorization", &auth.authorization)
75 .body(body)
76 .send()
77 .await?;
78
79 check_status(resp).await.map(|_| ())
80 }
81
82 pub async fn delete_bucket(&self, bucket: &str) -> Result<()> {
83 let path = format!("/{bucket}");
84 let auth = sign_request(
85 "DELETE",
86 &self.access_key,
87 &self.secret_key,
88 &self.host(),
89 &path,
90 "",
91 Some(&self.region),
92 );
93
94 let resp = self
95 .http
96 .delete(self.url(&path))
97 .header("host", self.host())
98 .header("x-amz-date", &auth.x_amz_date)
99 .header("x-amz-content-sha256", &auth.x_amz_content_sha256)
100 .header("authorization", &auth.authorization)
101 .send()
102 .await?;
103
104 check_status(resp).await.map(|_| ())
105 }
106
107 pub async fn list_buckets(&self) -> Result<Vec<String>> {
108 let auth = sign_request(
109 "GET",
110 &self.access_key,
111 &self.secret_key,
112 &self.host(),
113 "/",
114 "",
115 Some(&self.region),
116 );
117
118 let resp = self
119 .http
120 .get(self.url("/"))
121 .header("host", self.host())
122 .header("x-amz-date", &auth.x_amz_date)
123 .header("x-amz-content-sha256", &auth.x_amz_content_sha256)
124 .header("authorization", &auth.authorization)
125 .send()
126 .await?;
127
128 let xml = check_status(resp).await?.text().await?;
129 parse_bucket_names(&xml)
130 }
131}
132
133impl S3Client {
136 pub(crate) fn host(&self) -> String {
137 let host = self.base_url.host_str().unwrap_or_default();
138 match self.base_url.port() {
139 Some(port) => format!("{host}:{port}"),
140 None => host.to_string(),
141 }
142 }
143
144 pub(crate) fn url(&self, path: &str) -> String {
145 format!("{}://{}{}", self.base_url.scheme(), self.host(), path)
146 }
147}