pubhubs/servers/
object_store.rs1use 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
13pub 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
28pub trait AsObjectStore {
30 type ObjectStoreT: object_store::ObjectStore + ?Sized;
31
32 fn as_object_store(&self) -> &Self::ObjectStoreT;
33}
34
35pub 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 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 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 if let object_store::aws::AmazonS3ConfigKey::Client(client_key) =
92 &config_key
93 {
94 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 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 _ => {
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
148pub 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 fn to_put_payload(&self) -> anyhow::Result<object_store::PutPayload>;
166}
167
168pub 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 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 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 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 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}