Skip to main content

tl_package/
cache.rs

1use std::path::{Path, PathBuf};
2
3/// Manages the local package cache at ~/.tl/packages/.
4#[derive(Debug, Clone)]
5pub struct PackageCache {
6    root: PathBuf,
7}
8
9impl PackageCache {
10    /// Create a cache at the default location (~/.tl/packages/).
11    pub fn default_location() -> Result<Self, String> {
12        let home = std::env::var("HOME")
13            .or_else(|_| std::env::var("USERPROFILE"))
14            .map_err(|_| "Could not determine home directory".to_string())?;
15        Ok(PackageCache {
16            root: PathBuf::from(home).join(".tl").join("packages"),
17        })
18    }
19
20    /// Create a cache at a custom root path.
21    pub fn new(root: PathBuf) -> Self {
22        PackageCache { root }
23    }
24
25    /// Get the root directory of the cache.
26    pub fn root(&self) -> &Path {
27        &self.root
28    }
29
30    /// Get the directory for a specific package version: root/<name>/<version>/
31    pub fn package_dir(&self, name: &str, version: &str) -> PathBuf {
32        self.root.join(name).join(version)
33    }
34
35    /// Check if a package version is already cached.
36    pub fn is_cached(&self, name: &str, version: &str) -> bool {
37        self.package_dir(name, version).exists()
38    }
39
40    /// List all cached versions of a package.
41    pub fn list_versions(&self, name: &str) -> Vec<String> {
42        let pkg_dir = self.root.join(name);
43        if !pkg_dir.is_dir() {
44            return Vec::new();
45        }
46        let mut versions = Vec::new();
47        if let Ok(entries) = std::fs::read_dir(&pkg_dir) {
48            for entry in entries.flatten() {
49                if entry.path().is_dir()
50                    && let Some(name) = entry.file_name().to_str()
51                {
52                    versions.push(name.to_string());
53                }
54            }
55        }
56        versions.sort();
57        versions
58    }
59
60    /// Remove a cached package version.
61    pub fn remove(&self, name: &str, version: &str) -> Result<(), String> {
62        let dir = self.package_dir(name, version);
63        if dir.exists() {
64            std::fs::remove_dir_all(&dir)
65                .map_err(|e| format!("Failed to remove cached package: {e}"))?;
66        }
67        // Clean up empty parent directory
68        let pkg_dir = self.root.join(name);
69        if pkg_dir.exists()
70            && let Ok(entries) = std::fs::read_dir(&pkg_dir)
71            && entries.count() == 0
72        {
73            let _ = std::fs::remove_dir(&pkg_dir);
74        }
75        Ok(())
76    }
77
78    /// Find the source root for a cached package (where TL source files live).
79    /// Looks for: src/ directory, or the package root itself.
80    pub fn source_root(&self, name: &str, version: &str) -> Option<PathBuf> {
81        let dir = self.package_dir(name, version);
82        if !dir.exists() {
83            return None;
84        }
85        // If there's a src/ directory, that's the source root
86        let src_dir = dir.join("src");
87        if src_dir.is_dir() {
88            return Some(src_dir);
89        }
90        // Otherwise the package root itself
91        Some(dir)
92    }
93
94    /// Ensure the cache directory exists.
95    pub fn ensure_dir(&self) -> Result<(), String> {
96        std::fs::create_dir_all(&self.root)
97            .map_err(|e| format!("Failed to create cache directory: {e}"))
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use tempfile::TempDir;
105
106    #[test]
107    fn cache_dir_layout() {
108        let dir = TempDir::new().unwrap();
109        let cache = PackageCache::new(dir.path().to_path_buf());
110
111        let pkg_dir = cache.package_dir("mylib", "1.0.0");
112        assert!(pkg_dir.ends_with("mylib/1.0.0"));
113    }
114
115    #[test]
116    fn cache_is_cached() {
117        let dir = TempDir::new().unwrap();
118        let cache = PackageCache::new(dir.path().to_path_buf());
119
120        assert!(!cache.is_cached("mylib", "1.0.0"));
121
122        let pkg_dir = cache.package_dir("mylib", "1.0.0");
123        std::fs::create_dir_all(&pkg_dir).unwrap();
124        assert!(cache.is_cached("mylib", "1.0.0"));
125    }
126
127    #[test]
128    fn cache_list_versions() {
129        let dir = TempDir::new().unwrap();
130        let cache = PackageCache::new(dir.path().to_path_buf());
131
132        assert!(cache.list_versions("mylib").is_empty());
133
134        std::fs::create_dir_all(cache.package_dir("mylib", "1.0.0")).unwrap();
135        std::fs::create_dir_all(cache.package_dir("mylib", "2.0.0")).unwrap();
136
137        let versions = cache.list_versions("mylib");
138        assert_eq!(versions, vec!["1.0.0", "2.0.0"]);
139    }
140
141    #[test]
142    fn cache_remove() {
143        let dir = TempDir::new().unwrap();
144        let cache = PackageCache::new(dir.path().to_path_buf());
145
146        std::fs::create_dir_all(cache.package_dir("mylib", "1.0.0")).unwrap();
147        assert!(cache.is_cached("mylib", "1.0.0"));
148
149        cache.remove("mylib", "1.0.0").unwrap();
150        assert!(!cache.is_cached("mylib", "1.0.0"));
151    }
152
153    #[test]
154    fn cache_source_root() {
155        let dir = TempDir::new().unwrap();
156        let cache = PackageCache::new(dir.path().to_path_buf());
157
158        // Not cached
159        assert!(cache.source_root("mylib", "1.0.0").is_none());
160
161        // With src/ dir
162        let pkg_dir = cache.package_dir("mylib", "1.0.0");
163        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
164        let root = cache.source_root("mylib", "1.0.0").unwrap();
165        assert!(root.ends_with("src"));
166
167        // Without src/ dir
168        let cache2 = PackageCache::new(dir.path().to_path_buf());
169        let pkg_dir2 = cache2.package_dir("nolib", "1.0.0");
170        std::fs::create_dir_all(&pkg_dir2).unwrap();
171        let root2 = cache2.source_root("nolib", "1.0.0").unwrap();
172        assert!(root2.ends_with("1.0.0"));
173    }
174}