Skip to main content

coil_storage/policy/
model.rs

1use std::fmt;
2
3use coil_config::StorageClass;
4
5use super::StoragePolicyError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum StorageBackendKind {
9    LocalDisk,
10    S3Compatible,
11}
12
13impl fmt::Display for StorageBackendKind {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        match self {
16            Self::LocalDisk => f.write_str("local_disk"),
17            Self::S3Compatible => f.write_str("s3_compatible"),
18        }
19    }
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum PathPolicyKind {
24    Folder,
25    Upload,
26}
27
28impl fmt::Display for PathPolicyKind {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::Folder => f.write_str("folder"),
32            Self::Upload => f.write_str("upload"),
33        }
34    }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum DurableStore {
39    LocalDisk,
40    ObjectStore,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum DeliveryMode {
45    PublicCdn,
46    SignedUrl,
47    AppProxy,
48    LocalOnly,
49}
50
51impl fmt::Display for DeliveryMode {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::PublicCdn => f.write_str("public_cdn"),
55            Self::SignedUrl => f.write_str("signed_url"),
56            Self::AppProxy => f.write_str("app_proxy"),
57            Self::LocalOnly => f.write_str("local_only"),
58        }
59    }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum SyncMode {
64    ObjectStore,
65    LocalOnly,
66}
67
68impl fmt::Display for SyncMode {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Self::ObjectStore => f.write_str("object_store"),
72            Self::LocalOnly => f.write_str("local_only"),
73        }
74    }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum Sensitivity {
79    Public,
80    Internal,
81    Restricted,
82    Secret,
83}
84
85impl fmt::Display for Sensitivity {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Self::Public => f.write_str("public"),
89            Self::Internal => f.write_str("internal"),
90            Self::Restricted => f.write_str("restricted"),
91            Self::Secret => f.write_str("secret"),
92        }
93    }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97pub struct StoragePolicy {
98    pub delivery_mode: DeliveryMode,
99    pub sync_mode: SyncMode,
100    pub sensitivity: Sensitivity,
101}
102
103impl StoragePolicy {
104    pub const fn new(
105        delivery_mode: DeliveryMode,
106        sync_mode: SyncMode,
107        sensitivity: Sensitivity,
108    ) -> Self {
109        Self {
110            delivery_mode,
111            sync_mode,
112            sensitivity,
113        }
114    }
115
116    pub const fn public_asset() -> Self {
117        Self::new(
118            DeliveryMode::PublicCdn,
119            SyncMode::ObjectStore,
120            Sensitivity::Public,
121        )
122    }
123
124    pub const fn public_upload() -> Self {
125        Self::new(
126            DeliveryMode::PublicCdn,
127            SyncMode::ObjectStore,
128            Sensitivity::Public,
129        )
130    }
131
132    pub const fn private_shared() -> Self {
133        Self::new(
134            DeliveryMode::SignedUrl,
135            SyncMode::ObjectStore,
136            Sensitivity::Restricted,
137        )
138    }
139
140    pub const fn single_node_sensitive() -> Self {
141        Self::new(
142            DeliveryMode::LocalOnly,
143            SyncMode::LocalOnly,
144            Sensitivity::Secret,
145        )
146    }
147
148    pub fn validate(&self) -> Result<(), StoragePolicyError> {
149        match (self.delivery_mode, self.sync_mode, self.sensitivity) {
150            (DeliveryMode::PublicCdn, SyncMode::LocalOnly, _) => {
151                Err(StoragePolicyError::InvalidCombination {
152                    detail: "public_cdn delivery requires object_store sync".to_string(),
153                })
154            }
155            (DeliveryMode::SignedUrl, SyncMode::LocalOnly, _) => {
156                Err(StoragePolicyError::InvalidCombination {
157                    detail: "signed_url delivery requires object_store sync".to_string(),
158                })
159            }
160            (
161                DeliveryMode::PublicCdn,
162                _,
163                Sensitivity::Internal | Sensitivity::Restricted | Sensitivity::Secret,
164            ) => Err(StoragePolicyError::InvalidCombination {
165                detail: "public_cdn delivery is only valid for public content".to_string(),
166            }),
167            (DeliveryMode::LocalOnly, SyncMode::ObjectStore, _) => {
168                Err(StoragePolicyError::InvalidCombination {
169                    detail: "local_only delivery cannot use object_store sync".to_string(),
170                })
171            }
172            (DeliveryMode::SignedUrl, _, Sensitivity::Public) => {
173                Err(StoragePolicyError::InvalidCombination {
174                    detail: "signed_url delivery is for non-public content".to_string(),
175                })
176            }
177            (_, _, _) => Ok(()),
178        }
179    }
180
181    pub const fn durable_store(&self) -> DurableStore {
182        match self.sync_mode {
183            SyncMode::ObjectStore => DurableStore::ObjectStore,
184            SyncMode::LocalOnly => DurableStore::LocalDisk,
185        }
186    }
187
188    pub const fn is_public_delivery_eligible(&self) -> bool {
189        matches!(
190            (self.delivery_mode, self.sensitivity),
191            (DeliveryMode::PublicCdn, Sensitivity::Public)
192        )
193    }
194}
195
196impl From<StorageClass> for StoragePolicy {
197    fn from(value: StorageClass) -> Self {
198        match value {
199            StorageClass::PublicAsset => Self::public_asset(),
200            StorageClass::PublicUpload => Self::public_upload(),
201            StorageClass::PrivateShared => Self::private_shared(),
202            StorageClass::LocalOnlySensitive => Self::single_node_sensitive(),
203        }
204    }
205}
206
207#[derive(Debug, Clone, Default, PartialEq, Eq)]
208pub struct StoragePolicyOverride {
209    pub delivery_mode: Option<DeliveryMode>,
210    pub sync_mode: Option<SyncMode>,
211    pub sensitivity: Option<Sensitivity>,
212}
213
214impl StoragePolicyOverride {
215    pub fn apply_to(&self, base: StoragePolicy) -> StoragePolicy {
216        StoragePolicy {
217            delivery_mode: self.delivery_mode.unwrap_or(base.delivery_mode),
218            sync_mode: self.sync_mode.unwrap_or(base.sync_mode),
219            sensitivity: self.sensitivity.unwrap_or(base.sensitivity),
220        }
221    }
222
223    pub const fn is_local_only_escape_hatch(&self) -> bool {
224        matches!(
225            (self.delivery_mode, self.sync_mode, self.sensitivity,),
226            (
227                Some(DeliveryMode::LocalOnly),
228                Some(SyncMode::LocalOnly),
229                Some(Sensitivity::Secret),
230            )
231        )
232    }
233
234    pub fn force_single_node_escape_hatch() -> Self {
235        Self {
236            delivery_mode: Some(DeliveryMode::LocalOnly),
237            sync_mode: Some(SyncMode::LocalOnly),
238            sensitivity: Some(Sensitivity::Secret),
239        }
240    }
241}