Skip to main content

coil_assets/
delivery.rs

1use std::fmt;
2
3use coil_storage::{
4    DeliveryMode, DurableStore, Sensitivity, StoragePlan, StoragePolicyOverride, SyncMode,
5};
6
7use crate::{AssetModelError, RevisionId, join_delivery_base, require_non_empty};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum AssetKind {
11    DeploymentArtifact,
12    ManagedAsset,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum DeliveryAudience {
17    Public,
18    Authorized,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum FingerprintAlgorithm {
23    Sha256,
24    Sha384,
25    Sha512,
26}
27
28impl fmt::Display for FingerprintAlgorithm {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::Sha256 => f.write_str("sha256"),
32            Self::Sha384 => f.write_str("sha384"),
33            Self::Sha512 => f.write_str("sha512"),
34        }
35    }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39pub struct ContentFingerprint {
40    algorithm: FingerprintAlgorithm,
41    digest: String,
42}
43
44impl ContentFingerprint {
45    pub fn new(
46        algorithm: FingerprintAlgorithm,
47        digest: impl Into<String>,
48    ) -> Result<Self, AssetModelError> {
49        Ok(Self {
50            algorithm,
51            digest: require_non_empty("digest", digest.into())?,
52        })
53    }
54
55    pub fn algorithm(&self) -> FingerprintAlgorithm {
56        self.algorithm
57    }
58
59    pub fn digest(&self) -> &str {
60        &self.digest
61    }
62}
63
64#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
65pub struct DeliveryContext<'a> {
66    pub cdn_base_url: Option<&'a str>,
67    pub app_proxy_base: Option<&'a str>,
68}
69
70impl<'a> DeliveryContext<'a> {
71    pub fn with_cdn_base_url(mut self, base_url: &'a str) -> Self {
72        self.cdn_base_url = Some(base_url);
73        self
74    }
75
76    pub fn with_app_proxy_base(mut self, base_path: &'a str) -> Self {
77        self.app_proxy_base = Some(base_path);
78        self
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum AssetDeliveryTarget {
84    Cdn {
85        public_url: String,
86        object_key: String,
87    },
88    SignedObject {
89        object_key: String,
90    },
91    AppProxy {
92        path: String,
93    },
94    LocalPath {
95        path: String,
96    },
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct AssetDeliveryPlan {
101    asset_kind: AssetKind,
102    audience: DeliveryAudience,
103    storage_plan: StoragePlan,
104    revision_id: Option<RevisionId>,
105    target: AssetDeliveryTarget,
106    immutable: bool,
107}
108
109impl AssetDeliveryPlan {
110    pub fn asset_kind(&self) -> AssetKind {
111        self.asset_kind
112    }
113
114    pub fn audience(&self) -> DeliveryAudience {
115        self.audience
116    }
117
118    pub fn storage_plan(&self) -> &StoragePlan {
119        &self.storage_plan
120    }
121
122    pub fn revision_id(&self) -> Option<&RevisionId> {
123        self.revision_id.as_ref()
124    }
125
126    pub fn target(&self) -> &AssetDeliveryTarget {
127        &self.target
128    }
129
130    pub fn immutable(&self) -> bool {
131        self.immutable
132    }
133
134    pub fn delivery_mode(&self) -> DeliveryMode {
135        self.storage_plan.policy.delivery_mode
136    }
137
138    pub fn durable_store(&self) -> DurableStore {
139        self.storage_plan.durable_store
140    }
141
142    pub fn sensitivity(&self) -> Sensitivity {
143        self.storage_plan.policy.sensitivity
144    }
145}
146
147pub fn public_deployment_override() -> StoragePolicyOverride {
148    StoragePolicyOverride {
149        delivery_mode: Some(DeliveryMode::PublicCdn),
150        sync_mode: Some(SyncMode::ObjectStore),
151        sensitivity: Some(Sensitivity::Public),
152    }
153}
154
155pub fn public_delivery_plan(
156    asset_kind: AssetKind,
157    storage_plan: &StoragePlan,
158    revision_id: Option<RevisionId>,
159    context: &DeliveryContext<'_>,
160    immutable: bool,
161) -> Result<AssetDeliveryPlan, AssetModelError> {
162    storage_plan
163        .ensure_public_delivery_allowed()
164        .map_err(|error| match error {
165            coil_storage::StoragePlanningError::PublicDeliveryNotEligible { policy, .. } => {
166                AssetModelError::PublicDeliveryRequiresPublicCdn {
167                    asset_id: revision_id
168                        .as_ref()
169                        .map(ToString::to_string)
170                        .unwrap_or_else(|| storage_plan.logical_path.clone()),
171                    delivery_mode: policy.delivery_mode,
172                }
173            }
174            other => AssetModelError::Storage(other),
175        })?;
176
177    Ok(AssetDeliveryPlan {
178        asset_kind,
179        audience: DeliveryAudience::Public,
180        storage_plan: storage_plan.clone(),
181        revision_id,
182        target: AssetDeliveryTarget::Cdn {
183            public_url: join_delivery_base(
184                context
185                    .cdn_base_url
186                    .ok_or_else(|| AssetModelError::MissingCdnBaseUrl {
187                        logical_path: storage_plan.logical_path.clone(),
188                    })?,
189                storage_plan.object_key.as_ref().ok_or_else(|| {
190                    AssetModelError::MissingObjectKey {
191                        logical_path: storage_plan.logical_path.clone(),
192                    }
193                })?,
194            ),
195            object_key: storage_plan.object_key.clone().ok_or_else(|| {
196                AssetModelError::MissingObjectKey {
197                    logical_path: storage_plan.logical_path.clone(),
198                }
199            })?,
200        },
201        immutable,
202    })
203}
204
205pub fn authorized_delivery_plan(
206    asset_kind: AssetKind,
207    storage_plan: &StoragePlan,
208    revision_id: Option<RevisionId>,
209    context: &DeliveryContext<'_>,
210) -> Result<AssetDeliveryPlan, AssetModelError> {
211    let target = match storage_plan.policy.delivery_mode {
212        DeliveryMode::PublicCdn => AssetDeliveryTarget::Cdn {
213            public_url: join_delivery_base(
214                context
215                    .cdn_base_url
216                    .ok_or_else(|| AssetModelError::MissingCdnBaseUrl {
217                        logical_path: storage_plan.logical_path.clone(),
218                    })?,
219                storage_plan.object_key.as_ref().ok_or_else(|| {
220                    AssetModelError::MissingObjectKey {
221                        logical_path: storage_plan.logical_path.clone(),
222                    }
223                })?,
224            ),
225            object_key: storage_plan.object_key.clone().ok_or_else(|| {
226                AssetModelError::MissingObjectKey {
227                    logical_path: storage_plan.logical_path.clone(),
228                }
229            })?,
230        },
231        DeliveryMode::SignedUrl => AssetDeliveryTarget::SignedObject {
232            object_key: storage_plan.object_key.clone().ok_or_else(|| {
233                AssetModelError::MissingObjectKey {
234                    logical_path: storage_plan.logical_path.clone(),
235                }
236            })?,
237        },
238        DeliveryMode::AppProxy => AssetDeliveryTarget::AppProxy {
239            path: join_delivery_base(
240                context
241                    .app_proxy_base
242                    .ok_or_else(|| AssetModelError::MissingAppProxyBase {
243                        logical_path: storage_plan.logical_path.clone(),
244                    })?,
245                &storage_plan.logical_path,
246            ),
247        },
248        DeliveryMode::LocalOnly => AssetDeliveryTarget::LocalPath {
249            path: storage_plan.local_path.clone().ok_or_else(|| {
250                AssetModelError::MissingLocalPath {
251                    logical_path: storage_plan.logical_path.clone(),
252                }
253            })?,
254        },
255    };
256
257    Ok(AssetDeliveryPlan {
258        asset_kind,
259        audience: DeliveryAudience::Authorized,
260        storage_plan: storage_plan.clone(),
261        revision_id,
262        target,
263        immutable: false,
264    })
265}