typub_storage/
deferred.rs1use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use typub_core::AssetStrategy;
8
9#[derive(Debug, Clone)]
12pub struct PendingAsset {
13 pub index: usize,
15 pub local_path: PathBuf,
17 pub original_ref: String,
19}
20
21impl PendingAsset {
22 pub fn placeholder(&self) -> String {
25 format!("{{{{ASSET:{}}}}}", self.index)
26 }
27}
28
29#[derive(Debug, Clone)]
31pub struct PendingAssetList {
32 pub assets: Vec<PendingAsset>,
34}
35
36impl PendingAssetList {
37 pub fn new() -> Self {
39 Self { assets: Vec::new() }
40 }
41
42 pub fn placeholder_for(&self, original_ref: &str) -> Option<String> {
44 self.assets
45 .iter()
46 .find(|a| a.original_ref == original_ref)
47 .map(|a| a.placeholder())
48 }
49}
50
51impl Default for PendingAssetList {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57#[derive(Debug, Clone)]
68pub struct DeferredAssets {
69 pub pending: PendingAssetList,
71 pub strategy: AssetStrategy,
73 pub resolved: HashMap<usize, String>,
75}
76
77impl DeferredAssets {
78 pub fn new(pending: PendingAssetList, strategy: AssetStrategy) -> Self {
80 Self {
81 pending,
82 strategy,
83 resolved: HashMap::new(),
84 }
85 }
86
87 pub fn empty() -> Self {
89 Self::new(PendingAssetList::new(), AssetStrategy::Copy)
90 }
91
92 pub fn needs_materialize(&self) -> bool {
94 self.strategy.requires_deferred_upload() && !self.pending.assets.is_empty()
95 }
96
97 pub fn is_resolved(&self) -> bool {
99 self.pending.assets.len() == self.resolved.len()
100 }
101}
102
103impl Default for DeferredAssets {
104 fn default() -> Self {
105 Self::empty()
106 }
107}
108
109pub fn build_pending_asset_list(assets: &[PathBuf], content_path: &Path) -> PendingAssetList {
124 let mut pending = Vec::with_capacity(assets.len());
125
126 for (index, asset) in assets.iter().enumerate() {
127 let local_path = if asset.is_absolute() {
129 asset.clone()
130 } else {
131 content_path.join(asset)
132 };
133
134 let original_ref = if let Ok(rel) = asset.strip_prefix(content_path) {
136 rel.to_string_lossy().replace('\\', "/")
137 } else if asset.is_relative() {
138 asset.to_string_lossy().replace('\\', "/")
139 } else {
140 local_path
141 .file_name()
142 .map(|n| n.to_string_lossy().to_string())
143 .unwrap_or_else(|| asset.to_string_lossy().to_string())
144 };
145
146 pending.push(PendingAsset {
147 index,
148 local_path,
149 original_ref,
150 });
151 }
152
153 PendingAssetList { assets: pending }
154}
155
156#[cfg(test)]
157mod tests {
158 #![allow(clippy::expect_used)]
159 use super::*;
160
161 #[test]
162 fn test_pending_asset_placeholder() {
163 let asset = PendingAsset {
164 index: 0,
165 local_path: PathBuf::from("/tmp/image.png"),
166 original_ref: "assets/image.png".to_string(),
167 };
168 assert_eq!(asset.placeholder(), "{{ASSET:0}}");
169
170 let asset2 = PendingAsset {
171 index: 42,
172 local_path: PathBuf::from("/tmp/photo.jpg"),
173 original_ref: "photo.jpg".to_string(),
174 };
175 assert_eq!(asset2.placeholder(), "{{ASSET:42}}");
176 }
177
178 #[test]
179 fn test_pending_asset_list_placeholder_for() {
180 let list = PendingAssetList {
181 assets: vec![
182 PendingAsset {
183 index: 0,
184 local_path: PathBuf::from("/tmp/a.png"),
185 original_ref: "a.png".to_string(),
186 },
187 PendingAsset {
188 index: 1,
189 local_path: PathBuf::from("/tmp/b.jpg"),
190 original_ref: "b.jpg".to_string(),
191 },
192 ],
193 };
194 assert_eq!(
195 list.placeholder_for("a.png"),
196 Some("{{ASSET:0}}".to_string())
197 );
198 assert_eq!(
199 list.placeholder_for("b.jpg"),
200 Some("{{ASSET:1}}".to_string())
201 );
202 assert_eq!(list.placeholder_for("c.gif"), None);
203 }
204
205 #[test]
206 fn test_deferred_assets_empty() {
207 let da = DeferredAssets::empty();
208 assert!(da.pending.assets.is_empty());
209 assert_eq!(da.strategy, AssetStrategy::Copy);
210 assert!(da.resolved.is_empty());
211 assert!(!da.needs_materialize());
212 assert!(da.is_resolved()); }
214
215 #[test]
216 fn test_deferred_assets_needs_materialize() {
217 let pending = PendingAssetList {
218 assets: vec![PendingAsset {
219 index: 0,
220 local_path: PathBuf::from("/tmp/a.png"),
221 original_ref: "a.png".to_string(),
222 }],
223 };
224
225 let da_external = DeferredAssets::new(pending.clone(), AssetStrategy::External);
227 assert!(da_external.needs_materialize());
228
229 let da_upload = DeferredAssets::new(pending.clone(), AssetStrategy::Upload);
231 assert!(da_upload.needs_materialize());
232
233 let da_copy = DeferredAssets::new(pending.clone(), AssetStrategy::Copy);
235 assert!(!da_copy.needs_materialize());
236
237 let da_embed = DeferredAssets::new(pending, AssetStrategy::Embed);
239 assert!(!da_embed.needs_materialize());
240 }
241
242 #[test]
243 fn test_deferred_assets_is_resolved() {
244 let pending = PendingAssetList {
245 assets: vec![
246 PendingAsset {
247 index: 0,
248 local_path: PathBuf::from("/tmp/a.png"),
249 original_ref: "a.png".to_string(),
250 },
251 PendingAsset {
252 index: 1,
253 local_path: PathBuf::from("/tmp/b.jpg"),
254 original_ref: "b.jpg".to_string(),
255 },
256 ],
257 };
258
259 let mut da = DeferredAssets::new(pending, AssetStrategy::External);
260 assert!(!da.is_resolved());
261
262 da.resolved
263 .insert(0, "https://cdn.example.com/a.png".to_string());
264 assert!(!da.is_resolved());
265
266 da.resolved
267 .insert(1, "https://cdn.example.com/b.jpg".to_string());
268 assert!(da.is_resolved());
269 }
270
271 #[test]
272 fn test_build_pending_asset_list() {
273 let content_path = PathBuf::from("/project/content/my-post");
274 let assets = vec![
275 PathBuf::from("image.png"),
276 PathBuf::from("./assets/photo.jpg"),
277 ];
278
279 let list = build_pending_asset_list(&assets, &content_path);
280 assert_eq!(list.assets.len(), 2);
281
282 assert_eq!(list.assets[0].index, 0);
283 assert_eq!(
284 list.assets[0].local_path,
285 PathBuf::from("/project/content/my-post/image.png")
286 );
287 assert_eq!(list.assets[0].original_ref, "image.png");
288
289 assert_eq!(list.assets[1].index, 1);
290 assert_eq!(
291 list.assets[1].local_path,
292 PathBuf::from("/project/content/my-post/./assets/photo.jpg")
293 );
294 assert_eq!(list.assets[1].original_ref, "./assets/photo.jpg");
295 }
296}