1use super::aws_credentials::CredentialsProvider;
2use super::aws_sigv4;
3use super::{Storage, StorageError};
4use crate::http_client::{Client, Response};
5use std::env;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8#[derive(Clone, Debug)]
10pub struct S3Config {
11 pub bucket: String,
12 pub region: String,
13 pub access_key: String,
17 pub secret_key: String,
20 pub endpoint: String,
24}
25
26impl S3Config {
27 pub fn from_env() -> Result<Self, StorageError> {
39 let bucket = env::var("RWS_S3_BUCKET")
40 .map_err(|_| StorageError::new("RWS_S3_BUCKET environment variable is required"))?;
41 let region = env::var("RWS_S3_REGION").unwrap_or_else(|_| "us-east-1".to_string());
42 let access_key = env::var("RWS_S3_ACCESS_KEY").unwrap_or_default();
43 let secret_key = env::var("RWS_S3_SECRET_KEY").unwrap_or_default();
44 let endpoint = env::var("RWS_S3_ENDPOINT")
45 .unwrap_or_else(|_| format!("https://s3.{region}.amazonaws.com"));
46 Ok(S3Config { bucket, region, access_key, secret_key, endpoint })
47 }
48}
49
50pub struct S3Storage {
63 config: S3Config,
64 client: Client,
65 credentials: CredentialsProvider,
66}
67
68impl S3Storage {
69 pub fn new(config: S3Config) -> Self {
70 let access_key = non_empty(&config.access_key);
71 let secret_key = non_empty(&config.secret_key);
72 let credentials = CredentialsProvider::detect(&config.region, access_key, secret_key);
73 S3Storage { config, client: Client::new(), credentials }
74 }
75
76 pub fn with_ca_file(mut self, path: &str) -> Result<Self, StorageError> {
80 self.client = Client::new().ca_file(path).map_err(|e| StorageError::new(e.to_string()))?;
81 Ok(self)
82 }
83
84 pub fn from_env() -> Result<Self, StorageError> {
87 let storage = S3Storage::new(S3Config::from_env()?);
88 match env::var("RWS_S3_CA_FILE").ok().filter(|v| !v.trim().is_empty()) {
89 Some(path) => storage.with_ca_file(path.trim()),
90 None => Ok(storage),
91 }
92 }
93
94 fn host(&self) -> String {
100 let endpoint = &self.config.endpoint;
101 let (default_port, rest) = match (endpoint.strip_prefix("https://"), endpoint.strip_prefix("http://")) {
102 (Some(rest), _) => (":443", rest),
103 (None, Some(rest)) => (":80", rest),
104 (None, None) => (":443", endpoint.as_str()),
105 };
106 let authority = rest.split('/').next().unwrap_or(rest);
107 authority.strip_suffix(default_port).unwrap_or(authority).to_string()
108 }
109
110 fn canonical_path(&self, key: &str) -> String {
113 aws_sigv4::uri_encode_path(&format!("/{}/{}", self.config.bucket, key))
114 }
115
116 fn request_url(&self, key: &str) -> String {
117 format!("{}{}", self.config.endpoint.trim_end_matches('/'), self.canonical_path(key))
118 }
119
120 fn signed_headers(&self, method: &str, key: &str, payload: &[u8]) -> Result<Vec<(String, String)>, StorageError> {
121 let creds = self.credentials.get()?;
122 let epoch_secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
123 Ok(aws_sigv4::sign(
124 method,
125 &self.host(),
126 &self.canonical_path(key),
127 payload,
128 &self.config.region,
129 &creds.access_key,
130 &creds.secret_key,
131 creds.session_token.as_deref(),
132 epoch_secs,
133 "s3",
134 ))
135 }
136
137 fn error_from_response(action: &str, key: &str, resp: &Response) -> StorageError {
138 StorageError::new(format!(
139 "S3 {action} '{key}' failed: HTTP {} {}",
140 resp.status(),
141 resp.text().unwrap_or_default()
142 ))
143 }
144}
145
146impl Storage for S3Storage {
147 fn put(&self, key: &str, data: &[u8], content_type: &str) -> Result<String, StorageError> {
148 let url = self.request_url(key);
149 let mut builder = self.client.put(&url).header("Content-Type", content_type);
150 for (name, value) in self.signed_headers("PUT", key, data)? {
151 if name.eq_ignore_ascii_case("host") {
154 continue;
155 }
156 builder = builder.header(&name, &value);
157 }
158 let resp = builder
159 .body(data.to_vec())
160 .send()
161 .map_err(|e| StorageError::new(format!("S3 PUT '{key}' failed: {e}")))?;
162 if !resp.is_success() {
163 return Err(Self::error_from_response("PUT", key, &resp));
164 }
165 Ok(key.to_string())
166 }
167
168 fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
169 let url = self.request_url(key);
170 let mut builder = self.client.get(&url);
171 for (name, value) in self.signed_headers("GET", key, b"")? {
172 if name.eq_ignore_ascii_case("host") {
173 continue;
174 }
175 builder = builder.header(&name, &value);
176 }
177 let resp = builder.send().map_err(|e| StorageError::new(format!("S3 GET '{key}' failed: {e}")))?;
178 if !resp.is_success() {
179 return Err(Self::error_from_response("GET", key, &resp));
180 }
181 Ok(resp.bytes().to_vec())
182 }
183
184 fn delete(&self, key: &str) -> Result<(), StorageError> {
185 let url = self.request_url(key);
186 let mut builder = self.client.delete(&url);
187 for (name, value) in self.signed_headers("DELETE", key, b"")? {
188 if name.eq_ignore_ascii_case("host") {
189 continue;
190 }
191 builder = builder.header(&name, &value);
192 }
193 let resp = builder.send().map_err(|e| StorageError::new(format!("S3 DELETE '{key}' failed: {e}")))?;
194 if !resp.is_success() {
197 return Err(Self::error_from_response("DELETE", key, &resp));
198 }
199 Ok(())
200 }
201
202 fn url(&self, key: &str) -> String {
203 self.request_url(key)
204 }
205
206 fn put_stream(&self, key: &str, reader: &mut dyn std::io::Read, content_type: &str) -> Result<String, StorageError> {
211 let mut part = read_part(reader, key)?;
212 if part.len() < S3_PART_SIZE {
213 return self.put(key, &part, content_type);
214 }
215 let upload_id = self.create_multipart_upload(key, content_type)?;
216 let mut etags: Vec<String> = Vec::new();
217 let uploaded = (|| -> Result<(), StorageError> {
218 loop {
219 let number = (etags.len() + 1).to_string();
220 etags.push(self.upload_part(key, &upload_id, &number, &part)?);
221 part = read_part(reader, key)?;
222 if part.is_empty() {
223 return Ok(());
224 }
225 }
226 })()
227 .and_then(|_| self.complete_multipart_upload(key, &upload_id, &etags));
228 if let Err(e) = uploaded {
229 let _ = self.abort_multipart_upload(key, &upload_id);
230 return Err(e);
231 }
232 Ok(key.to_string())
233 }
234}
235
236pub const S3_PART_SIZE: usize = 8 * 1024 * 1024;
240
241fn read_part(reader: &mut dyn std::io::Read, key: &str) -> Result<Vec<u8>, StorageError> {
243 let mut part = vec![0u8; S3_PART_SIZE];
244 let mut filled = 0;
245 while filled < part.len() {
246 match reader.read(&mut part[filled..]) {
247 Ok(0) => break,
248 Ok(n) => filled += n,
249 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
250 Err(e) => return Err(StorageError::new(format!("failed to read the data for '{key}': {e}"))),
251 }
252 }
253 part.truncate(filled);
254 Ok(part)
255}
256
257impl S3Storage {
258 fn send_with_query(&self, method: &str, key: &str, params: &[(&str, &str)], body: Vec<u8>, content_type: Option<&str>) -> Result<Response, StorageError> {
260 let query = aws_sigv4::canonical_query(params);
261 let url = format!("{}?{}", self.request_url(key), query);
262 let creds = self.credentials.get()?;
263 let epoch_secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
264 let headers = aws_sigv4::sign_with_query(
265 method, &self.host(), &self.canonical_path(key), params, &body, &self.config.region,
266 &creds.access_key, &creds.secret_key, creds.session_token.as_deref(), epoch_secs, "s3",
267 );
268 let mut builder = self.client.request(method, &url);
269 if let Some(content_type) = content_type {
270 builder = builder.header("Content-Type", content_type);
271 }
272 for (name, value) in headers {
273 if !name.eq_ignore_ascii_case("host") {
274 builder = builder.header(&name, &value);
275 }
276 }
277 builder.body(body).send().map_err(|e| StorageError::new(format!("S3 {method} '{key}' failed: {e}")))
278 }
279
280 fn create_multipart_upload(&self, key: &str, content_type: &str) -> Result<String, StorageError> {
281 let resp = self.send_with_query("POST", key, &[("uploads", "")], Vec::new(), Some(content_type))?;
282 if !resp.is_success() {
283 return Err(Self::error_from_response("CreateMultipartUpload", key, &resp));
284 }
285 xml_element(&resp.text().unwrap_or_default(), "UploadId")
286 .ok_or_else(|| StorageError::new(format!("S3 CreateMultipartUpload '{key}': no UploadId in the response")))
287 }
288
289 fn upload_part(&self, key: &str, upload_id: &str, number: &str, data: &[u8]) -> Result<String, StorageError> {
290 let resp = self.send_with_query("PUT", key, &[("partNumber", number), ("uploadId", upload_id)], data.to_vec(), None)?;
291 if !resp.is_success() {
292 return Err(Self::error_from_response("UploadPart", key, &resp));
293 }
294 resp.header("etag").map(str::to_string)
295 .ok_or_else(|| StorageError::new(format!("S3 UploadPart '{key}' part {number}: no ETag in the response")))
296 }
297
298 fn complete_multipart_upload(&self, key: &str, upload_id: &str, etags: &[String]) -> Result<(), StorageError> {
299 let mut body = String::from("<CompleteMultipartUpload>");
300 for (i, etag) in etags.iter().enumerate() {
301 body.push_str(&format!("<Part><PartNumber>{}</PartNumber><ETag>{}</ETag></Part>", i + 1, xml_escape(etag)));
302 }
303 body.push_str("</CompleteMultipartUpload>");
304 let resp = self.send_with_query("POST", key, &[("uploadId", upload_id)], body.into_bytes(), Some("application/xml"))?;
305 let text = resp.text().unwrap_or_default();
307 if !resp.is_success() || text.contains("<Error>") {
308 return Err(StorageError::new(format!("S3 CompleteMultipartUpload '{key}' failed: HTTP {} {text}", resp.status())));
309 }
310 Ok(())
311 }
312
313 fn abort_multipart_upload(&self, key: &str, upload_id: &str) -> Result<(), StorageError> {
314 let resp = self.send_with_query("DELETE", key, &[("uploadId", upload_id)], Vec::new(), None)?;
315 if !resp.is_success() {
316 return Err(Self::error_from_response("AbortMultipartUpload", key, &resp));
317 }
318 Ok(())
319 }
320}
321
322fn xml_element(xml: &str, name: &str) -> Option<String> {
324 let open = format!("<{name}>");
325 let start = xml.find(&open)? + open.len();
326 let end = xml[start..].find(&format!("</{name}>"))?;
327 Some(xml[start..start + end].to_string())
328}
329
330fn xml_escape(text: &str) -> String {
331 text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
332}
333
334fn non_empty(s: &str) -> Option<String> {
335 if s.is_empty() { None } else { Some(s.to_string()) }
336}
337
338#[cfg(test)]
339mod tests;