From 960ddd20cea8a79dad4a39832bd0e5a142170777 Mon Sep 17 00:00:00 2001 From: grabbit Date: Wed, 18 Feb 2026 09:39:58 +0800 Subject: [PATCH] Add incremental warmup with cache check and auto-warmup on startup Warmup now checks the rclone VFS cache directory before reading each file through the FUSE mount, skipping already-cached files for fast re-runs. Also adds WarmupConfig with configurable rules that auto-execute when the supervisor starts (best-effort, non-blocking). Co-Authored-By: Claude Opus 4.6 --- warpgate/src/cli/warmup.rs | 28 +++++++++++++++++++++-- warpgate/src/config.rs | 31 ++++++++++++++++++++++++++ warpgate/src/supervisor.rs | 15 +++++++++++++ warpgate/templates/config.toml.default | 11 +++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/warpgate/src/cli/warmup.rs b/warpgate/src/cli/warmup.rs index 4acd576..863ed1c 100644 --- a/warpgate/src/cli/warmup.rs +++ b/warpgate/src/cli/warmup.rs @@ -59,9 +59,15 @@ pub fn run(config: &Config, path: &str, newer_than: Option<&str>) -> Result<()> println!("Found {total} files to cache."); let mut cached = 0usize; + let mut skipped = 0usize; let mut errors = 0usize; for file in &files { + if is_cached(config, path, file) { + skipped += 1; + continue; + } + let full_path = warmup_path.join(file); match std::fs::File::open(&full_path) { Ok(mut f) => { @@ -71,7 +77,7 @@ pub fn run(config: &Config, path: &str, newer_than: Option<&str>) -> Result<()> errors += 1; } else { cached += 1; - eprint!("\r Cached {cached}/{total}"); + eprint!("\r Cached {cached}/{total} (skipped {skipped})"); } } Err(e) => { @@ -82,6 +88,24 @@ pub fn run(config: &Config, path: &str, newer_than: Option<&str>) -> Result<()> } eprintln!(); - println!("Warmup complete: {cached} cached, {errors} errors."); + println!( + "Warmup complete: skipped {skipped} (already cached), cached {cached}, errors {errors}." + ); Ok(()) } + +/// Check if a file is already in the rclone VFS cache. +/// +/// `warmup_path` is the subdir passed to `warpgate warmup` (e.g. "Image/2026"). +/// `relative_path` is the filename from `rclone lsf` (relative to warmup_path). +fn is_cached(config: &Config, warmup_path: &str, relative_path: &str) -> bool { + let cache_path = config + .cache + .dir + .join("vfs") + .join("nas") + .join(config.connection.remote_path.trim_start_matches('/')) + .join(warmup_path) + .join(relative_path); + cache_path.exists() +} diff --git a/warpgate/src/config.rs b/warpgate/src/config.rs index 6daf2f3..3e9be59 100644 --- a/warpgate/src/config.rs +++ b/warpgate/src/config.rs @@ -22,6 +22,8 @@ pub struct Config { pub directory_cache: DirectoryCacheConfig, pub protocols: ProtocolsConfig, pub mount: MountConfig, + #[serde(default)] + pub warmup: WarmupConfig, } /// SFTP connection to remote NAS. @@ -141,6 +143,35 @@ pub struct MountConfig { pub point: PathBuf, } +/// Warmup configuration — auto-cache paths on startup. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WarmupConfig { + /// Auto-warmup on startup (default: true when rules exist). + #[serde(default = "default_true")] + pub auto: bool, + /// Warmup rules — paths to pre-cache. + #[serde(default)] + pub rules: Vec, +} + +impl Default for WarmupConfig { + fn default() -> Self { + Self { + auto: true, + rules: Vec::new(), + } + } +} + +/// A single warmup rule specifying a path to pre-cache. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WarmupRule { + /// Path relative to remote_path. + pub path: String, + /// Only cache files newer than this (e.g. "7d", "24h"). + pub newer_than: Option, +} + // --- Default value functions --- fn default_sftp_port() -> u16 { diff --git a/warpgate/src/supervisor.rs b/warpgate/src/supervisor.rs index 71bfbce..137fa2b 100644 --- a/warpgate/src/supervisor.rs +++ b/warpgate/src/supervisor.rs @@ -106,6 +106,21 @@ pub fn run(config: &Config) -> Result<()> { println!("Starting protocol services..."); let mut protocols = start_protocols(config)?; + // Phase 3.5: Auto-warmup (non-blocking, best-effort) + if !config.warmup.rules.is_empty() && config.warmup.auto { + println!("Running auto-warmup..."); + for rule in &config.warmup.rules { + if shutdown.load(Ordering::SeqCst) { + break; + } + if let Err(e) = + crate::cli::warmup::run(config, &rule.path, rule.newer_than.as_deref()) + { + eprintln!("Warmup warning: {e}"); + } + } + } + // Phase 4: Supervision loop println!("Supervision active. Press Ctrl+C to stop."); let result = supervise(config, &mut mount_child, &mut protocols, Arc::clone(&shutdown)); diff --git a/warpgate/templates/config.toml.default b/warpgate/templates/config.toml.default index e679fad..70a9848 100644 --- a/warpgate/templates/config.toml.default +++ b/warpgate/templates/config.toml.default @@ -70,3 +70,14 @@ webdav_port = 8080 [mount] # FUSE mount point (all protocols share this) point = "/mnt/nas-photos" + +[warmup] +# Auto-warmup configured paths on startup +auto = true + +# [[warmup.rules]] +# path = "2024" +# newer_than = "30d" +# +# [[warmup.rules]] +# path = "Lightroom/Catalog"