Skip to main content

pubhubs/servers/
object_store.rs

1//! Storage backend for pubhubs servers
2use std::borrow::Cow;
3
4use anyhow::Context as _;
5use object_store::{ObjectStore as _, ObjectStoreExt as _};
6
7use crate::api;
8use crate::id::Id;
9use crate::servers;
10
11use crate::servers::config::ObjectStoreConfig;
12
13/// Don't use an object store.
14pub struct UseNone;
15
16impl<'a> TryFrom<&'a Option<ObjectStoreConfig>> for UseNone {
17    type Error = anyhow::Error;
18
19    fn try_from(c: &'a Option<ObjectStoreConfig>) -> anyhow::Result<Self> {
20        if c.is_none() {
21            return Ok(UseNone);
22        }
23
24        anyhow::bail!("Object store configured, but this server does not use one.");
25    }
26}
27
28/// A type that can provide an [`object_store::ObjectStore`] implementation.
29pub trait AsObjectStore {
30    type ObjectStoreT: object_store::ObjectStore + ?Sized;
31
32    fn as_object_store(&self) -> &Self::ObjectStoreT;
33}
34
35/// The default object store we use.
36pub struct DefaultObjectStore(Box<object_store::DynObjectStore>);
37
38impl AsObjectStore for DefaultObjectStore {
39    type ObjectStoreT = object_store::DynObjectStore;
40
41    fn as_object_store(&self) -> &Self::ObjectStoreT {
42        &self.0
43    }
44}
45
46impl std::ops::Deref for DefaultObjectStore {
47    type Target = object_store::DynObjectStore;
48
49    fn deref(&self) -> &Self::Target {
50        &*self.0
51    }
52}
53
54impl<'a> TryFrom<&'a Option<ObjectStoreConfig>> for DefaultObjectStore {
55    type Error = anyhow::Error;
56
57    fn try_from(c_maybe: &'a Option<ObjectStoreConfig>) -> anyhow::Result<Self> {
58        // Turn &Option<ObjectStoreConfig> into &ObjectStoreConfig,
59        // by using the default value of ObjectStoreConfig if necessary
60        let c: Cow<'a, ObjectStoreConfig> = match c_maybe {
61            None => Cow::<'a, ObjectStoreConfig>::Owned(Default::default()),
62            Some(c) => Cow::<'a, ObjectStoreConfig>::Borrowed(c),
63        };
64
65        let url = c.url.as_ref();
66
67        let (scheme, path) = object_store::ObjectStoreScheme::parse(url)
68            .with_context(|| format!("could not determine object store type from url {url}"))?;
69
70        // We disabled object_store's built-in reqwest client (see Cargo.toml), so the S3 store has
71        // no HTTP client unless we provide one.  `parse_url_opts` offers no way to inject a
72        // connector and would fail at runtime for `s3://`, so we build the S3 store ourselves and
73        // hand it our awc-based connector.  Other schemes (e.g. `memory://`) need no HTTP client
74        // and keep going through `parse_url_opts`.
75        let store: Box<object_store::DynObjectStore> = match scheme {
76            object_store::ObjectStoreScheme::AmazonS3 => {
77                let mut builder = object_store::aws::AmazonS3Builder::new()
78                    .with_url(url.to_string())
79                    .with_http_connector(crate::misc::awc_http_connector::AwcHttpConnector::new());
80
81                for (key, value) in c.options.iter() {
82                    match key
83                        .to_ascii_lowercase()
84                        .parse::<object_store::aws::AmazonS3ConfigKey>()
85                    {
86                        Ok(config_key) => {
87                            // A client option our awc connector doesn't read back would only
88                            // configure object_store's built-in HTTP client, which we replaced;
89                            // reject it rather than silently ignore an admin's setting.  The honored
90                            // set lives next to the connector that consumes it.
91                            if let object_store::aws::AmazonS3ConfigKey::Client(client_key) =
92                                &config_key
93                            {
94                                // `DefaultContentType` is a client key but not a transport option:
95                                // object_store applies it as a `Content-Type` request header, which
96                                // our connector forwards verbatim, so it works regardless of the HTTP
97                                // client.  Every other client key configures the transport we replaced.
98                                anyhow::ensure!(
99                                    matches!(
100                                        client_key,
101                                        object_store::client::ClientConfigKey::DefaultContentType
102                                    ) || crate::misc::awc_http_connector::ConnectorOptions::honors_client_config_key(
103                                        client_key
104                                    ),
105                                    "object store option {key:?} configures the HTTP client, which \
106                                     is not currently supported by the awc-based S3 connector"
107                                );
108                            }
109                            builder = builder.with_config(config_key, value);
110                        }
111                        // object_store's own parse_url_opts silently ignores unknown keys; we warn
112                        // instead, so a typo in the config surfaces.
113                        Err(_) => {
114                            log::warn!("ignoring unrecognized S3 object store option {key:?}")
115                        }
116                    }
117                }
118
119                Box::new(builder.build().with_context(|| {
120                    format!(
121                        "creating S3 object store from url {url} and options {}",
122                        serde_json::to_string(&c.options)
123                            .unwrap_or_else(|_| "<failed to format>".to_string())
124                    )
125                })?)
126            }
127
128            // Non-S3 object store
129            _ => {
130                let (store, _path) = object_store::parse_url_opts(url, c.options.iter())
131                    .with_context(|| {
132                        format!(
133                            "creating object store from url {url} and options {}",
134                            serde_json::to_string(&c.options)
135                                .unwrap_or_else(|_| "<failed to format>".to_string())
136                        )
137                    })?;
138                store
139            }
140        };
141
142        Ok(Self(Box::new(object_store::prefix::PrefixStore::new(
143            store, path,
144        ))))
145    }
146}
147
148/// Details on how to store this type in the object store.
149///
150/// You probably want to implement this trait via [`JsonObjectDetails`].
151pub trait ObjectDetails: std::marker::Sized {
152    type Identifier: std::fmt::Display;
153
154    const PREFIX: &'static str;
155
156    fn object_id(&self) -> &Self::Identifier;
157
158    fn path_for(id: &Self::Identifier) -> object_store::path::Path {
159        std::format!("{}/{id}", Self::PREFIX).into()
160    }
161
162    fn from_bytes(bytes: bytes::Bytes) -> anyhow::Result<Self>;
163
164    /// Turn this object into one (or more) [`bytes::Bytes`]
165    fn to_put_payload(&self) -> anyhow::Result<object_store::PutPayload>;
166}
167
168/// Default way to implement [`ObjectDetails`], via json serialization.
169pub trait JsonObjectDetails: serde::Serialize + serde::de::DeserializeOwned {
170    type Identifier: std::fmt::Display;
171
172    const PREFIX: &'static str;
173
174    fn object_id(&self) -> &Self::Identifier;
175}
176
177impl<T: JsonObjectDetails> ObjectDetails for T {
178    type Identifier = <T as JsonObjectDetails>::Identifier;
179
180    const PREFIX: &str = <T as JsonObjectDetails>::PREFIX;
181
182    fn object_id(&self) -> &Self::Identifier {
183        <T as JsonObjectDetails>::object_id(self)
184    }
185
186    fn from_bytes(bytes: bytes::Bytes) -> anyhow::Result<Self> {
187        Ok(serde_json::from_slice(&bytes)?)
188    }
189
190    fn to_put_payload(&self) -> anyhow::Result<object_store::PutPayload> {
191        Ok(object_store::PutPayload::from_bytes(
192            serde_json::to_vec(&self)?.into(),
193        ))
194    }
195}
196
197impl<S> crate::servers::AppBase<S>
198where
199    S::ObjectStoreT: AsObjectStore,
200    S: servers::Server,
201{
202    /// Tries to retrieve an object of type `T` from this server's object store with the given
203    /// `id`, returning `Ok(None)` if no such object exists.
204    pub async fn get_object<T>(
205        &self,
206        id: &T::Identifier,
207    ) -> api::Result<Option<(T, object_store::UpdateVersion)>>
208    where
209        T: ObjectDetails,
210    {
211        let os = self.shared.object_store.as_object_store();
212
213        let path = T::path_for(id);
214
215        log::debug!("getting {path}");
216
217        match os.get(&path).await {
218            Ok(get_result) => {
219                let version = object_store::UpdateVersion {
220                    e_tag: get_result.meta.e_tag.clone(),
221                    version: get_result.meta.version.clone(),
222                };
223
224                let bytes: bytes::Bytes = get_result.bytes().await.map_err(|err| {
225                    log::error!(
226                        "{}'s object store: unexpected error getting body of {path}: {err:#}",
227                        S::NAME
228                    );
229                    api::ErrorCode::InternalError
230                })?;
231
232                log::debug!("got {path}");
233
234                Ok(Some((
235                    T::from_bytes(bytes).map_err(|err| {
236                        log::error!(
237                            "{}'s object store: unexpected error parsing object at {path}: {err:#}",
238                            S::NAME
239                        );
240                        api::ErrorCode::InternalError
241                    })?,
242                    version,
243                )))
244            }
245            Err(object_store::Error::NotFound { .. }) => {
246                log::debug!("did not get {path}: not found");
247                Ok(None)
248            }
249            // TODO: deal with timeouts
250            Err(err) => Err({
251                log::error!(
252                    "{}'s object store: unexpected error getting {path}: {err:#}",
253                    S::NAME
254                );
255                api::ErrorCode::InternalError
256            }),
257        }
258    }
259
260    /// Attempts to put an object of type `T` into the object store, only overwriting the object that
261    /// is already present when the version of the to-be-overwritten object is passed via `update`.
262    ///
263    /// Returs `Ok(None)` when there is already an object present in the store with that id and
264    /// type, but its version was not specified in `update`.
265    ///
266    /// [`get_object`]: Self::get_object
267    pub async fn put_object<T>(
268        &self,
269        obj: &T,
270        update: Option<object_store::UpdateVersion>,
271    ) -> api::Result<Option<object_store::UpdateVersion>>
272    where
273        T: ObjectDetails,
274    {
275        let os = self.shared.object_store.as_object_store();
276
277        let path = T::path_for(obj.object_id());
278
279        log::debug!("putting {path}");
280
281        let put_payload: object_store::PutPayload = obj.to_put_payload().map_err(|err| {
282            log::error!(
283                "{}'s object store: unexpected error encoding object to be put at {path}: {err:#}",
284                S::NAME
285            );
286            api::ErrorCode::InternalError
287        })?;
288
289        match os
290            .put_opts(
291                &path,
292                put_payload,
293                object_store::PutOptions {
294                    mode: if let Some(ref version) = update {
295                        object_store::PutMode::Update(version.clone())
296                    } else {
297                        object_store::PutMode::Create
298                    },
299                    tags: Default::default(),
300                    attributes: Default::default(),
301                    extensions: Default::default(),
302                },
303            )
304            .await
305        {
306            Ok(put_result) => {
307                log::debug!("putting {path} succeeded");
308
309                Ok(Some(object_store::UpdateVersion {
310                    e_tag: put_result.e_tag,
311                    version: put_result.version,
312                }))
313            }
314            Err(object_store::Error::Precondition { .. }) => {
315                if update.is_some() {
316                    return Ok(None);
317                }
318                log::error!("object store create put mode caused unexpected 'precondition' error");
319                Err(api::ErrorCode::InternalError)
320            }
321            Err(object_store::Error::AlreadyExists { .. }) => {
322                if update.is_none() {
323                    return Ok(None);
324                }
325                log::error!(
326                    "object store update put mode caused unexpected 'already exists' error"
327                );
328                Err(api::ErrorCode::InternalError)
329            }
330            Err(err) => Err({
331                log::error!(
332                    "{}'s object store: unexpected error putting {path}: {err:#}",
333                    S::NAME
334                );
335                api::ErrorCode::InternalError
336            }),
337        }
338    }
339
340    /// Attempts to delete an object with the given [`Id`]; returns `true` when an object was
341    /// deleted, and false when no object with the given `id` was found.
342    pub async fn delete_object<T>(&self, id: T::Identifier) -> api::Result<bool>
343    where
344        T: ObjectDetails,
345    {
346        let os = self.shared.object_store.as_object_store();
347
348        let path = T::path_for(&id);
349
350        log::debug!("deleting {path}");
351
352        match os.delete(&path).await {
353            Ok(()) => {
354                log::debug!("deleted {path}");
355                Ok(true)
356            }
357            Err(object_store::Error::NotFound { .. }) => {
358                log::info!("deleting {path} failed: not found");
359                Ok(false)
360            }
361            Err(err) => Err({
362                log::error!(
363                    "{}'s object store: failed to delete {path}: {err:#}",
364                    S::NAME
365                );
366                api::ErrorCode::InternalError
367            }),
368        }
369    }
370}
371
372impl JsonObjectDetails for crate::attr::AttrState {
373    type Identifier = Id;
374    const PREFIX: &str = "attr";
375
376    fn object_id(&self) -> &Id {
377        &self.attr
378    }
379}
380
381impl JsonObjectDetails for crate::servers::phc::UserState {
382    type Identifier = Id;
383    const PREFIX: &str = "user";
384
385    fn object_id(&self) -> &Id {
386        &self.id
387    }
388}