Skip to main content

micromegas_telemetry/
blob_storage.rs

1use anyhow::Result;
2use futures::StreamExt;
3use futures::stream;
4use object_store::prefix::PrefixStore;
5use object_store::{ObjectStore, ObjectStoreExt, PutMode, PutOptions, path::Path};
6use std::sync::Arc;
7
8/// Outcome of a create-only write.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum PutIfAbsent {
11    /// The object did not exist and now holds the supplied bytes.
12    Created,
13    /// The object already existed; it was left untouched and the bytes were discarded.
14    AlreadyExists,
15}
16
17/// Parses an object-store URI into the raw store + root prefix, feeding
18/// `object_store` the process env vars as options (it normalizes the keys).
19/// The single home for the `parse_url_opts(url, env vars)` idiom.
20pub fn parse_object_store_url(uri: &str) -> Result<(Arc<dyn ObjectStore>, Path)> {
21    parse_object_store_url_parsed(&url::Url::parse(uri)?)
22}
23
24/// Like `parse_object_store_url` but takes an already-parsed URL, for callers
25/// that also need the `url::Url` themselves and would otherwise parse it twice.
26pub fn parse_object_store_url_parsed(url: &url::Url) -> Result<(Arc<dyn ObjectStore>, Path)> {
27    let (store, prefix) = object_store::parse_url_opts(url, std::env::vars())?;
28    Ok((Arc::new(store), prefix))
29}
30
31/// A client for interacting with blob storage.
32///
33/// This struct wraps an `ObjectStore` and prefixes all paths with a root path,
34/// providing a convenient way to interact with a specific "folder" within the blob storage.
35#[derive(Debug)]
36pub struct BlobStorage {
37    blob_store: Arc<dyn ObjectStore>,
38}
39
40impl BlobStorage {
41    /// Creates a new `BlobStorage` instance.
42    pub fn new(blob_store: Arc<dyn ObjectStore>, blob_store_root: Path) -> Self {
43        Self {
44            blob_store: Arc::new(PrefixStore::new(blob_store, blob_store_root)),
45        }
46    }
47
48    /// Connects to a blob storage service using the provided URL.
49    pub fn connect(object_store_url: &str) -> Result<Self> {
50        Self::connect_with_layer(object_store_url, |s| s)
51    }
52
53    /// Parses an object store URL into the raw (unwrapped, un-prefixed) store and
54    /// the lake root path, using the same env-var-derived options as `connect` /
55    /// `connect_with_layer`. Exposed so callers that need to apply their own layer
56    /// (e.g. the object cache client) and also need the root prefix (e.g. to key a
57    /// write-time cache warm the same way `PrefixStore` keys a demand read) don't
58    /// have to re-parse the URL or duplicate the env-var option plumbing.
59    pub fn parse_url_opts(object_store_url: &str) -> Result<(Arc<dyn ObjectStore>, Path)> {
60        parse_object_store_url(object_store_url)
61    }
62
63    /// Connects to a blob storage service and applies a layer to the raw store before
64    /// wrapping it in `PrefixStore`. The layer receives the full-bucket store so its
65    /// keys are bucket-relative (including the lake root prefix).
66    pub fn connect_with_layer(
67        object_store_url: &str,
68        layer: impl FnOnce(Arc<dyn ObjectStore>) -> Arc<dyn ObjectStore>,
69    ) -> Result<Self> {
70        let (blob_store, blob_store_root) = Self::parse_url_opts(object_store_url)?;
71        let layered = layer(blob_store);
72        Ok(Self {
73            blob_store: Arc::new(PrefixStore::new(layered, blob_store_root)),
74        })
75    }
76
77    /// Returns a shared reference to the inner `ObjectStore`.
78    pub fn inner(&self) -> Arc<dyn ObjectStore> {
79        self.blob_store.clone()
80    }
81
82    /// Puts a blob into storage at the specified path.
83    pub async fn put(&self, obj_path: &str, buffer: bytes::Bytes) -> Result<()> {
84        self.blob_store
85            .put(&Path::from(obj_path), buffer.into())
86            .await?;
87        Ok(())
88    }
89
90    /// Writes a blob only if the key is absent, leaving an existing object untouched.
91    ///
92    /// The lake's object keys are write-once and content-addressed — the range cache keys
93    /// carry no etag or generation and are never invalidated (see `range_cache`'s module
94    /// docs) — and this is what enforces that: a colliding write is reported, never applied.
95    pub async fn put_if_absent(&self, obj_path: &str, buffer: bytes::Bytes) -> Result<PutIfAbsent> {
96        match self
97            .blob_store
98            .put_opts(
99                &Path::from(obj_path),
100                buffer.into(),
101                PutOptions::from(PutMode::Create),
102            )
103            .await
104        {
105            Ok(_) => Ok(PutIfAbsent::Created),
106            Err(object_store::Error::AlreadyExists { .. }) => Ok(PutIfAbsent::AlreadyExists),
107            Err(object_store::Error::NotImplemented { .. }) => Err(anyhow::anyhow!(
108                "object store does not support conditional put (PutMode::Create), which \
109                 create-only block writes require; if this is an S3-compatible store, check \
110                 that it is not configured with aws_conditional_put=disabled"
111            )),
112            Err(e) => Err(e.into()),
113        }
114    }
115
116    /// Reads a blob from storage at the specified path.
117    pub async fn read_blob(&self, obj_path: &str) -> Result<bytes::Bytes> {
118        let get_result = self.blob_store.get(&Path::from(obj_path)).await?;
119        Ok(get_result.bytes().await?)
120    }
121
122    /// Deletes a blob from storage at the specified path.
123    pub async fn delete(&self, obj_path: &str) -> Result<()> {
124        self.blob_store.delete(&Path::from(obj_path)).await?;
125        Ok(())
126    }
127
128    /// Probes blob storage reachability and credentials by fetching the first
129    /// page of a bucket listing (a single request; does not enumerate the bucket).
130    pub async fn probe(&self) -> anyhow::Result<()> {
131        match self.blob_store.list(None).next().await {
132            Some(Ok(_)) | None => Ok(()),
133            Some(Err(e)) => Err(e.into()),
134        }
135    }
136
137    /// Deletes a batch of blobs from storage.
138    pub async fn delete_batch(&self, objects: &[String]) -> Result<()> {
139        let paths: Vec<_> = objects
140            .iter()
141            .map(|obj_path| Ok(Path::from(obj_path.as_str())))
142            .collect();
143        let path_stream = stream::iter(paths);
144        let mut stream = self.blob_store.delete_stream(Box::pin(path_stream));
145        while let Some(res) = stream.next().await {
146            if let Err(e) = res {
147                match e {
148                    object_store::Error::NotFound { path: _, source: _ } => Ok(()),
149                    ref _other_error => Err(e),
150                }?
151            }
152        }
153        Ok(())
154    }
155}