shaperail_runtime/storage/
backend.rs1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct FileMetadata {
7 pub path: String,
9 pub filename: String,
11 pub mime_type: String,
13 pub size: u64,
15}
16
17#[derive(Debug)]
19pub enum StorageError {
20 NotFound(String),
22 FileTooLarge { max_bytes: u64, actual_bytes: u64 },
24 InvalidMimeType {
26 mime_type: String,
27 allowed: Vec<String>,
28 },
29 Backend(String),
31}
32
33impl fmt::Display for StorageError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::NotFound(path) => write!(f, "File not found: {path}"),
37 Self::FileTooLarge {
38 max_bytes,
39 actual_bytes,
40 } => {
41 write!(
42 f,
43 "File too large: {actual_bytes} bytes exceeds limit of {max_bytes} bytes"
44 )
45 }
46 Self::InvalidMimeType { mime_type, allowed } => {
47 write!(f, "Invalid MIME type '{mime_type}', allowed: {allowed:?}")
48 }
49 Self::Backend(msg) => write!(f, "Storage backend error: {msg}"),
50 }
51 }
52}
53
54impl std::error::Error for StorageError {}
55
56impl From<StorageError> for shaperail_core::ShaperailError {
57 fn from(err: StorageError) -> Self {
58 match err {
59 StorageError::NotFound(_) => shaperail_core::ShaperailError::NotFound,
60 StorageError::FileTooLarge {
61 max_bytes,
62 actual_bytes,
63 } => shaperail_core::ShaperailError::Validation(vec![shaperail_core::FieldError {
64 field: "file".to_string(),
65 message: format!(
66 "File too large: {actual_bytes} bytes exceeds limit of {max_bytes} bytes"
67 ),
68 code: "file_too_large".to_string(),
69 }]),
70 StorageError::InvalidMimeType { mime_type, allowed } => {
71 shaperail_core::ShaperailError::Validation(vec![shaperail_core::FieldError {
72 field: "file".to_string(),
73 message: format!("Invalid MIME type '{mime_type}', allowed: {allowed:?}"),
74 code: "invalid_mime_type".to_string(),
75 }])
76 }
77 StorageError::Backend(msg) => shaperail_core::ShaperailError::Internal(msg),
78 }
79 }
80}
81
82pub enum StorageBackend {
86 Local(super::LocalStorage),
87 S3(super::S3Storage),
88 Gcs(super::GcsStorage),
89 Azure(super::AzureStorage),
90}
91
92impl StorageBackend {
93 pub fn from_name(name: &str) -> Result<Self, StorageError> {
95 match name {
96 "local" => Ok(Self::Local(super::LocalStorage::from_env())),
97 "s3" => super::S3Storage::from_env().map(Self::S3),
98 "gcs" => super::GcsStorage::from_env().map(Self::Gcs),
99 "azure" => super::AzureStorage::from_env().map(Self::Azure),
100 other => Err(StorageError::Backend(format!(
101 "Unknown storage backend: '{other}'. Supported: local, s3, gcs, azure"
102 ))),
103 }
104 }
105
106 pub fn from_env() -> Result<Self, StorageError> {
111 let backend =
112 std::env::var("SHAPERAIL_STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
113 Self::from_name(&backend)
114 }
115
116 pub async fn upload(
118 &self,
119 path: &str,
120 data: &[u8],
121 mime_type: &str,
122 ) -> Result<FileMetadata, StorageError> {
123 match self {
124 Self::Local(s) => s.upload(path, data, mime_type).await,
125 Self::S3(s) => s.upload(path, data, mime_type).await,
126 Self::Gcs(s) => s.upload(path, data, mime_type).await,
127 Self::Azure(s) => s.upload(path, data, mime_type).await,
128 }
129 }
130
131 pub async fn download(&self, path: &str) -> Result<Vec<u8>, StorageError> {
133 match self {
134 Self::Local(s) => s.download(path).await,
135 Self::S3(s) => s.download(path).await,
136 Self::Gcs(s) => s.download(path).await,
137 Self::Azure(s) => s.download(path).await,
138 }
139 }
140
141 pub async fn delete(&self, path: &str) -> Result<(), StorageError> {
143 match self {
144 Self::Local(s) => s.delete(path).await,
145 Self::S3(s) => s.delete(path).await,
146 Self::Gcs(s) => s.delete(path).await,
147 Self::Azure(s) => s.delete(path).await,
148 }
149 }
150
151 pub async fn signed_url(&self, path: &str, expires_secs: u64) -> Result<String, StorageError> {
153 match self {
154 Self::Local(s) => s.signed_url(path, expires_secs).await,
155 Self::S3(s) => s.signed_url(path, expires_secs).await,
156 Self::Gcs(s) => s.signed_url(path, expires_secs).await,
157 Self::Azure(s) => s.signed_url(path, expires_secs).await,
158 }
159 }
160}