Skip to main content

coil_assets/
release.rs

1use super::*;
2use std::collections::{BTreeMap, btree_map::Entry};
3
4use coil_storage::{StoragePlanRequest, StoragePlanner, StoragePolicy};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct DeploymentArtifact {
8    logical_path: String,
9    hashed_path: String,
10    fingerprint: ContentFingerprint,
11    content_type: String,
12    byte_length: u64,
13}
14
15impl DeploymentArtifact {
16    pub fn new(
17        logical_path: impl Into<String>,
18        hashed_path: impl Into<String>,
19        fingerprint: ContentFingerprint,
20        content_type: impl Into<String>,
21        byte_length: u64,
22    ) -> Result<Self, AssetModelError> {
23        let logical_path = normalize_manifest_path("logical_path", logical_path.into())?;
24        let hashed_path = normalize_manifest_path("hashed_path", hashed_path.into())?;
25        let content_type = require_non_empty("content_type", content_type.into())?;
26
27        if logical_path == hashed_path || !hashed_path.contains(fingerprint.digest()) {
28            return Err(AssetModelError::UnhashedDeploymentArtifact {
29                logical_path,
30                hashed_path,
31                fingerprint: fingerprint.digest().to_string(),
32            });
33        }
34
35        Ok(Self {
36            logical_path,
37            hashed_path,
38            fingerprint,
39            content_type,
40            byte_length,
41        })
42    }
43
44    pub fn logical_path(&self) -> &str {
45        &self.logical_path
46    }
47
48    pub fn hashed_path(&self) -> &str {
49        &self.hashed_path
50    }
51
52    pub fn fingerprint(&self) -> &ContentFingerprint {
53        &self.fingerprint
54    }
55
56    pub fn content_type(&self) -> &str {
57        &self.content_type
58    }
59
60    pub fn byte_length(&self) -> u64 {
61        self.byte_length
62    }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct PublishedDeploymentArtifact {
67    artifact: DeploymentArtifact,
68    delivery: AssetDeliveryPlan,
69}
70
71impl PublishedDeploymentArtifact {
72    pub fn artifact(&self) -> &DeploymentArtifact {
73        &self.artifact
74    }
75
76    pub fn delivery(&self) -> &AssetDeliveryPlan {
77        &self.delivery
78    }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct ActiveAssetManifest {
83    release_id: ReleaseId,
84    entries: BTreeMap<String, PublishedDeploymentArtifact>,
85}
86
87impl ActiveAssetManifest {
88    pub fn release_id(&self) -> &ReleaseId {
89        &self.release_id
90    }
91
92    pub fn resolve(&self, logical_path: &str) -> Option<&PublishedDeploymentArtifact> {
93        self.entries.get(logical_path)
94    }
95
96    pub fn entries(
97        &self,
98    ) -> impl ExactSizeIterator<Item = (&str, &PublishedDeploymentArtifact)> + '_ {
99        self.entries
100            .iter()
101            .map(|(path, artifact)| (path.as_str(), artifact))
102    }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct DeploymentRelease {
107    release_id: ReleaseId,
108    artifacts: Vec<DeploymentArtifact>,
109}
110
111impl DeploymentRelease {
112    pub fn new(
113        release_id: ReleaseId,
114        artifacts: impl IntoIterator<Item = DeploymentArtifact>,
115    ) -> Result<Self, AssetModelError> {
116        let artifacts = artifacts.into_iter().collect::<Vec<_>>();
117        if artifacts.is_empty() {
118            return Err(AssetModelError::EmptyField {
119                field: "deployment_artifacts",
120            });
121        }
122
123        Ok(Self {
124            release_id,
125            artifacts,
126        })
127    }
128
129    pub fn release_id(&self) -> &ReleaseId {
130        &self.release_id
131    }
132
133    pub fn artifacts(&self) -> &[DeploymentArtifact] {
134        &self.artifacts
135    }
136
137    pub fn publish(
138        &self,
139        planner: &StoragePlanner,
140        cdn_base_url: &str,
141    ) -> Result<ActiveAssetManifest, AssetModelError> {
142        let context = DeliveryContext::default().with_cdn_base_url(cdn_base_url);
143        let mut entries = BTreeMap::new();
144
145        for artifact in &self.artifacts {
146            let storage_plan = planner
147                .plan_scalable_write(
148                    StoragePlanRequest::new(artifact.hashed_path())
149                        .with_override(public_deployment_override()),
150                )
151                .map_err(AssetModelError::Storage)?;
152
153            if storage_plan.policy != StoragePolicy::public_asset() {
154                return Err(AssetModelError::InvalidDeploymentPolicy {
155                    logical_path: artifact.logical_path().to_string(),
156                    policy: storage_plan.policy,
157                });
158            }
159
160            let delivery = public_delivery_plan(
161                AssetKind::DeploymentArtifact,
162                &storage_plan,
163                None,
164                &context,
165                true,
166            )?;
167
168            match entries.entry(artifact.logical_path().to_string()) {
169                Entry::Vacant(entry) => {
170                    entry.insert(PublishedDeploymentArtifact {
171                        artifact: artifact.clone(),
172                        delivery,
173                    });
174                }
175                Entry::Occupied(_) => {
176                    return Err(AssetModelError::DuplicateDeploymentArtifact {
177                        release_id: self.release_id.to_string(),
178                        logical_path: artifact.logical_path().to_string(),
179                    });
180                }
181            }
182        }
183
184        Ok(ActiveAssetManifest {
185            release_id: self.release_id.clone(),
186            entries,
187        })
188    }
189}