Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(assets): Make it possible to configure the location of the assets cache (#68)

* feat(assets): Make it possible to configure the location of the assets cache

* refactor: make ImageCache less weird

* fix: pass cache as param

* fix: neat

* fix: oops

* feat: make ImageCache mutex and Arc internal

* fix: do cli change differently

* chore: changeset

authored by

Erika and committed by
GitHub
2ebedf8c fd6ad9d4

+329 -101
+1 -1
crates/maudit-cli/src/init.rs
··· 226 226 info!(name: "SKIP_FORMAT", "👑 {} {}! Next steps:", "Project created".bold(), "successfully".green().to_string().bold()); 227 227 println!(); 228 228 229 - let enter_directory = if project_path != PathBuf::from(".") { 229 + let enter_directory = if project_path.to_string_lossy() != "." { 230 230 format!( 231 231 "1. Run {} to enter your project's directory.\n2. ", 232 232 format!("cd {}", project_path.display())
+43 -5
crates/maudit/src/assets.rs
··· 15 15 pub use style::{Style, StyleOptions}; 16 16 pub use tailwind::TailwindPlugin; 17 17 18 + use crate::assets::image_cache::ImageCache; 18 19 use crate::{AssetHashingStrategy, BuildOptions}; 19 20 20 21 #[derive(Default)] ··· 24 25 pub styles: FxHashSet<Style>, 25 26 26 27 pub(crate) options: RouteAssetsOptions, 28 + pub(crate) image_cache: Option<ImageCache>, 27 29 } 28 30 29 31 #[derive(Clone)] ··· 47 49 } 48 50 49 51 impl RouteAssets { 50 - pub fn new(assets_options: &RouteAssetsOptions) -> Self { 52 + pub fn new(assets_options: &RouteAssetsOptions, image_cache: Option<ImageCache>) -> Self { 51 53 Self { 52 54 options: assets_options.clone(), 55 + image_cache, 53 56 ..Default::default() 54 57 } 55 58 } 56 59 60 + /// Get a placeholder for an image using the cache if available 61 + pub fn get_image_placeholder( 62 + &self, 63 + image_path: &Path, 64 + ) -> Option<crate::assets::image::ImagePlaceholder> { 65 + if let Some(cache) = &self.image_cache { 66 + use base64::Engine; 67 + use log::debug; 68 + 69 + if let Some(cached) = cache.get_placeholder(image_path) { 70 + debug!("Using cached placeholder for {}", image_path.display()); 71 + let thumbhash_base64 = 72 + base64::engine::general_purpose::STANDARD.encode(&cached.thumbhash); 73 + return Some(crate::assets::image::ImagePlaceholder::new( 74 + cached.thumbhash, 75 + thumbhash_base64, 76 + )); 77 + } 78 + } 79 + None 80 + } 81 + 82 + /// Cache a placeholder for an image 83 + pub fn cache_image_placeholder(&self, image_path: &Path, thumbhash: Vec<u8>) { 84 + if let Some(cache) = &self.image_cache { 85 + cache.cache_placeholder(image_path, thumbhash); 86 + } 87 + } 88 + 57 89 pub fn assets(&self) -> impl Iterator<Item = &dyn Asset> { 58 90 self.images 59 91 .iter() ··· 97 129 }), 98 130 ); 99 131 100 - let image = Image::new(image_path, image_options, hash, &self.options); 132 + let image = Image::new( 133 + image_path, 134 + image_options, 135 + hash, 136 + &self.options, 137 + self.image_cache.clone(), 138 + ); 101 139 102 140 self.images.insert(image.clone()); 103 141 ··· 584 622 let temp_dir = setup_temp_dir(); 585 623 let style_path = temp_dir.join("style.css"); 586 624 587 - let mut page_assets = RouteAssets::new(&RouteAssetsOptions::default()); 625 + let mut page_assets = RouteAssets::new(&RouteAssetsOptions::default(), None); 588 626 589 627 // Test that different tailwind options produce different hashes 590 628 let style_default = page_assets.add_style(&style_path); ··· 609 647 std::fs::write(&style1_path, content).unwrap(); 610 648 std::fs::write(&style2_path, content).unwrap(); 611 649 612 - let mut page_assets = RouteAssets::new(&RouteAssetsOptions::default()); 650 + let mut page_assets = RouteAssets::new(&RouteAssetsOptions::default(), None); 613 651 614 652 let style1 = page_assets.add_style(&style1_path); 615 653 let style2 = page_assets.add_style(&style2_path); ··· 626 664 let style_path = temp_dir.join("dynamic_style.css"); 627 665 628 666 let assets_options = RouteAssetsOptions::default(); 629 - let mut page_assets = RouteAssets::new(&assets_options); 667 + let mut page_assets = RouteAssets::new(&assets_options, None); 630 668 631 669 // Write first content and get hash 632 670 std::fs::write(&style_path, "body { background: red; }").unwrap();
+52 -20
crates/maudit/src/assets/image.rs
··· 7 7 use log::debug; 8 8 use thumbhash::{rgba_to_thumb_hash, thumb_hash_to_average_rgba, thumb_hash_to_rgba}; 9 9 10 - use super::image_cache::ImageCache; 10 + use crate::assets::image_cache::ImageCache; 11 11 use crate::assets::{RouteAssetsOptions, make_filename, make_final_path, make_final_url}; 12 12 use crate::is_dev; 13 13 ··· 78 78 /// } 79 79 /// } 80 80 /// ``` 81 - #[derive(Clone, Debug, Hash, PartialEq, Eq)] 81 + #[derive(Clone, Debug)] 82 82 pub struct Image { 83 83 pub path: PathBuf, 84 84 pub(crate) hash: String, ··· 87 87 pub(crate) filename: PathBuf, 88 88 pub(crate) url: String, 89 89 pub(crate) build_path: PathBuf, 90 + pub(crate) cache: Option<ImageCache>, 90 91 } 91 92 93 + impl Hash for Image { 94 + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { 95 + self.path.hash(state); 96 + self.hash.hash(state); 97 + self.options.hash(state); 98 + self.filename.hash(state); 99 + self.url.hash(state); 100 + self.build_path.hash(state); 101 + // Intentionally exclude cache from hash 102 + } 103 + } 104 + 105 + impl PartialEq for Image { 106 + fn eq(&self, other: &Self) -> bool { 107 + self.path == other.path 108 + && self.hash == other.hash 109 + && self.options == other.options 110 + && self.filename == other.filename 111 + && self.url == other.url 112 + && self.build_path == other.build_path 113 + // Intentionally exclude cache from equality 114 + } 115 + } 116 + 117 + impl Eq for Image {} 118 + 92 119 impl Image { 93 120 pub fn new( 94 121 path: PathBuf, 95 122 image_options: Option<ImageOptions>, 96 123 hash: String, 97 124 route_assets_options: &RouteAssetsOptions, 125 + cache: Option<ImageCache>, 98 126 ) -> Self { 99 127 let filename = make_filename( 100 128 &path, ··· 119 147 filename, 120 148 url, 121 149 build_path, 150 + cache, 122 151 } 123 152 } 124 153 ··· 126 155 /// 127 156 /// This uses the [ThumbHash](https://evanw.github.io/thumbhash/) algorithm to generate a very small placeholder image. 128 157 pub fn placeholder(&self) -> ImagePlaceholder { 129 - get_placeholder(&self.path) 158 + get_placeholder(&self.path, self.cache.as_ref()) 130 159 } 131 160 132 161 // Get the dimensions of an image. Note that at this time, unsupported file formats such as SVGs will return (0, 0). ··· 166 195 } 167 196 168 197 impl ImagePlaceholder { 198 + pub fn new(thumbhash: Vec<u8>, thumbhash_base64: String) -> Self { 199 + Self { 200 + thumbhash, 201 + thumbhash_base64, 202 + average_rgba_cache: OnceLock::new(), 203 + data_uri_cache: OnceLock::new(), 204 + } 205 + } 206 + 169 207 pub fn average_rgba(&self) -> Option<(u8, u8, u8, u8)> { 170 208 *self.average_rgba_cache.get_or_init(|| { 171 209 let start = Instant::now(); ··· 220 258 } 221 259 } 222 260 223 - fn get_placeholder(path: &PathBuf) -> ImagePlaceholder { 224 - // Check cache first 225 - if let Some(cached) = ImageCache::get_placeholder(path) { 261 + fn get_placeholder(path: &PathBuf, cache: Option<&ImageCache>) -> ImagePlaceholder { 262 + // Check cache first if provided 263 + if let Some(cache) = cache 264 + && let Some(cached) = cache.get_placeholder(path) 265 + { 226 266 debug!("Using cached placeholder for {}", path.display()); 227 267 let thumbhash_base64 = base64::engine::general_purpose::STANDARD.encode(&cached.thumbhash); 228 - return ImagePlaceholder { 229 - thumbhash: cached.thumbhash, 230 - thumbhash_base64, 231 - average_rgba_cache: OnceLock::new(), 232 - data_uri_cache: OnceLock::new(), 233 - }; 268 + return ImagePlaceholder::new(cached.thumbhash, thumbhash_base64); 234 269 } 235 270 236 271 let total_start = Instant::now(); ··· 289 324 path.display() 290 325 ); 291 326 292 - // Cache the result 293 - ImageCache::cache_placeholder(path, thumb_hash.clone()); 327 + // Cache the result if cache is provided 328 + if let Some(cache) = cache { 329 + cache.cache_placeholder(path, thumb_hash.clone()); 330 + } 294 331 295 - ImagePlaceholder { 296 - thumbhash: thumb_hash, 297 - thumbhash_base64, 298 - average_rgba_cache: OnceLock::new(), 299 - data_uri_cache: OnceLock::new(), 300 - } 332 + ImagePlaceholder::new(thumb_hash, thumbhash_base64) 301 333 } 302 334 303 335 /// Port of https://github.com/evanw/thumbhash/blob/a652ce6ed691242f459f468f0a8756cda3b90a82/js/thumbhash.js#L234
+182 -51
crates/maudit/src/assets/image_cache.rs
··· 1 1 use std::{ 2 2 fs, 3 3 path::{Path, PathBuf}, 4 - sync::{Mutex, OnceLock}, 4 + sync::{Arc, Mutex, MutexGuard}, 5 5 }; 6 6 7 7 use base64::Engine; 8 8 use log::debug; 9 9 use rustc_hash::FxHashMap; 10 10 11 - // TODO: Make this configurable 12 - pub const IMAGE_CACHE_DIR: &str = "target/maudit_cache/images"; 11 + pub const DEFAULT_IMAGE_CACHE_DIR: &str = "target/maudit_cache/images"; 13 12 pub const MANIFEST_VERSION: u32 = 1; 14 13 15 14 #[derive(Debug, Clone)] ··· 31 30 transformed: FxHashMap<PathBuf, TransformedImageCacheEntry>, 32 31 } 33 32 34 - pub struct ImageCache { 33 + #[derive(Debug)] 34 + struct ImageCacheInner { 35 35 manifest: CacheManifest, 36 36 cache_dir: PathBuf, 37 37 manifest_path: PathBuf, 38 38 } 39 39 40 - static CACHE: OnceLock<Mutex<ImageCache>> = OnceLock::new(); 40 + #[derive(Debug, Clone)] 41 + pub struct ImageCache(Arc<Mutex<ImageCacheInner>>); 42 + 43 + impl Default for ImageCache { 44 + fn default() -> Self { 45 + Self::new() 46 + } 47 + } 48 + 49 + impl ImageCacheInner { 50 + pub fn new() -> Self { 51 + Self::with_cache_dir(DEFAULT_IMAGE_CACHE_DIR) 52 + } 41 53 42 - impl ImageCache { 43 - fn new() -> Self { 44 - let cache_dir = PathBuf::from(IMAGE_CACHE_DIR); 54 + pub fn with_cache_dir<P: AsRef<Path>>(cache_dir_path: P) -> Self { 55 + let cache_dir = cache_dir_path.as_ref().to_path_buf(); 45 56 let manifest_path = cache_dir.join("manifest"); 46 57 47 58 // Create cache directory if it doesn't exist ··· 141 152 Some(manifest) 142 153 } 143 154 144 - fn get() -> &'static Mutex<ImageCache> { 145 - CACHE.get_or_init(|| Mutex::new(ImageCache::new())) 146 - } 147 - 148 - fn save_manifest(&self) { 155 + pub fn save_manifest(&self) { 149 156 let mut content = String::new(); 150 157 content.push_str("# Maudit Image Cache Manifest\n"); 151 158 content.push_str(&format!("version = {}\n\n", MANIFEST_VERSION)); ··· 172 179 } 173 180 174 181 /// Get cached placeholder or None if not found 175 - pub fn get_placeholder(src_path: &Path) -> Option<PlaceholderCacheEntry> { 176 - let cache = Self::get().lock().ok()?; 177 - let entry = cache.manifest.placeholders.get(src_path)?; 182 + pub fn get_placeholder(&self, src_path: &Path) -> Option<PlaceholderCacheEntry> { 183 + let entry = self.manifest.placeholders.get(src_path)?; 178 184 179 185 debug!("Placeholder cache hit for {}", src_path.display()); 180 186 Some(entry.clone()) 181 187 } 182 188 183 189 /// Cache a placeholder 184 - pub fn cache_placeholder(src_path: &Path, thumbhash: Vec<u8>) { 185 - if let Ok(mut cache) = Self::get().lock() { 186 - let entry = PlaceholderCacheEntry { thumbhash }; 190 + pub fn cache_placeholder(&mut self, src_path: &Path, thumbhash: Vec<u8>) { 191 + let entry = PlaceholderCacheEntry { thumbhash }; 187 192 188 - cache 189 - .manifest 190 - .placeholders 191 - .insert(src_path.to_path_buf(), entry); 192 - cache.save_manifest(); 193 - debug!("Cached placeholder for {}", src_path.display()); 194 - } 193 + self.manifest 194 + .placeholders 195 + .insert(src_path.to_path_buf(), entry); 196 + self.save_manifest(); 197 + debug!("Cached placeholder for {}", src_path.display()); 195 198 } 196 199 197 200 /// Get cached transformed image path or None if not found 198 - pub fn get_transformed_image(final_filename: &Path) -> Option<PathBuf> { 199 - let cache = Self::get().lock().ok()?; 200 - let entry = cache.manifest.transformed.get(final_filename)?; 201 + pub fn get_transformed_image(&self, final_filename: &Path) -> Option<PathBuf> { 202 + let entry = self.manifest.transformed.get(final_filename)?; 201 203 202 204 // Check if cached file still exists 203 205 if !entry.cached_path.exists() { ··· 217 219 } 218 220 219 221 /// Cache a transformed image 220 - pub fn cache_transformed_image(final_filename: &Path, cached_path: PathBuf) { 221 - if let Ok(mut cache) = Self::get().lock() { 222 - let entry = TransformedImageCacheEntry { 223 - cached_path: cached_path.clone(), 224 - }; 222 + pub fn cache_transformed_image(&mut self, final_filename: &Path, cached_path: PathBuf) { 223 + let entry = TransformedImageCacheEntry { 224 + cached_path: cached_path.clone(), 225 + }; 225 226 226 - cache 227 - .manifest 228 - .transformed 229 - .insert(final_filename.to_path_buf(), entry); 230 - cache.save_manifest(); 231 - debug!( 232 - "Cached transformed image {} -> {}", 233 - final_filename.display(), 234 - cached_path.display() 235 - ); 227 + self.manifest 228 + .transformed 229 + .insert(final_filename.to_path_buf(), entry); 230 + self.save_manifest(); 231 + debug!( 232 + "Cached transformed image {} -> {}", 233 + final_filename.display(), 234 + cached_path.display() 235 + ); 236 + } 237 + 238 + /// Get the cache directory path 239 + pub fn get_cache_dir(&self) -> &PathBuf { 240 + &self.cache_dir 241 + } 242 + 243 + /// Generate a cache path for a transformed image 244 + pub fn generate_cache_path(&self, final_filename: &Path) -> PathBuf { 245 + self.cache_dir.join(final_filename) 246 + } 247 + } 248 + 249 + impl ImageCache { 250 + pub fn new() -> Self { 251 + Self(Arc::new(Mutex::new(ImageCacheInner::new()))) 252 + } 253 + 254 + pub fn with_cache_dir<P: AsRef<Path>>(cache_dir_path: P) -> Self { 255 + Self(Arc::new(Mutex::new(ImageCacheInner::with_cache_dir( 256 + cache_dir_path, 257 + )))) 258 + } 259 + 260 + fn lock_inner(&'_ self) -> MutexGuard<'_, ImageCacheInner> { 261 + match self.0.lock() { 262 + Ok(guard) => guard, 263 + Err(poisoned) => { 264 + debug!("ImageCache mutex was poisoned, recovering"); 265 + // This should be fine for our use case because the data won't be corrupted 266 + poisoned.into_inner() 267 + } 236 268 } 269 + } 270 + 271 + /// Get cached placeholder or None if not found 272 + pub fn get_placeholder(&self, src_path: &Path) -> Option<PlaceholderCacheEntry> { 273 + self.lock_inner().get_placeholder(src_path) 274 + } 275 + 276 + /// Cache a placeholder 277 + pub fn cache_placeholder(&self, src_path: &Path, thumbhash: Vec<u8>) { 278 + self.lock_inner().cache_placeholder(src_path, thumbhash) 279 + } 280 + 281 + /// Get cached transformed image path or None if not found 282 + pub fn get_transformed_image(&self, final_filename: &Path) -> Option<PathBuf> { 283 + self.lock_inner().get_transformed_image(final_filename) 284 + } 285 + 286 + /// Cache a transformed image 287 + pub fn cache_transformed_image(&self, final_filename: &Path, cached_path: PathBuf) { 288 + self.lock_inner() 289 + .cache_transformed_image(final_filename, cached_path) 290 + } 291 + 292 + /// Get the cache directory path 293 + pub fn get_cache_dir(&self) -> PathBuf { 294 + self.lock_inner().get_cache_dir().clone() 237 295 } 238 296 239 297 /// Generate a cache path for a transformed image 240 - pub fn generate_cache_path(final_filename: &Path) -> PathBuf { 241 - if let Ok(cache) = Self::get().lock() { 242 - cache.cache_dir.join(final_filename) 243 - } else { 244 - // Fallback path if cache is unavailable 245 - PathBuf::from(IMAGE_CACHE_DIR).join(final_filename) 246 - } 298 + pub fn generate_cache_path(&self, final_filename: &Path) -> PathBuf { 299 + self.lock_inner().generate_cache_path(final_filename) 300 + } 301 + 302 + /// Save the manifest to disk 303 + pub fn save_manifest(&self) { 304 + self.lock_inner().save_manifest() 305 + } 306 + } 307 + 308 + #[cfg(test)] 309 + mod tests { 310 + use super::*; 311 + use std::env; 312 + 313 + #[test] 314 + fn test_configurable_cache_dir() { 315 + let custom_cache_dir = env::temp_dir().join("test_maudit_cache"); 316 + 317 + // Create cache with custom directory 318 + let cache = ImageCache::with_cache_dir(&custom_cache_dir); 319 + 320 + // Verify the cache directory is set correctly 321 + assert_eq!(cache.get_cache_dir(), custom_cache_dir); 322 + 323 + // Test generate_cache_path uses the custom directory 324 + let test_filename = Path::new("test_image.jpg"); 325 + let cache_path = cache.generate_cache_path(test_filename); 326 + assert_eq!(cache_path, custom_cache_dir.join(test_filename)); 327 + } 328 + 329 + #[test] 330 + fn test_default_cache_dir() { 331 + // Test that the default cache directory is used when no custom dir is set 332 + let expected_default = PathBuf::from(DEFAULT_IMAGE_CACHE_DIR); 333 + 334 + // Create a new cache instance (will use default) 335 + let cache = ImageCache::new(); 336 + assert_eq!(cache.get_cache_dir(), expected_default); 337 + } 338 + 339 + #[test] 340 + fn test_build_options_integration() { 341 + use crate::build::options::{AssetsOptions, BuildOptions}; 342 + 343 + // Test that BuildOptions can configure the cache directory 344 + let custom_cache = PathBuf::from("/tmp/custom_maudit_cache"); 345 + let build_options = BuildOptions { 346 + assets: AssetsOptions { 347 + image_cache_dir: custom_cache.clone(), 348 + ..Default::default() 349 + }, 350 + ..Default::default() 351 + }; 352 + 353 + // Create cache with build options 354 + let cache = ImageCache::with_cache_dir(&build_options.assets.image_cache_dir); 355 + 356 + // Verify it uses the configured directory 357 + assert_eq!(cache.get_cache_dir(), custom_cache); 358 + } 359 + 360 + #[test] 361 + fn test_thread_safety() { 362 + use std::thread; 363 + 364 + let cache = ImageCache::new(); 365 + let cache_clone = cache.clone(); 366 + 367 + // Test that the cache can be shared across threads 368 + let handle = thread::spawn(move || { 369 + cache_clone.cache_placeholder(Path::new("test.jpg"), vec![1, 2, 3, 4]); 370 + }); 371 + 372 + handle.join().unwrap(); 373 + 374 + // Verify the placeholder was cached 375 + let entry = cache.get_placeholder(Path::new("test.jpg")); 376 + assert!(entry.is_some()); 377 + assert_eq!(entry.unwrap().thumbhash, vec![1, 2, 3, 4]); 247 378 } 248 379 }
+21 -15
crates/maudit/src/build.rs
··· 10 10 11 11 use crate::{ 12 12 BuildOptions, BuildOutput, 13 - assets::{ 14 - self, RouteAssets, TailwindPlugin, 15 - image_cache::{IMAGE_CACHE_DIR, ImageCache}, 16 - }, 13 + assets::{self, RouteAssets, TailwindPlugin, image_cache::ImageCache}, 17 14 build::images::process_image, 18 15 content::ContentSources, 19 16 is_dev, ··· 72 69 None 73 70 }; 74 71 72 + // Create the image cache early so it can be shared across routes 73 + let image_cache = ImageCache::with_cache_dir(&options.assets.image_cache_dir); 74 + let _ = fs::create_dir_all(image_cache.get_cache_dir()); 75 + 76 + // Create route_assets_options with the image cache 75 77 let route_assets_options = options.route_assets_options(); 76 78 77 79 info!(target: "build", "Output directory: {}", options.output_dir.display()); ··· 113 115 ..Default::default() 114 116 }; 115 117 118 + // This is okay, build_pages_images Hash function does not use mutable data 119 + #[allow(clippy::mutable_key_type)] 116 120 let mut build_pages_images: FxHashSet<assets::Image> = FxHashSet::default(); 117 121 let mut build_pages_scripts: FxHashSet<assets::Script> = FxHashSet::default(); 118 122 let mut build_pages_styles: FxHashSet<assets::Style> = FxHashSet::default(); ··· 130 134 RouteType::Static => { 131 135 let route_start = Instant::now(); 132 136 133 - let mut page_assets = RouteAssets::new(&route_assets_options); 137 + let mut route_assets = 138 + RouteAssets::new(&route_assets_options, Some(image_cache.clone())); 134 139 135 140 let params = PageParams::default(); 136 141 let url = cached_route.url(&params); 137 142 138 143 let result = route.build(&mut PageContext::from_static_route( 139 144 content_sources, 140 - &mut page_assets, 145 + &mut route_assets, 141 146 &url, 142 147 &options.base_url, 143 148 ))?; ··· 148 153 149 154 info!(target: "pages", "{} -> {} {}", url, file_path.to_string_lossy().dimmed(), format_elapsed_time(route_start.elapsed(), &route_format_options)); 150 155 151 - build_pages_images.extend(page_assets.images); 152 - build_pages_scripts.extend(page_assets.scripts); 153 - build_pages_styles.extend(page_assets.styles); 156 + build_pages_images.extend(route_assets.images); 157 + build_pages_scripts.extend(route_assets.scripts); 158 + build_pages_styles.extend(route_assets.styles); 154 159 155 160 build_metadata.add_page( 156 161 route.route_raw().to_string(), ··· 161 166 page_count += 1; 162 167 } 163 168 RouteType::Dynamic => { 164 - let mut page_assets = RouteAssets::new(&route_assets_options); 169 + let mut page_assets = 170 + RouteAssets::new(&route_assets_options, Some(image_cache.clone())); 165 171 166 172 let pages = route.get_pages(&mut DynamicRouteContext { 167 173 content: content_sources, ··· 296 302 if !build_pages_images.is_empty() { 297 303 print_title("processing images"); 298 304 299 - let _ = fs::create_dir_all(IMAGE_CACHE_DIR); 300 - 301 305 let start_time = Instant::now(); 302 306 build_pages_images.par_iter().for_each(|image| { 303 307 let start_process = Instant::now(); ··· 307 311 let final_filename = image.filename(); 308 312 309 313 // Check cache for transformed images 310 - if let Some(cached_path) = ImageCache::get_transformed_image(final_filename) { 314 + let cached_path = image_cache.get_transformed_image(final_filename); 315 + 316 + if let Some(cached_path) = cached_path { 311 317 // Copy from cache instead of processing 312 318 if fs::copy(&cached_path, dest_path).is_ok() { 313 319 info!(target: "assets", "{} -> {} (from cache) {}", image.path().to_string_lossy(), dest_path.to_string_lossy().dimmed(), format_elapsed_time(start_process.elapsed(), &route_format_options).dimmed()); ··· 316 322 } 317 323 318 324 // Generate cache path for transformed image 319 - let cache_path = ImageCache::generate_cache_path(final_filename); 325 + let cache_path = image_cache.generate_cache_path(final_filename); 320 326 321 327 // Process image directly to cache 322 328 process_image(image, &cache_path, image_options); ··· 324 330 // Copy from cache to destination 325 331 if fs::copy(&cache_path, dest_path).is_ok() { 326 332 // Cache the processed image path 327 - ImageCache::cache_transformed_image(final_filename, cache_path); 333 + image_cache.cache_transformed_image(final_filename, cache_path); 328 334 } else { 329 335 debug!("Failed to copy from cache {} to dest {}", cache_path.display(), dest_path.display()); 330 336 }
+13 -1
crates/maudit/src/build/options.rs
··· 1 - use std::path::PathBuf; 1 + use std::{env, path::PathBuf}; 2 2 3 3 use crate::{assets::RouteAssetsOptions, is_dev}; 4 4 ··· 35 35 /// assets: AssetsOptions { 36 36 /// assets_dir: "_assets".into(), 37 37 /// tailwind_binary_path: "./node_modules/.bin/tailwindcss".into(), 38 + /// image_cache_dir: ".cache/maudit/images".into(), 38 39 /// ..Default::default() 39 40 /// }, 40 41 /// ..Default::default() ··· 83 84 /// Note that this value is not automatically joined with the `output_dir` in `BuildOptions`. Use [`BuildOptions::route_assets_options()`] to get a `RouteAssetsOptions` with the correct final path. 84 85 pub assets_dir: PathBuf, 85 86 87 + /// Directory to use for image cache storage. 88 + /// Defaults to `target/maudit_cache/images`. 89 + /// 90 + /// This cache is used to store processed images and their placeholders to speed up subsequent builds. 91 + pub image_cache_dir: PathBuf, 92 + 86 93 /// Strategy to use when hashing assets for fingerprinting. 87 94 /// 88 95 /// Defaults to [`AssetHashingStrategy::Precise`] in production builds, and [`AssetHashingStrategy::FastImprecise`] in development builds. Note that this means that the cache isn't shared between dev and prod builds by default, if you have a lot of assets you may want to set this to the same value in both environments. ··· 102 109 Self { 103 110 tailwind_binary_path: "tailwindcss".into(), 104 111 assets_dir: "_maudit".into(), 112 + image_cache_dir: { 113 + let target_dir = 114 + env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); 115 + PathBuf::from(target_dir).join("maudit_cache/images") 116 + }, 105 117 hashing_strategy: if is_dev() { 106 118 AssetHashingStrategy::FastImprecise 107 119 } else {
+7 -4
crates/maudit/src/content/markdown/shortcodes_tests.rs
··· 60 60 use crate::{assets::RouteAssets, content::ContentSources}; 61 61 62 62 let content_sources = ContentSources::new(vec![]); 63 - let mut page_assets = RouteAssets::new(&RouteAssetsOptions { 64 - assets_dir: "assets".into(), 65 - ..Default::default() 66 - }); 63 + let mut page_assets = RouteAssets::new( 64 + &RouteAssetsOptions { 65 + assets_dir: "assets".into(), 66 + ..Default::default() 67 + }, 68 + None, 69 + ); 67 70 68 71 let mut ctx = PageContext { 69 72 content: &content_sources,
+2 -2
examples/library/src/build.rs
··· 26 26 match route.route_type() { 27 27 RouteType::Static => { 28 28 // Our page does not include content or assets, but we'll set those up for future use. 29 - let mut page_assets = RouteAssets::new(&route_assets_options); 29 + let mut page_assets = RouteAssets::new(&route_assets_options, None); 30 30 31 31 // Static and dynamic routes share the same interface for building, but static routes do not require any parameters. 32 32 // As such, we can just pass an empty set of parameters (the default for PageParams). ··· 60 60 RouteType::Dynamic => { 61 61 // Every page of a dynamic route may share a reference to the same RouteAssets instance, as it can help with caching. 62 62 // However, it is not stricly necessary, and you may want to instead create a new instance of RouteAssets especially if you were to parallelize the building of pages. 63 - let mut page_assets = RouteAssets::new(&route_assets_options); 63 + let mut page_assets = RouteAssets::new(&route_assets_options, None); 64 64 65 65 // The `get_pages` method returns all the possible pages for this route, along with their parameters and properties. 66 66 // It is very common for dynamic pages to be based on content, for instance a blog post page that has one route per blog post.
+5
website/.sampo/changesets/bold-prince-tursas.md
··· 1 + --- 2 + cargo/maudit: patch 3 + --- 4 + 5 + Adds support for customizing the location of the image cache
+3 -2
website/content/docs/library.md
··· 51 51 match route.route_type() { 52 52 RouteType::Static => { 53 53 // Our page does not include content or assets, but we'll set those up for future use. 54 - let mut route_assets = RouteAssets::new(&route_assets_options); 54 + // RouteAssets also can take a cache parameter, but we'll leave it empty for simplicity. 55 + let mut route_assets = RouteAssets::new(&route_assets_options, None); 55 56 56 57 // Static and dynamic routes share the same interface for building, but static routes do not require any parameters. 57 58 // As such, we can just pass an empty set of parameters (the default for PageParams). ··· 126 127 RouteType::Dynamic => { 127 128 // Every page of a dynamic route may share a reference to the same RouteAssets instance, as it can help with caching. 128 129 // However, it is not stricly necessary, and you may want to instead create a new instance of RouteAssets especially if you were to parallelize the building of pages. 129 - let mut page_assets = RouteAssets::new(&route_assets_options); 130 + let mut page_assets = RouteAssets::new(&route_assets_options, None); 130 131 131 132 // The `get_pages` method returns all the possible pages for this route, along with their parameters and properties. 132 133 // It is very common for dynamic pages to be based on content, for instance a blog post page that has one route per blog post.