diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-05 13:12:56 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-05 13:12:56 +0900 |
| commit | a10adb3b9ff3e1fe7ae0b8dc6440f171dde5f0ab (patch) | |
| tree | 5dde666652768c4cbf3294e2ce17d78daa18b0c2 /crates/mozart-registry/src/cache.rs | |
| parent | 78a627f9b839e902faec6e5f7fee4ec19fc0e4b8 (diff) | |
| download | php-mozart-a10adb3b9ff3e1fe7ae0b8dc6440f171dde5f0ab.tar.gz php-mozart-a10adb3b9ff3e1fe7ae0b8dc6440f171dde5f0ab.tar.zst php-mozart-a10adb3b9ff3e1fe7ae0b8dc6440f171dde5f0ab.zip | |
feat(cache): include cache-vcs-dir in clear-cache command
Composer's clear-cache deletes cache-vcs-dir alongside repo/files
caches, and GCs it via gcVcsCache (TTL-based, top-level subdir
deletion, no size cap). Mozart was silently leaving VCS mirrors
on disk forever — every clear-cache run grew the cache monotonically.
Add cache_vcs_dir to CacheConfig (default {cache-dir}/vcs, env
override COMPOSER_CACHE_VCS_DIR), wire it into clear-cache, and
add Cache::gc_vcs mirroring Composer's gcVcsCache semantics.
Diffstat (limited to 'crates/mozart-registry/src/cache.rs')
| -rw-r--r-- | crates/mozart-registry/src/cache.rs | 70 |
1 files changed, 70 insertions, 0 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] |
