1use std::time::Duration;
2
3use axum::body::Bytes;
4use futures_util::Stream;
5use rusty_s3::actions::{
6 DeleteObject, GetObject, HeadBucket, HeadObject, ListObjectsV2, PutObject, S3Action,
7};
8use rusty_s3::{Bucket, Credentials, UrlStyle};
9
10use crate::error::Error;
11use crate::storage::s3::S3Config;
12
13const COPY_SOURCE: &str = "x-amz-copy-source";
14
15pub(crate) struct Listing {
18 pub(crate) entries: Vec<Entry>,
19 pub(crate) complete: bool,
20}
21
22pub(crate) struct Entry {
23 pub(crate) key: String,
24 last_modified: String,
25 pub(crate) size: u64,
26}
27
28impl Entry {
29 pub(crate) fn age(&self) -> Option<Duration> {
33 let written = time::OffsetDateTime::parse(
34 &self.last_modified,
35 &time::format_description::well_known::Rfc3339,
36 )
37 .ok()?;
38
39 Duration::try_from(time::OffsetDateTime::now_utc() - written).ok()
40 }
41}
42
43pub struct Presigned {
46 pub href: String,
47 pub headers: Vec<(String, String)>,
48}
49
50async fn read_retrying(request: reqwest::RequestBuilder) -> Result<reqwest::Response, Error> {
65 let retry = request.try_clone();
66
67 match request.send().await {
68 Ok(response) => Ok(response),
69 Err(_) => match retry {
70 Some(retry) => retry.send().await.map_err(|_| unreachable_store()),
71 None => Err(unreachable_store()),
72 },
73 }
74}
75
76fn unreachable_store() -> Error {
77 Error::Storage(std::io::Error::other("the object store is unreachable"))
78}
79
80#[derive(Clone)]
86pub struct Keyspace {
87 bucket: Bucket,
88 credentials: Credentials,
89 client: reqwest::Client,
90 lifetime: Duration,
91}
92
93impl Keyspace {
94 pub fn new(config: &S3Config) -> Result<Self, Error> {
95 crate::tls::install_crypto_provider();
96
97 let style = if config.path_style {
98 UrlStyle::Path
99 } else {
100 UrlStyle::VirtualHost
101 };
102
103 let bucket = Bucket::new(
104 config
105 .endpoint
106 .parse()
107 .map_err(|_| Error::Misconfigured("LFSX_S3_ENDPOINT is not a URL"))?,
108 style,
109 config.bucket.clone(),
110 config.region.clone(),
111 )
112 .map_err(|_| Error::Misconfigured("LFSX_S3_BUCKET is not a usable bucket name"))?;
113
114 Ok(Self {
115 bucket,
116 credentials: Credentials::new(config.access_key.clone(), config.secret_key.clone()),
117 client: reqwest::Client::new(),
118 lifetime: config.lifetime,
119 })
120 }
121
122 pub(crate) async fn reachable(&self) -> Result<(), Error> {
126 let action = HeadBucket::new(&self.bucket, Some(&self.credentials));
127 let response = read_retrying(self.client.head(action.sign(self.lifetime))).await?;
128
129 if !response.status().is_success() {
130 return Err(Error::Storage(std::io::Error::other(format!(
131 "the object store answered {} for the bucket",
132 response.status()
133 ))));
134 }
135
136 Ok(())
137 }
138
139 pub(crate) fn bucket(&self) -> &Bucket {
149 &self.bucket
150 }
151
152 pub(crate) fn credentials(&self) -> &Credentials {
153 &self.credentials
154 }
155
156 pub(crate) fn lifetime(&self) -> Duration {
157 self.lifetime
158 }
159
160 pub(crate) fn client(&self) -> &reqwest::Client {
161 &self.client
162 }
163
164 pub(crate) fn signed_download(&self, key: &str) -> String {
167 GetObject::new(&self.bucket, Some(&self.credentials), key)
168 .sign(self.lifetime)
169 .to_string()
170 }
171
172 pub(crate) fn signed_upload(&self, key: &str, headers: Vec<(String, String)>) -> Presigned {
180 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), key);
181
182 for (name, value) in &headers {
183 action
184 .headers_mut()
185 .insert(name.clone(), std::borrow::Cow::Owned(value.clone()));
186 }
187
188 Presigned {
189 href: action.sign(self.lifetime).to_string(),
190 headers,
191 }
192 }
193
194 pub(crate) async fn get_range(
198 &self,
199 key: &str,
200 start: u64,
201 length: u64,
202 ) -> Result<impl Stream<Item = Result<Bytes, reqwest::Error>> + use<>, Error> {
203 let action = GetObject::new(&self.bucket, Some(&self.credentials), key);
204
205 let response = self
206 .client
207 .get(action.sign(self.lifetime))
208 .header(
209 reqwest::header::RANGE,
210 format!("bytes={start}-{}", start + length.saturating_sub(1)),
211 )
212 .send()
213 .await
214 .map_err(|_| unreachable_store())?;
215
216 if !response.status().is_success() {
217 return Err(Error::NotFound);
218 }
219
220 Ok(response.bytes_stream())
221 }
222
223 pub(crate) async fn head(&self, key: &str) -> Result<u64, Error> {
228 let action = HeadObject::new(&self.bucket, Some(&self.credentials), key);
229 let url = action.sign(self.lifetime);
230
231 let response = read_retrying(self.client.head(url)).await?;
232
233 if !response.status().is_success() {
234 return Err(Error::NotFound);
235 }
236
237 response
241 .headers()
242 .get(reqwest::header::CONTENT_LENGTH)
243 .and_then(|value| value.to_str().ok())
244 .and_then(|value| value.parse().ok())
245 .ok_or_else(|| {
246 Error::Storage(std::io::Error::other(
247 "the object store gave no object size",
248 ))
249 })
250 }
251
252 pub(crate) async fn put(
253 &self,
254 key: &str,
255 body: reqwest::Body,
256 length: u64,
257 ) -> Result<(), Error> {
258 let action = PutObject::new(&self.bucket, Some(&self.credentials), key);
259 let url = action.sign(self.lifetime);
260
261 let response = self
262 .client
263 .put(url)
264 .header(reqwest::header::CONTENT_LENGTH, length)
268 .body(body)
269 .send()
270 .await
271 .map_err(|_| {
272 Error::Storage(std::io::Error::other("the object store is unreachable"))
273 })?;
274
275 let status = response.status();
276 if !status.is_success() {
277 let detail = response.text().await.unwrap_or_default();
282
283 return Err(Error::Storage(std::io::Error::other(format!(
284 "the object store refused a write with {status}: {}",
285 detail.trim()
286 ))));
287 }
288
289 Ok(())
290 }
291
292 pub(crate) async fn copy(&self, from: &str, to: &str) -> Result<(), Error> {
296 let source = format!("/{}/{from}", self.bucket.name());
297 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), to);
298 action
299 .headers_mut()
300 .insert(COPY_SOURCE, std::borrow::Cow::Owned(source.clone()));
301
302 let response = self
303 .client
304 .put(action.sign(self.lifetime))
305 .header(COPY_SOURCE, source)
306 .header(reqwest::header::CONTENT_LENGTH, 0)
307 .send()
308 .await
309 .map_err(|_| unreachable_store())?;
310
311 self.expect_success(response, "copy").await?;
312
313 Ok(())
314 }
315
316 pub(crate) async fn put_if_absent(&self, key: &str, body: Vec<u8>) -> Result<bool, Error> {
324 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), key);
325 action.headers_mut().insert("if-none-match", "*");
326 let url = action.sign(self.lifetime);
327
328 let length = body.len();
329 let response = self
330 .client
331 .put(url)
332 .header("if-none-match", "*")
333 .header(reqwest::header::CONTENT_LENGTH, length)
334 .body(body)
335 .send()
336 .await
337 .map_err(|_| unreachable_store())?;
338
339 if response.status() == reqwest::StatusCode::PRECONDITION_FAILED {
340 return Ok(false);
341 }
342
343 self.expect_success(response, "write").await?;
344
345 Ok(true)
346 }
347
348 pub(crate) async fn get_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, Error> {
349 let action = GetObject::new(&self.bucket, Some(&self.credentials), key);
350 let response = read_retrying(self.client.get(action.sign(self.lifetime))).await?;
351
352 if response.status() == reqwest::StatusCode::NOT_FOUND {
353 return Ok(None);
354 }
355
356 let response = self.expect_success(response, "read").await?;
357
358 response
359 .bytes()
360 .await
361 .map(|bytes| Some(bytes.to_vec()))
362 .map_err(|_| unreachable_store())
363 }
364
365 pub(crate) async fn delete(&self, key: &str) -> Result<bool, Error> {
366 let existed = self.head(key).await.is_ok();
369
370 let action = DeleteObject::new(&self.bucket, Some(&self.credentials), key);
371 let response = self
372 .client
373 .delete(action.sign(self.lifetime))
374 .send()
375 .await
376 .map_err(|_| unreachable_store())?;
377
378 self.expect_success(response, "delete").await?;
379
380 Ok(existed)
381 }
382
383 pub(crate) async fn keys(&self, prefix: &str) -> Result<Vec<String>, Error> {
388 Ok(self
389 .entries(prefix)
390 .await?
391 .into_iter()
392 .map(|entry| entry.key)
393 .collect())
394 }
395
396 pub(crate) async fn listing(&self, prefix: &str) -> Listing {
401 match self.entries(prefix).await {
402 Ok(entries) => Listing {
403 entries,
404 complete: true,
405 },
406 Err(error) => {
407 tracing::warn!(%error, prefix, "the listing could not be finished");
408 Listing {
409 entries: Vec::new(),
410 complete: false,
411 }
412 }
413 }
414 }
415
416 pub(crate) async fn entries(&self, prefix: &str) -> Result<Vec<Entry>, Error> {
417 let mut out = Vec::new();
418 let mut token: Option<String> = None;
419
420 loop {
421 let mut action = ListObjectsV2::new(&self.bucket, Some(&self.credentials));
422 action.with_prefix(prefix);
423 if let Some(token) = &token {
424 action.with_continuation_token(token);
425 }
426
427 let response = read_retrying(self.client.get(action.sign(self.lifetime))).await?;
428 let body = self
429 .expect_success(response, "list")
430 .await?
431 .text()
432 .await
433 .map_err(|_| unreachable_store())?;
434
435 let listing = ListObjectsV2::parse_response(&body).map_err(|error| {
436 Error::Storage(std::io::Error::other(format!(
437 "the object store sent a listing this server could not read: {error}"
438 )))
439 })?;
440
441 out.extend(listing.contents.into_iter().map(|object| Entry {
442 key: object.key,
443 last_modified: object.last_modified,
444 size: object.size,
445 }));
446
447 match listing.next_continuation_token {
448 Some(next) => token = Some(next),
449 None => break,
450 }
451 }
452
453 Ok(out)
454 }
455
456 pub(crate) async fn expect_success(
457 &self,
458 response: reqwest::Response,
459 what: &str,
460 ) -> Result<reqwest::Response, Error> {
461 let status = response.status();
462 if status.is_success() {
463 return Ok(response);
464 }
465
466 let detail = response.text().await.unwrap_or_default();
467
468 Err(Error::Storage(std::io::Error::other(format!(
469 "the object store refused a {what} with {status}: {}",
470 detail.trim()
471 ))))
472 }
473}