Skip to main content

coil_assets/
managed.rs

1use super::*;
2use coil_auth::{DefaultSubject, DefaultTuple, DefaultTupleUpdate, Entity, Relation};
3use coil_storage::{
4    SingleNodeEscapeHatchPlanner, StoragePlan, StoragePlanRequest, StoragePlanner,
5    StoragePolicyOverride,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ManagedAssetRevision {
10    id: RevisionId,
11    storage_plan: StoragePlan,
12    content_type: String,
13    byte_length: u64,
14    fingerprint: ContentFingerprint,
15}
16
17impl ManagedAssetRevision {
18    pub fn plan(
19        id: RevisionId,
20        planner: &StoragePlanner,
21        logical_path: impl Into<String>,
22        override_policy: Option<StoragePolicyOverride>,
23        content_type: impl Into<String>,
24        byte_length: u64,
25        fingerprint: ContentFingerprint,
26    ) -> Result<Self, AssetModelError> {
27        let mut request = StoragePlanRequest::new(logical_path);
28        if let Some(override_policy) = override_policy {
29            request = request.with_override(override_policy);
30        }
31
32        let storage_plan = planner
33            .plan_scalable_write(request)
34            .map_err(AssetModelError::Storage)?;
35        Self::new(id, storage_plan, content_type, byte_length, fingerprint)
36    }
37
38    pub fn plan_with_single_node_escape_hatch(
39        id: RevisionId,
40        planner: &SingleNodeEscapeHatchPlanner,
41        logical_path: impl Into<String>,
42        override_policy: Option<StoragePolicyOverride>,
43        content_type: impl Into<String>,
44        byte_length: u64,
45        fingerprint: ContentFingerprint,
46    ) -> Result<Self, AssetModelError> {
47        let mut request = StoragePlanRequest::new(logical_path);
48        if let Some(override_policy) = override_policy {
49            request = request.with_override(override_policy);
50        }
51
52        let storage_plan = planner
53            .plan_write(request)
54            .map_err(AssetModelError::Storage)?;
55        Self::new(id, storage_plan, content_type, byte_length, fingerprint)
56    }
57
58    pub fn new(
59        id: RevisionId,
60        storage_plan: StoragePlan,
61        content_type: impl Into<String>,
62        byte_length: u64,
63        fingerprint: ContentFingerprint,
64    ) -> Result<Self, AssetModelError> {
65        Ok(Self {
66            id,
67            storage_plan,
68            content_type: require_non_empty("content_type", content_type.into())?,
69            byte_length,
70            fingerprint,
71        })
72    }
73
74    pub fn id(&self) -> &RevisionId {
75        &self.id
76    }
77
78    pub fn storage_plan(&self) -> &StoragePlan {
79        &self.storage_plan
80    }
81
82    pub fn content_type(&self) -> &str {
83        &self.content_type
84    }
85
86    pub fn byte_length(&self) -> u64 {
87        self.byte_length
88    }
89
90    pub fn fingerprint(&self) -> &ContentFingerprint {
91        &self.fingerprint
92    }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum PublicationStatus {
97    Draft,
98    Published,
99    Unpublished,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum PublicationTransition {
104    PublishCurrent,
105    Unpublish,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct PublicationState {
110    status: PublicationStatus,
111    live_revision: Option<ManagedAssetRevision>,
112}
113
114impl PublicationState {
115    pub fn status(&self) -> PublicationStatus {
116        self.status
117    }
118
119    pub fn is_published(&self) -> bool {
120        self.status == PublicationStatus::Published
121    }
122
123    pub fn live_revision(&self) -> Option<&ManagedAssetRevision> {
124        self.live_revision.as_ref()
125    }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct ManagedAsset {
130    id: AssetId,
131    display_name: String,
132    current_revision: ManagedAssetRevision,
133    publication: PublicationState,
134}
135
136impl ManagedAsset {
137    pub fn new(
138        id: AssetId,
139        display_name: impl Into<String>,
140        initial_revision: ManagedAssetRevision,
141    ) -> Result<Self, AssetModelError> {
142        Ok(Self {
143            id,
144            display_name: require_non_empty("display_name", display_name.into())?,
145            current_revision: initial_revision,
146            publication: PublicationState {
147                status: PublicationStatus::Draft,
148                live_revision: None,
149            },
150        })
151    }
152
153    pub fn id(&self) -> &AssetId {
154        &self.id
155    }
156
157    pub fn display_name(&self) -> &str {
158        &self.display_name
159    }
160
161    pub fn current_revision(&self) -> &ManagedAssetRevision {
162        &self.current_revision
163    }
164
165    pub fn publication(&self) -> &PublicationState {
166        &self.publication
167    }
168
169    pub fn auth_entity(&self) -> Entity {
170        Entity::asset(self.id.to_string())
171    }
172
173    pub fn has_pending_changes(&self) -> bool {
174        match self.publication.live_revision() {
175            Some(live_revision) => live_revision.id() != self.current_revision.id(),
176            None => true,
177        }
178    }
179
180    pub fn publish_current(&mut self) {
181        self.publication.live_revision = Some(self.current_revision.clone());
182        self.publication.status = PublicationStatus::Published;
183    }
184
185    pub fn apply_publication_transition(
186        &mut self,
187        transition: PublicationTransition,
188    ) -> Result<(), AssetModelError> {
189        match transition {
190            PublicationTransition::PublishCurrent => {
191                self.publish_current();
192                Ok(())
193            }
194            PublicationTransition::Unpublish => self.unpublish(),
195        }
196    }
197
198    pub fn replace_current_revision(&mut self, revision: ManagedAssetRevision) {
199        self.current_revision = revision;
200    }
201
202    pub fn unpublish(&mut self) -> Result<(), AssetModelError> {
203        if self.publication.live_revision.is_none() {
204            return Err(AssetModelError::CannotUnpublishWithoutLiveRevision {
205                asset_id: self.id.to_string(),
206            });
207        }
208
209        self.publication.live_revision = None;
210        self.publication.status = PublicationStatus::Unpublished;
211        Ok(())
212    }
213
214    pub fn plan_public_delivery(
215        &self,
216        context: &DeliveryContext<'_>,
217    ) -> Result<AssetDeliveryPlan, AssetModelError> {
218        if !self.publication.is_published() {
219            return Err(AssetModelError::NotPublished {
220                asset_id: self.id.to_string(),
221            });
222        }
223
224        let live_revision = self.publication.live_revision().ok_or_else(|| {
225            AssetModelError::MissingLiveRevision {
226                asset_id: self.id.to_string(),
227            }
228        })?;
229
230        public_delivery_plan(
231            AssetKind::ManagedAsset,
232            live_revision.storage_plan(),
233            Some(live_revision.id().clone()),
234            context,
235            false,
236        )
237        .map_err(|error| match error {
238            AssetModelError::PublicDeliveryRequiresPublicCdn { .. } => {
239                AssetModelError::PublicDeliveryRequiresPublicCdn {
240                    asset_id: self.id.to_string(),
241                    delivery_mode: live_revision.storage_plan().policy.delivery_mode,
242                }
243            }
244            other => other,
245        })
246    }
247
248    pub fn plan_authorized_delivery(
249        &self,
250        context: &DeliveryContext<'_>,
251    ) -> Result<AssetDeliveryPlan, AssetModelError> {
252        authorized_delivery_plan(
253            AssetKind::ManagedAsset,
254            self.current_revision.storage_plan(),
255            Some(self.current_revision.id().clone()),
256            context,
257        )
258    }
259
260    pub fn auth_updates(&self) -> Vec<DefaultTupleUpdate> {
261        let public_tuple = DefaultTuple::new(
262            self.auth_entity(),
263            Relation::ReadPublic,
264            DefaultSubject::entity(Entity::any_user()),
265        );
266
267        if self.publication.is_published()
268            && self
269                .publication
270                .live_revision()
271                .is_some_and(|revision| revision.storage_plan().public_delivery_eligible())
272        {
273            vec![DefaultTupleUpdate::Write(public_tuple)]
274        } else {
275            vec![DefaultTupleUpdate::Delete(public_tuple)]
276        }
277    }
278}