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 client(&self) -> &reqwest::Client {
145 &self.client
146 }
147
148 pub(crate) fn signed_download(&self, key: &str) -> String {
151 GetObject::new(&self.bucket, Some(&self.credentials), key)
152 .sign(self.lifetime)
153 .to_string()
154 }
155
156 pub(crate) fn signed_upload(&self, key: &str, headers: Vec<(String, String)>) -> Presigned {
164 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), key);
165
166 for (name, value) in &headers {
167 action
168 .headers_mut()
169 .insert(name.clone(), std::borrow::Cow::Owned(value.clone()));
170 }
171
172 Presigned {
173 href: action.sign(self.lifetime).to_string(),
174 headers,
175 }
176 }
177
178 pub(crate) async fn get_range(
182 &self,
183 key: &str,
184 start: u64,
185 length: u64,
186 ) -> Result<impl Stream<Item = Result<Bytes, reqwest::Error>> + use<>, Error> {
187 let action = GetObject::new(&self.bucket, Some(&self.credentials), key);
188
189 let response = self
190 .client
191 .get(action.sign(self.lifetime))
192 .header(
193 reqwest::header::RANGE,
194 format!("bytes={start}-{}", start + length.saturating_sub(1)),
195 )
196 .send()
197 .await
198 .map_err(|_| unreachable_store())?;
199
200 if !response.status().is_success() {
201 return Err(Error::NotFound);
202 }
203
204 Ok(response.bytes_stream())
205 }
206
207 pub(crate) async fn head(&self, key: &str) -> Result<u64, Error> {
212 let action = HeadObject::new(&self.bucket, Some(&self.credentials), key);
213 let url = action.sign(self.lifetime);
214
215 let response = read_retrying(self.client.head(url)).await?;
216
217 if !response.status().is_success() {
218 return Err(Error::NotFound);
219 }
220
221 response
225 .headers()
226 .get(reqwest::header::CONTENT_LENGTH)
227 .and_then(|value| value.to_str().ok())
228 .and_then(|value| value.parse().ok())
229 .ok_or_else(|| {
230 Error::Storage(std::io::Error::other(
231 "the object store gave no object size",
232 ))
233 })
234 }
235
236 pub(crate) async fn put(
237 &self,
238 key: &str,
239 body: reqwest::Body,
240 length: u64,
241 ) -> Result<(), Error> {
242 let action = PutObject::new(&self.bucket, Some(&self.credentials), key);
243 let url = action.sign(self.lifetime);
244
245 let response = self
246 .client
247 .put(url)
248 .header(reqwest::header::CONTENT_LENGTH, length)
252 .body(body)
253 .send()
254 .await
255 .map_err(|_| {
256 Error::Storage(std::io::Error::other("the object store is unreachable"))
257 })?;
258
259 let status = response.status();
260 if !status.is_success() {
261 let detail = response.text().await.unwrap_or_default();
266
267 return Err(Error::Storage(std::io::Error::other(format!(
268 "the object store refused a write with {status}: {}",
269 detail.trim()
270 ))));
271 }
272
273 Ok(())
274 }
275
276 pub(crate) async fn copy(&self, from: &str, to: &str) -> Result<(), Error> {
280 let source = format!("/{}/{from}", self.bucket.name());
281 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), to);
282 action
283 .headers_mut()
284 .insert(COPY_SOURCE, std::borrow::Cow::Owned(source.clone()));
285
286 let response = self
287 .client
288 .put(action.sign(self.lifetime))
289 .header(COPY_SOURCE, source)
290 .header(reqwest::header::CONTENT_LENGTH, 0)
291 .send()
292 .await
293 .map_err(|_| unreachable_store())?;
294
295 self.expect_success(response, "copy").await?;
296
297 Ok(())
298 }
299
300 pub(crate) async fn put_if_absent(&self, key: &str, body: Vec<u8>) -> Result<bool, Error> {
308 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), key);
309 action.headers_mut().insert("if-none-match", "*");
310 let url = action.sign(self.lifetime);
311
312 let length = body.len();
313 let response = self
314 .client
315 .put(url)
316 .header("if-none-match", "*")
317 .header(reqwest::header::CONTENT_LENGTH, length)
318 .body(body)
319 .send()
320 .await
321 .map_err(|_| unreachable_store())?;
322
323 if response.status() == reqwest::StatusCode::PRECONDITION_FAILED {
324 return Ok(false);
325 }
326
327 self.expect_success(response, "write").await?;
328
329 Ok(true)
330 }
331
332 pub(crate) async fn get_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, Error> {
333 let action = GetObject::new(&self.bucket, Some(&self.credentials), key);
334 let response = read_retrying(self.client.get(action.sign(self.lifetime))).await?;
335
336 if response.status() == reqwest::StatusCode::NOT_FOUND {
337 return Ok(None);
338 }
339
340 let response = self.expect_success(response, "read").await?;
341
342 response
343 .bytes()
344 .await
345 .map(|bytes| Some(bytes.to_vec()))
346 .map_err(|_| unreachable_store())
347 }
348
349 pub(crate) async fn delete(&self, key: &str) -> Result<bool, Error> {
350 let existed = self.head(key).await.is_ok();
353
354 let action = DeleteObject::new(&self.bucket, Some(&self.credentials), key);
355 let response = self
356 .client
357 .delete(action.sign(self.lifetime))
358 .send()
359 .await
360 .map_err(|_| unreachable_store())?;
361
362 self.expect_success(response, "delete").await?;
363
364 Ok(existed)
365 }
366
367 pub(crate) async fn keys(&self, prefix: &str) -> Result<Vec<String>, Error> {
372 Ok(self
373 .entries(prefix)
374 .await?
375 .into_iter()
376 .map(|entry| entry.key)
377 .collect())
378 }
379
380 pub(crate) async fn listing(&self, prefix: &str) -> Listing {
385 match self.entries(prefix).await {
386 Ok(entries) => Listing {
387 entries,
388 complete: true,
389 },
390 Err(error) => {
391 tracing::warn!(%error, prefix, "the listing could not be finished");
392 Listing {
393 entries: Vec::new(),
394 complete: false,
395 }
396 }
397 }
398 }
399
400 pub(crate) async fn entries(&self, prefix: &str) -> Result<Vec<Entry>, Error> {
401 let mut out = Vec::new();
402 let mut token: Option<String> = None;
403
404 loop {
405 let mut action = ListObjectsV2::new(&self.bucket, Some(&self.credentials));
406 action.with_prefix(prefix);
407 if let Some(token) = &token {
408 action.with_continuation_token(token);
409 }
410
411 let response = read_retrying(self.client.get(action.sign(self.lifetime))).await?;
412 let body = self
413 .expect_success(response, "list")
414 .await?
415 .text()
416 .await
417 .map_err(|_| unreachable_store())?;
418
419 let listing = ListObjectsV2::parse_response(&body).map_err(|error| {
420 Error::Storage(std::io::Error::other(format!(
421 "the object store sent a listing this server could not read: {error}"
422 )))
423 })?;
424
425 out.extend(listing.contents.into_iter().map(|object| Entry {
426 key: object.key,
427 last_modified: object.last_modified,
428 size: object.size,
429 }));
430
431 match listing.next_continuation_token {
432 Some(next) => token = Some(next),
433 None => break,
434 }
435 }
436
437 Ok(out)
438 }
439
440 async fn expect_success(
441 &self,
442 response: reqwest::Response,
443 what: &str,
444 ) -> Result<reqwest::Response, Error> {
445 let status = response.status();
446 if status.is_success() {
447 return Ok(response);
448 }
449
450 let detail = response.text().await.unwrap_or_default();
451
452 Err(Error::Storage(std::io::Error::other(format!(
453 "the object store refused a {what} with {status}: {}",
454 detail.trim()
455 ))))
456 }
457}