shaperail_runtime/storage/
object_store_backend.rs1use super::backend::{FileMetadata, StorageError};
2use object_store::path::Path as ObjectPath;
3use object_store::signer::Signer;
4use object_store::{ObjectStore, ObjectStoreExt, PutPayload};
5use std::time::Duration;
6
7async fn do_upload(
9 store: &dyn ObjectStore,
10 path: &str,
11 data: &[u8],
12 mime_type: &str,
13) -> Result<FileMetadata, StorageError> {
14 let obj_path = ObjectPath::from(path);
15 let payload = PutPayload::from(data.to_vec());
16 store
17 .put(&obj_path, payload)
18 .await
19 .map_err(|e| StorageError::Backend(format!("Upload failed: {e}")))?;
20
21 let filename = path.rsplit('/').next().unwrap_or(path).to_string();
22
23 Ok(FileMetadata {
24 path: path.to_string(),
25 filename,
26 mime_type: mime_type.to_string(),
27 size: data.len() as u64,
28 })
29}
30
31async fn do_download(store: &dyn ObjectStore, path: &str) -> Result<Vec<u8>, StorageError> {
33 let obj_path = ObjectPath::from(path);
34 let result = store.get(&obj_path).await.map_err(|e| {
35 if e.to_string().contains("not found") || e.to_string().contains("404") {
36 StorageError::NotFound(path.to_string())
37 } else {
38 StorageError::Backend(format!("Download failed: {e}"))
39 }
40 })?;
41 result
42 .bytes()
43 .await
44 .map(|b| b.to_vec())
45 .map_err(|e| StorageError::Backend(format!("Failed to read bytes: {e}")))
46}
47
48async fn do_delete(store: &dyn ObjectStore, path: &str) -> Result<(), StorageError> {
50 let obj_path = ObjectPath::from(path);
51 store.delete(&obj_path).await.map_err(|e| {
52 if e.to_string().contains("not found") || e.to_string().contains("404") {
53 StorageError::NotFound(path.to_string())
54 } else {
55 StorageError::Backend(format!("Delete failed: {e}"))
56 }
57 })
58}
59
60async fn do_signed_url(
62 signer: &dyn Signer,
63 path: &str,
64 expires_secs: u64,
65 base_url: &str,
66) -> Result<String, StorageError> {
67 let obj_path = ObjectPath::from(path);
68 let duration = Duration::from_secs(expires_secs);
69 match signer
70 .signed_url(http::Method::GET, &obj_path, duration)
71 .await
72 {
73 Ok(url) => Ok(url.to_string()),
74 Err(_) => Ok(format!("{}/{}", base_url.trim_end_matches('/'), path)),
75 }
76}
77
78pub struct S3Storage {
86 store: object_store::aws::AmazonS3,
87 base_url: String,
88}
89
90impl S3Storage {
91 pub fn from_env() -> Result<Self, StorageError> {
93 let bucket = std::env::var("SHAPERAIL_STORAGE_BUCKET").map_err(|_| {
94 StorageError::Backend("SHAPERAIL_STORAGE_BUCKET env var required for S3".to_string())
95 })?;
96 let region = std::env::var("SHAPERAIL_STORAGE_REGION")
97 .or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
98 .unwrap_or_else(|_| "us-east-1".to_string());
99
100 let store = object_store::aws::AmazonS3Builder::from_env()
101 .with_bucket_name(&bucket)
102 .with_region(®ion)
103 .build()
104 .map_err(|e| StorageError::Backend(format!("Failed to build S3 client: {e}")))?;
105
106 let base_url = format!("https://{bucket}.s3.{region}.amazonaws.com");
107
108 Ok(Self { store, base_url })
109 }
110
111 pub async fn upload(
112 &self,
113 path: &str,
114 data: &[u8],
115 mime_type: &str,
116 ) -> Result<FileMetadata, StorageError> {
117 do_upload(&self.store, path, data, mime_type).await
118 }
119
120 pub async fn download(&self, path: &str) -> Result<Vec<u8>, StorageError> {
121 do_download(&self.store, path).await
122 }
123
124 pub async fn delete(&self, path: &str) -> Result<(), StorageError> {
125 do_delete(&self.store, path).await
126 }
127
128 pub async fn signed_url(&self, path: &str, expires_secs: u64) -> Result<String, StorageError> {
129 do_signed_url(&self.store, path, expires_secs, &self.base_url).await
130 }
131}
132
133pub struct GcsStorage {
139 store: object_store::gcp::GoogleCloudStorage,
140 base_url: String,
141}
142
143impl GcsStorage {
144 pub fn from_env() -> Result<Self, StorageError> {
146 let bucket = std::env::var("SHAPERAIL_STORAGE_BUCKET").map_err(|_| {
147 StorageError::Backend("SHAPERAIL_STORAGE_BUCKET env var required for GCS".to_string())
148 })?;
149
150 let store = object_store::gcp::GoogleCloudStorageBuilder::from_env()
151 .with_bucket_name(&bucket)
152 .build()
153 .map_err(|e| StorageError::Backend(format!("Failed to build GCS client: {e}")))?;
154
155 let base_url = format!("https://storage.googleapis.com/{bucket}");
156
157 Ok(Self { store, base_url })
158 }
159
160 pub async fn upload(
161 &self,
162 path: &str,
163 data: &[u8],
164 mime_type: &str,
165 ) -> Result<FileMetadata, StorageError> {
166 do_upload(&self.store, path, data, mime_type).await
167 }
168
169 pub async fn download(&self, path: &str) -> Result<Vec<u8>, StorageError> {
170 do_download(&self.store, path).await
171 }
172
173 pub async fn delete(&self, path: &str) -> Result<(), StorageError> {
174 do_delete(&self.store, path).await
175 }
176
177 pub async fn signed_url(&self, path: &str, expires_secs: u64) -> Result<String, StorageError> {
178 do_signed_url(&self.store, path, expires_secs, &self.base_url).await
179 }
180}
181
182pub struct AzureStorage {
189 store: object_store::azure::MicrosoftAzure,
190 base_url: String,
191}
192
193impl AzureStorage {
194 pub fn from_env() -> Result<Self, StorageError> {
196 let container = std::env::var("SHAPERAIL_STORAGE_BUCKET").map_err(|_| {
197 StorageError::Backend("SHAPERAIL_STORAGE_BUCKET env var required for Azure".to_string())
198 })?;
199 let account = std::env::var("AZURE_STORAGE_ACCOUNT_NAME")
200 .unwrap_or_else(|_| "devstoreaccount1".to_string());
201
202 let store = object_store::azure::MicrosoftAzureBuilder::from_env()
203 .with_container_name(&container)
204 .build()
205 .map_err(|e| StorageError::Backend(format!("Failed to build Azure client: {e}")))?;
206
207 let base_url = format!("https://{account}.blob.core.windows.net/{container}");
208
209 Ok(Self { store, base_url })
210 }
211
212 pub async fn upload(
213 &self,
214 path: &str,
215 data: &[u8],
216 mime_type: &str,
217 ) -> Result<FileMetadata, StorageError> {
218 do_upload(&self.store, path, data, mime_type).await
219 }
220
221 pub async fn download(&self, path: &str) -> Result<Vec<u8>, StorageError> {
222 do_download(&self.store, path).await
223 }
224
225 pub async fn delete(&self, path: &str) -> Result<(), StorageError> {
226 do_delete(&self.store, path).await
227 }
228
229 pub async fn signed_url(&self, path: &str, expires_secs: u64) -> Result<String, StorageError> {
230 do_signed_url(&self.store, path, expires_secs, &self.base_url).await
231 }
232}