diff options
| -rw-r--r-- | crates/mozart-registry/src/cache.rs | 70 | ||||
| -rw-r--r-- | crates/mozart/src/commands/clear_cache.rs | 12 |
2 files changed, 77 insertions, 5 deletions
diff --git a/crates/mozart-registry/src/cache.rs b/crates/mozart-registry/src/cache.rs index ac4b507..9b7d165 100644 --- a/crates/mozart-registry/src/cache.rs +++ b/crates/mozart-registry/src/cache.rs @@ -5,6 +5,7 @@ //! ~/.cache/mozart/ (or $COMPOSER_CACHE_DIR) //! files/ dist archives (key: vendor~package~reference.ext) //! repo/ API responses (key: provider-vendor~package.json) +//! vcs/ VCS mirrors (one subdir per sanitized URL) //! ``` use std::fs; @@ -23,6 +24,8 @@ pub struct CacheConfig { pub cache_files_dir: PathBuf, /// Directory for API responses. pub cache_repo_dir: PathBuf, + /// Directory for VCS mirrors (one subdirectory per sanitized URL). + pub cache_vcs_dir: PathBuf, /// TTL in seconds for repo entries (default: 15,552,000 = 6 months). pub cache_ttl: u64, /// TTL in seconds for files entries (falls back to `cache_ttl`). @@ -62,10 +65,14 @@ pub fn build_cache_config(cli_no_cache: bool) -> CacheConfig { let cache_files_dir = cache_dir.join("files"); let cache_repo_dir = cache_dir.join("repo"); + let cache_vcs_dir = std::env::var("COMPOSER_CACHE_VCS_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| cache_dir.join("vcs")); CacheConfig { cache_files_dir, cache_repo_dir, + cache_vcs_dir, cache_ttl: CacheConfig::DEFAULT_TTL, cache_files_ttl: CacheConfig::DEFAULT_TTL, cache_files_maxsize: CacheConfig::DEFAULT_FILES_MAXSIZE, @@ -246,6 +253,42 @@ impl Cache { Ok(()) } + /// Run garbage collection on a VCS cache bucket. + /// + /// Each top-level subdirectory is one bare mirror keyed by sanitized URL. + /// Deletes entire subdirectories whose mtime is older than `ttl_seconds`. + /// Mirrors Composer's `Cache::gcVcsCache`. + pub fn gc_vcs(&self, ttl_seconds: u64) -> anyhow::Result<()> { + if !self.enabled || !self.root.exists() { + return Ok(()); + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + for entry in fs::read_dir(&self.root)? { + let entry = entry?; + let path = entry.path(); + let metadata = entry.metadata()?; + if !metadata.is_dir() { + continue; + } + let mtime = metadata + .modified() + .ok() + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_secs()) + .unwrap_or(0); + if now.saturating_sub(mtime) > ttl_seconds { + let _ = fs::remove_dir_all(&path); + } + } + + Ok(()) + } + /// Return the age in seconds of a cached entry based on its mtime, /// or `None` if the entry doesn't exist or mtime can't be read. pub fn age(&self, key: &str) -> Option<u64> { @@ -461,6 +504,33 @@ mod tests { ); } + // ──────────── gc_vcs (top-level subdir TTL deletion) ──────────── + + #[test] + fn test_gc_vcs_removes_old_subdirs() { + let dir = tempdir().unwrap(); + let cache = Cache::new(dir.path().to_path_buf(), true); + + let old_mirror = dir.path().join("old-mirror"); + let new_mirror = dir.path().join("new-mirror"); + fs::create_dir_all(&old_mirror).unwrap(); + fs::write(old_mirror.join("HEAD"), "ref: refs/heads/main\n").unwrap(); + fs::create_dir_all(&new_mirror).unwrap(); + fs::write(new_mirror.join("HEAD"), "ref: refs/heads/main\n").unwrap(); + + let two_hours_ago = SystemTime::now() - Duration::from_secs(7200); + filetime::set_file_mtime( + &old_mirror, + filetime::FileTime::from_system_time(two_hours_ago), + ) + .unwrap(); + + cache.gc_vcs(3600).unwrap(); + + assert!(!old_mirror.exists(), "expired mirror should be removed"); + assert!(new_mirror.exists(), "fresh mirror should remain"); + } + // ──────────── age ──────────── #[test] diff --git a/crates/mozart/src/commands/clear_cache.rs b/crates/mozart/src/commands/clear_cache.rs index d3acc28..b9a9303 100644 --- a/crates/mozart/src/commands/clear_cache.rs +++ b/crates/mozart/src/commands/clear_cache.rs @@ -18,6 +18,7 @@ pub async fn execute( // Build the list of (key, path) pairs to process. // cache-dir is only included in full clear mode, not GC mode. let mut cache_paths: Vec<(&str, &std::path::PathBuf)> = vec![ + ("cache-vcs-dir", &config.cache_vcs_dir), ("cache-repo-dir", &config.cache_repo_dir), ("cache-files-dir", &config.cache_files_dir), ]; @@ -47,11 +48,11 @@ pub async fn execute( path.display() )); let cache = Cache::new((*path).clone(), !config.no_cache); - let result = if *key == "cache-files-dir" { - cache.gc(config.cache_files_ttl, config.cache_files_maxsize) - } else { + let result = match *key { + "cache-files-dir" => cache.gc(config.cache_files_ttl, config.cache_files_maxsize), + "cache-vcs-dir" => cache.gc_vcs(config.cache_ttl), // cache-repo-dir: 1 GB cap (matches Composer) - cache.gc(config.cache_ttl, 1024 * 1024 * 1024) + _ => cache.gc(config.cache_ttl, 1024 * 1024 * 1024), }; if let Err(e) = result { console.error(&format!("Error during GC of {key}: {e}")); @@ -64,9 +65,10 @@ pub async fn execute( for entry in std::fs::read_dir(path)? { let entry = entry?; let entry_path = entry.path(); - // Skip repo/files subdirs (cleared by their own iterations) + // Skip repo/files/vcs subdirs (cleared by their own iterations) if entry_path == config.cache_files_dir || entry_path == config.cache_repo_dir + || entry_path == config.cache_vcs_dir { continue; } |
