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