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}