kellnr_storage/
gcs_storage.rs1use std::time::Duration;
2
3use async_trait::async_trait;
4use bytes::Bytes;
5use futures_util::StreamExt;
6use kellnr_settings::Settings;
7use object_store::gcp::{GoogleCloudStorage, GoogleCloudStorageBuilder};
8use object_store::path::Path;
9use object_store::{ClientOptions, ObjectStore, ObjectStoreExt, PutMode};
10
11use crate::storage::{ObjectWithMeta, Storage};
12use crate::storage_error::StorageError;
13
14pub struct GCSStorage(GoogleCloudStorage);
15
16#[async_trait]
17impl Storage for GCSStorage {
18 async fn get(&self, key: &str) -> Result<Bytes, StorageError> {
19 self.storage()
20 .get(&Self::try_path_from(key)?)
21 .await?
22 .bytes()
23 .await
24 .map_err(StorageError::from)
25 }
26
27 async fn get_with_meta(&self, key: &str) -> Result<ObjectWithMeta, StorageError> {
28 let result = self.storage().get(&Self::try_path_from(key)?).await?;
29 let e_tag = result.meta.e_tag.clone();
30 let last_modified = result.meta.last_modified;
31 let bytes = result.bytes().await?;
32 Ok(ObjectWithMeta {
33 bytes,
34 e_tag,
35 last_modified,
36 })
37 }
38
39 async fn put(&self, key: &str, object: Bytes) -> Result<(), StorageError> {
40 self.storage()
41 .put_opts(
42 &Self::try_path_from(key)?,
43 object.into(),
44 PutMode::Create.into(),
45 )
46 .await?;
47 Ok(())
48 }
49
50 async fn put_overwrite(&self, key: &str, object: Bytes) -> Result<(), StorageError> {
51 self.storage()
52 .put_opts(
53 &Self::try_path_from(key)?,
54 object.into(),
55 PutMode::Overwrite.into(),
56 )
57 .await?;
58 Ok(())
59 }
60
61 async fn delete(&self, key: &str) -> Result<(), StorageError> {
62 let path = Self::try_path_from(key)?;
63 self.storage().delete(&path).await?;
64 Ok(())
65 }
66
67 async fn exists(&self, key: &str) -> Result<bool, StorageError> {
68 let path = Self::try_path_from(key)?;
69 self.storage()
70 .head(&path)
71 .await
72 .map(|_| true)
73 .or_else(|e| match e {
74 object_store::Error::NotFound { .. } => Ok(false),
75 _ => Err(StorageError::from(e)),
76 })
77 }
78
79 async fn size(&self, key: &str) -> Result<u64, StorageError> {
80 let path = Self::try_path_from(key)?;
81 Ok(self.storage().head(&path).await?.size)
82 }
83
84 async fn list(&self, prefix: &str) -> Result<Vec<String>, StorageError> {
85 let prefix = if prefix.is_empty() {
86 None
87 } else {
88 Some(Self::try_path_from(prefix)?)
89 };
90 let mut stream = self.storage().list(prefix.as_ref());
91 let mut keys = Vec::new();
92 while let Some(meta) = stream.next().await {
93 keys.push(meta?.location.to_string());
94 }
95 Ok(keys)
96 }
97}
98
99impl GCSStorage {
100 fn try_path_from(key: &str) -> Result<Path, object_store::path::Error> {
101 Path::from_url_path(key)
102 }
103
104 fn storage(&self) -> &GoogleCloudStorage {
105 &self.0
106 }
107}
108
109impl TryFrom<(&str, &Settings)> for GCSStorage {
110 type Error = StorageError;
111
112 fn try_from((bucket, settings): (&str, &Settings)) -> Result<Self, Self::Error> {
113 let client_options = ClientOptions::new()
120 .with_connect_timeout(Duration::from_secs(settings.gcs.connect_timeout_seconds))
121 .with_timeout(Duration::from_secs(settings.gcs.request_timeout_seconds))
122 .with_allow_http(settings.gcs.allow_http);
123
124 let mut gcs = if settings.gcs.skip_signature {
125 const FAKE_SERVICE_ACCOUNT_KEY: &str = r#"{"private_key":"unused","private_key_id":"unused","client_email":"unused@example.com","disable_oauth":true}"#;
134 GoogleCloudStorageBuilder::new()
135 .with_service_account_key(FAKE_SERVICE_ACCOUNT_KEY)
136 .with_skip_signature(true)
137 } else {
138 GoogleCloudStorageBuilder::from_env()
139 };
140 if let Some(endpoint) = &settings.gcs.endpoint {
141 gcs = gcs.with_base_url(endpoint);
142 }
143 let gcs = gcs
144 .with_bucket_name(bucket)
145 .with_client_options(client_options);
146 Ok(Self(gcs.build()?))
147 }
148}