toolcraft_s3_kit/
client.rs1use reqwest::Client;
2use toolcraft_utils::{sign_request, DEFAULT_REGION};
3use url::Url;
4
5use crate::error::{Error, Result};
6use crate::util::{check_status, parse_bucket_names};
7
8pub struct S3Client {
11 pub(crate) access_key: String,
12 pub(crate) secret_key: String,
13 pub(crate) base_url: Url,
14 pub(crate) region: String,
15 pub(crate) http: Client,
16}
17
18impl S3Client {
21 pub fn new(
22 endpoint: &str,
23 access_key: &str,
24 secret_key: &str,
25 region: Option<&str>,
26 ) -> Result<Self> {
27 let base_url = Url::parse(endpoint)?;
28 let http = Client::builder()
29 .build()
30 .map_err(|e| Error::Message(e.to_string().into()))?;
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>\
59 <LocationConstraint>{}</LocationConstraint>\
60 </CreateBucketConfiguration>",
61 self.region,
62 )
63 } else {
64 String::new()
65 };
66
67 let resp = self
68 .http
69 .put(self.url(&path))
70 .header("host", self.host())
71 .header("x-amz-date", &auth.x_amz_date)
72 .header("x-amz-content-sha256", &auth.x_amz_content_sha256)
73 .header("authorization", &auth.authorization)
74 .body(body)
75 .send()
76 .await?;
77
78 check_status(resp).await.map(|_| ())
79 }
80
81 pub async fn delete_bucket(&self, bucket: &str) -> Result<()> {
82 let path = format!("/{bucket}");
83 let auth = sign_request(
84 "DELETE",
85 &self.access_key,
86 &self.secret_key,
87 &self.host(),
88 &path,
89 "",
90 Some(&self.region),
91 );
92
93 let resp = self
94 .http
95 .delete(self.url(&path))
96 .header("host", self.host())
97 .header("x-amz-date", &auth.x_amz_date)
98 .header("x-amz-content-sha256", &auth.x_amz_content_sha256)
99 .header("authorization", &auth.authorization)
100 .send()
101 .await?;
102
103 check_status(resp).await.map(|_| ())
104 }
105
106 pub async fn list_buckets(&self) -> Result<Vec<String>> {
107 let auth = sign_request(
108 "GET",
109 &self.access_key,
110 &self.secret_key,
111 &self.host(),
112 "/",
113 "",
114 Some(&self.region),
115 );
116
117 let resp = self
118 .http
119 .get(self.url("/"))
120 .header("host", self.host())
121 .header("x-amz-date", &auth.x_amz_date)
122 .header("x-amz-content-sha256", &auth.x_amz_content_sha256)
123 .header("authorization", &auth.authorization)
124 .send()
125 .await?;
126
127 let xml = check_status(resp).await?.text().await?;
128 parse_bucket_names(&xml)
129 }
130}
131
132impl S3Client {
135 pub(crate) fn host(&self) -> String {
136 let host = self.base_url.host_str().unwrap_or_default();
137 match self.base_url.port() {
138 Some(port) => format!("{host}:{port}"),
139 None => host.to_string(),
140 }
141 }
142
143 pub(crate) fn url(&self, path: &str) -> String {
144 format!("{}://{}{}", self.base_url.scheme(), self.host(), path)
145 }
146}