From 9b37c88cd5055114ad947c3b605d2f2129f15714 Mon Sep 17 00:00:00 2001 From: grabbit Date: Wed, 18 Feb 2026 00:38:42 +0800 Subject: [PATCH] Fix warmup to use VFS cache, dynamic SMB share name, smbd long flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - warmup: read files through FUSE mount instead of rclone copy to temp dir. Files now actually land in rclone VFS SSD cache. - samba: derive share name from mount point dir name instead of hardcoded [nas-photos] (e.g. /mnt/projects → [projects]) - supervisor: use smbd long flags (--foreground, --debug-stdout, --no-process-group, --configfile) for compatibility with Samba 4.19 Co-Authored-By: Claude Opus 4.6 --- warpgate/src/cli/warmup.rs | 84 +++++++++++++++++++++++++--------- warpgate/src/services/samba.rs | 10 +++- warpgate/src/supervisor.rs | 3 +- 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/warpgate/src/cli/warmup.rs b/warpgate/src/cli/warmup.rs index e9a73c9..4acd576 100644 --- a/warpgate/src/cli/warmup.rs +++ b/warpgate/src/cli/warmup.rs @@ -1,5 +1,10 @@ //! `warpgate warmup` — pre-cache a remote directory to local SSD. +//! +//! Lists files via `rclone lsf`, then reads each through the FUSE mount +//! to trigger VFS caching. This ensures files land in the rclone VFS +//! SSD cache rather than being downloaded to a throwaway temp directory. +use std::io; use std::process::Command; use anyhow::{Context, Result}; @@ -8,38 +13,75 @@ use crate::config::Config; use crate::rclone::config as rclone_config; pub fn run(config: &Config, path: &str, newer_than: Option<&str>) -> Result<()> { + let warmup_path = config.mount.point.join(path); let remote_src = format!("nas:{}/{}", config.connection.remote_path, path); - let local_dest = std::env::temp_dir().join("warpgate-warmup"); - println!("Warming up: {}", remote_src); + println!("Warming up: {remote_src}"); + println!(" via mount: {}", warmup_path.display()); - // Create temp destination for downloaded files - std::fs::create_dir_all(&local_dest) - .context("Failed to create temp directory for warmup")?; + if !warmup_path.exists() { + anyhow::bail!( + "Path not found on mount: {}. Is the mount running?", + warmup_path.display() + ); + } + // List files on remote (supports --max-age for newer_than filter) let mut cmd = Command::new("rclone"); - cmd.arg("copy") + cmd.arg("lsf") .arg("--config") .arg(rclone_config::RCLONE_CONF_PATH) - .arg(&remote_src) - .arg(&local_dest) - .arg("--no-traverse") - .arg("--progress"); + .arg("--recursive") + .arg("--files-only") + .arg(&remote_src); if let Some(age) = newer_than { cmd.arg("--max-age").arg(age); } - println!("Downloading from remote NAS..."); - let status = cmd.status().context("Failed to run rclone copy")?; - - // Clean up temp directory - let _ = std::fs::remove_dir_all(&local_dest); - - if status.success() { - println!("Warmup complete."); - Ok(()) - } else { - anyhow::bail!("rclone copy exited with status {}", status); + let output = cmd.output().context("Failed to run rclone lsf")?; + if !output.status.success() { + anyhow::bail!( + "rclone lsf failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + ); } + + let file_list = String::from_utf8_lossy(&output.stdout); + let files: Vec<&str> = file_list.lines().filter(|l| !l.is_empty()).collect(); + let total = files.len(); + + if total == 0 { + println!("No files matched."); + return Ok(()); + } + + println!("Found {total} files to cache."); + + let mut cached = 0usize; + let mut errors = 0usize; + + for file in &files { + let full_path = warmup_path.join(file); + match std::fs::File::open(&full_path) { + Ok(mut f) => { + // Stream-read through FUSE mount → populates VFS cache + if let Err(e) = io::copy(&mut f, &mut io::sink()) { + eprintln!(" Warning: read failed: {file}: {e}"); + errors += 1; + } else { + cached += 1; + eprint!("\r Cached {cached}/{total}"); + } + } + Err(e) => { + eprintln!(" Warning: open failed: {file}: {e}"); + errors += 1; + } + } + } + + eprintln!(); + println!("Warmup complete: {cached} cached, {errors} errors."); + Ok(()) } diff --git a/warpgate/src/services/samba.rs b/warpgate/src/services/samba.rs index 7a3d12e..7f6cb1d 100644 --- a/warpgate/src/services/samba.rs +++ b/warpgate/src/services/samba.rs @@ -41,8 +41,14 @@ pub fn generate(config: &Config) -> Result { writeln!(conf, " disable spoolss = yes")?; writeln!(conf)?; - // [nas-photos] share section - writeln!(conf, "[nas-photos]")?; + // Share name derived from mount point directory name + let share_name = config + .mount + .point + .file_name() + .map(|n| n.to_string_lossy()) + .unwrap_or("warpgate".into()); + writeln!(conf, "[{share_name}]")?; writeln!(conf, " comment = Warpgate cached NAS share")?; writeln!(conf, " path = {mount_point}")?; writeln!(conf, " browseable = yes")?; diff --git a/warpgate/src/supervisor.rs b/warpgate/src/supervisor.rs index f3efdd4..71bfbce 100644 --- a/warpgate/src/supervisor.rs +++ b/warpgate/src/supervisor.rs @@ -204,7 +204,8 @@ fn start_and_wait_mount(config: &Config, shutdown: &AtomicBool) -> Result /// Spawn smbd as a foreground child process. fn spawn_smbd() -> Result { Command::new("smbd") - .args(["-F", "-S", "-N", "-s", samba::SMB_CONF_PATH]) + .args(["--foreground", "--debug-stdout", "--no-process-group", + "--configfile", samba::SMB_CONF_PATH]) .spawn() .context("Failed to spawn smbd") }