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 Entry {
17 pub(crate) key: String,
18 last_modified: String,
19 pub(crate) size: u64,
20}
21
22impl Entry {
23 pub(crate) fn age(&self) -> Option<Duration> {
27 let written = time::OffsetDateTime::parse(
28 &self.last_modified,
29 &time::format_description::well_known::Rfc3339,
30 )
31 .ok()?;
32
33 Duration::try_from(time::OffsetDateTime::now_utc() - written).ok()
34 }
35}
36
37pub struct Presigned {
40 pub href: String,
41 pub headers: Vec<(String, String)>,
42}
43
44async fn read_retrying(request: reqwest::RequestBuilder) -> Result<reqwest::Response, Error> {
59 let retry = request.try_clone();
60
61 match request.send().await {
62 Ok(response) => Ok(response),
63 Err(_) => match retry {
64 Some(retry) => retry.send().await.map_err(|_| unreachable_store()),
65 None => Err(unreachable_store()),
66 },
67 }
68}
69
70fn unreachable_store() -> Error {
71 Error::Storage(std::io::Error::other("the object store is unreachable"))
72}
73
74#[derive(Clone)]
80pub struct Keyspace {
81 bucket: Bucket,
82 credentials: Credentials,
83 client: reqwest::Client,
84 lifetime: Duration,
85}
86
87impl Keyspace {
88 pub fn new(config: &S3Config) -> Result<Self, Error> {
89 crate::tls::install_crypto_provider();
90
91 let style = if config.path_style {
92 UrlStyle::Path
93 } else {
94 UrlStyle::VirtualHost
95 };
96
97 let bucket = Bucket::new(
98 config
99 .endpoint
100 .parse()
101 .map_err(|_| Error::Misconfigured("LFSX_S3_ENDPOINT is not a URL"))?,
102 style,
103 config.bucket.clone(),
104 config.region.clone(),
105 )
106 .map_err(|_| Error::Misconfigured("LFSX_S3_BUCKET is not a usable bucket name"))?;
107
108 Ok(Self {
109 bucket,
110 credentials: Credentials::new(config.access_key.clone(), config.secret_key.clone()),
111 client: reqwest::Client::new(),
112 lifetime: config.lifetime,
113 })
114 }
115
116 pub(crate) async fn reachable(&self) -> Result<(), Error> {
120 let action = HeadBucket::new(&self.bucket, Some(&self.credentials));
121 let response = read_retrying(self.client.head(action.sign(self.lifetime))).await?;
122
123 if !response.status().is_success() {
124 return Err(Error::Storage(std::io::Error::other(format!(
125 "the object store answered {} for the bucket",
126 response.status()
127 ))));
128 }
129
130 Ok(())
131 }
132
133 pub(crate) fn signed_download(&self, key: &str) -> String {
137 GetObject::new(&self.bucket, Some(&self.credentials), key)
138 .sign(self.lifetime)
139 .to_string()
140 }
141
142 pub(crate) fn signed_upload(&self, key: &str, headers: Vec<(String, String)>) -> Presigned {
146 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), key);
147
148 for (name, value) in &headers {
149 action
150 .headers_mut()
151 .insert(name.clone(), std::borrow::Cow::Owned(value.clone()));
152 }
153
154 Presigned {
155 href: action.sign(self.lifetime).to_string(),
156 headers,
157 }
158 }
159
160 pub(crate) async fn get_range(
164 &self,
165 key: &str,
166 start: u64,
167 length: u64,
168 ) -> Result<impl Stream<Item = Result<Bytes, reqwest::Error>> + use<>, Error> {
169 let action = GetObject::new(&self.bucket, Some(&self.credentials), key);
170
171 let response = self
172 .client
173 .get(action.sign(self.lifetime))
174 .header(
175 reqwest::header::RANGE,
176 format!("bytes={start}-{}", start + length.saturating_sub(1)),
177 )
178 .send()
179 .await
180 .map_err(|_| unreachable_store())?;
181
182 if !response.status().is_success() {
183 return Err(Error::NotFound);
184 }
185
186 Ok(response.bytes_stream())
187 }
188
189 pub(crate) async fn head(&self, key: &str) -> Result<u64, Error> {
194 let action = HeadObject::new(&self.bucket, Some(&self.credentials), key);
195 let url = action.sign(self.lifetime);
196
197 let response = read_retrying(self.client.head(url)).await?;
198
199 if !response.status().is_success() {
200 return Err(Error::NotFound);
201 }
202
203 response
207 .headers()
208 .get(reqwest::header::CONTENT_LENGTH)
209 .and_then(|value| value.to_str().ok())
210 .and_then(|value| value.parse().ok())
211 .ok_or_else(|| {
212 Error::Storage(std::io::Error::other(
213 "the object store gave no object size",
214 ))
215 })
216 }
217
218 pub(crate) async fn put(
219 &self,
220 key: &str,
221 body: reqwest::Body,
222 length: u64,
223 ) -> Result<(), Error> {
224 let action = PutObject::new(&self.bucket, Some(&self.credentials), key);
225 let url = action.sign(self.lifetime);
226
227 let response = self
228 .client
229 .put(url)
230 .header(reqwest::header::CONTENT_LENGTH, length)
234 .body(body)
235 .send()
236 .await
237 .map_err(|_| {
238 Error::Storage(std::io::Error::other("the object store is unreachable"))
239 })?;
240
241 let status = response.status();
242 if !status.is_success() {
243 let detail = response.text().await.unwrap_or_default();
248
249 return Err(Error::Storage(std::io::Error::other(format!(
250 "the object store refused a write with {status}: {}",
251 detail.trim()
252 ))));
253 }
254
255 Ok(())
256 }
257
258 pub(crate) async fn copy(&self, from: &str, to: &str) -> Result<(), Error> {
262 let source = format!("/{}/{from}", self.bucket.name());
263 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), to);
264 action
265 .headers_mut()
266 .insert(COPY_SOURCE, std::borrow::Cow::Owned(source.clone()));
267
268 let response = self
269 .client
270 .put(action.sign(self.lifetime))
271 .header(COPY_SOURCE, source)
272 .header(reqwest::header::CONTENT_LENGTH, 0)
273 .send()
274 .await
275 .map_err(|_| unreachable_store())?;
276
277 self.expect_success(response, "copy").await?;
278
279 Ok(())
280 }
281
282 pub(crate) async fn put_if_absent(&self, key: &str, body: Vec<u8>) -> Result<bool, Error> {
290 let mut action = PutObject::new(&self.bucket, Some(&self.credentials), key);
291 action.headers_mut().insert("if-none-match", "*");
292 let url = action.sign(self.lifetime);
293
294 let length = body.len();
295 let response = self
296 .client
297 .put(url)
298 .header("if-none-match", "*")
299 .header(reqwest::header::CONTENT_LENGTH, length)
300 .body(body)
301 .send()
302 .await
303 .map_err(|_| unreachable_store())?;
304
305 if response.status() == reqwest::StatusCode::PRECONDITION_FAILED {
306 return Ok(false);
307 }
308
309 self.expect_success(response, "write").await?;
310
311 Ok(true)
312 }
313
314 pub(crate) async fn get_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, Error> {
315 let action = GetObject::new(&self.bucket, Some(&self.credentials), key);
316 let response = read_retrying(self.client.get(action.sign(self.lifetime))).await?;
317
318 if response.status() == reqwest::StatusCode::NOT_FOUND {
319 return Ok(None);
320 }
321
322 let response = self.expect_success(response, "read").await?;
323
324 response
325 .bytes()
326 .await
327 .map(|bytes| Some(bytes.to_vec()))
328 .map_err(|_| unreachable_store())
329 }
330
331 pub(crate) async fn delete(&self, key: &str) -> Result<bool, Error> {
332 let existed = self.head(key).await.is_ok();
335
336 let action = DeleteObject::new(&self.bucket, Some(&self.credentials), key);
337 let response = self
338 .client
339 .delete(action.sign(self.lifetime))
340 .send()
341 .await
342 .map_err(|_| unreachable_store())?;
343
344 self.expect_success(response, "delete").await?;
345
346 Ok(existed)
347 }
348
349 pub(crate) async fn keys(&self, prefix: &str) -> Result<Vec<String>, Error> {
354 Ok(self
355 .entries(prefix)
356 .await?
357 .into_iter()
358 .map(|entry| entry.key)
359 .collect())
360 }
361
362 pub(crate) async fn entries(&self, prefix: &str) -> Result<Vec<Entry>, Error> {
363 let mut out = Vec::new();
364 let mut token: Option<String> = None;
365
366 loop {
367 let mut action = ListObjectsV2::new(&self.bucket, Some(&self.credentials));
368 action.with_prefix(prefix);
369 if let Some(token) = &token {
370 action.with_continuation_token(token);
371 }
372
373 let response = read_retrying(self.client.get(action.sign(self.lifetime))).await?;
374 let body = self
375 .expect_success(response, "list")
376 .await?
377 .text()
378 .await
379 .map_err(|_| unreachable_store())?;
380
381 let listing = ListObjectsV2::parse_response(&body).map_err(|error| {
382 Error::Storage(std::io::Error::other(format!(
383 "the object store sent a listing this server could not read: {error}"
384 )))
385 })?;
386
387 out.extend(listing.contents.into_iter().map(|object| Entry {
388 key: object.key,
389 last_modified: object.last_modified,
390 size: object.size,
391 }));
392
393 match listing.next_continuation_token {
394 Some(next) => token = Some(next),
395 None => break,
396 }
397 }
398
399 Ok(out)
400 }
401
402 async fn expect_success(
403 &self,
404 response: reqwest::Response,
405 what: &str,
406 ) -> Result<reqwest::Response, Error> {
407 let status = response.status();
408 if status.is_success() {
409 return Ok(response);
410 }
411
412 let detail = response.text().await.unwrap_or_default();
413
414 Err(Error::Storage(std::io::Error::other(format!(
415 "the object store refused a {what} with {status}: {}",
416 detail.trim()
417 ))))
418 }
419}