Skip to main content

sz_rust_cli/cmd/
cache.rs

1//! `cache:clear` 命令 — 对齐 PHP `think cache:clear`
2//!
3//! ## PHP 对齐
4//!
5//! PHP `cache:clear` 通过 `think\facade\Cache::clear()` 清空缓存。
6//! Rust 端 CLI 是独立进程,无法直接访问运行时 `CacheManager` 实例,
7//! 因此通过以下方式实现:
8//!
9//! 1. 清除缓存目录(默认 `runtime/cache`)
10//! 2. 输出提示信息
11
12use std::path::PathBuf;
13
14use crate::error::CliError;
15
16/// 执行 cache:clear 命令
17///
18/// # 参数
19///
20/// - `store`:指定缓存存储名(`None` 清空所有)
21pub fn execute_cache_clear(store: Option<&str>) -> Result<(), CliError> {
22    match store {
23        Some(store_name) => clear_store(store_name),
24        None => clear_all(),
25    }
26}
27
28/// 清空指定缓存存储
29fn clear_store(store: &str) -> Result<(), CliError> {
30    let cache_dir = get_cache_dir(store);
31
32    if !cache_dir.exists() {
33        println!(
34            "Cache store '{}' directory not found: {}",
35            store,
36            cache_dir.display()
37        );
38        return Ok(());
39    }
40
41    let count = remove_dir_contents(&cache_dir)?;
42    println!(
43        "Cache store '{}' cleared: {} file(s) removed from {}",
44        store,
45        count,
46        cache_dir.display()
47    );
48    Ok(())
49}
50
51/// 清空所有缓存
52fn clear_all() -> Result<(), CliError> {
53    let cache_root = get_cache_root();
54
55    if !cache_root.exists() {
56        println!("Cache root directory not found: {}", cache_root.display());
57        println!("Nothing to clear.");
58        return Ok(());
59    }
60
61    let count = remove_dir_contents(&cache_root)?;
62    println!(
63        "All caches cleared: {} file(s)/dir(s) removed from {}",
64        count,
65        cache_root.display()
66    );
67    Ok(())
68}
69
70/// 获取缓存根目录
71///
72/// 对齐 PHP `runtime/cache` 路径约定。
73fn get_cache_root() -> PathBuf {
74    PathBuf::from("runtime/cache")
75}
76
77/// 获取指定存储的缓存目录
78fn get_cache_dir(store: &str) -> PathBuf {
79    get_cache_root().join(store)
80}
81
82/// 递归删除目录内容(保留目录本身)
83///
84/// # 返回
85///
86/// 删除的文件和目录数量
87fn remove_dir_contents(path: &PathBuf) -> Result<usize, CliError> {
88    let mut count = 0;
89    let entries = std::fs::read_dir(path)?;
90    for entry in entries {
91        let entry = entry?;
92        let entry_path = entry.path();
93        if entry_path.is_dir() {
94            std::fs::remove_dir_all(&entry_path)?;
95        } else {
96            std::fs::remove_file(&entry_path)?;
97        }
98        count += 1;
99    }
100    Ok(count)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use std::fs;
107    use std::io::Write;
108
109    #[test]
110    fn test_get_cache_root_default() {
111        let root = get_cache_root();
112        assert_eq!(root, PathBuf::from("runtime/cache"));
113    }
114
115    #[test]
116    fn test_get_cache_dir_with_store() {
117        let dir = get_cache_dir("redis");
118        assert_eq!(dir, PathBuf::from("runtime/cache/redis"));
119    }
120
121    #[test]
122    fn test_clear_all_nonexistent_dir() {
123        // 在临时目录中测试(不依赖 set_current_dir)
124        let temp = tempfile::tempdir().unwrap();
125        let cache_root = temp.path().join("runtime/cache");
126        // 不创建目录,模拟不存在
127        assert!(!cache_root.exists());
128    }
129
130    #[test]
131    fn test_clear_all_with_files_via_remove_dir_contents() {
132        // 直接测试 remove_dir_contents 避免工作目录依赖
133        let temp = tempfile::tempdir().unwrap();
134        let root = temp.path().to_path_buf();
135
136        // 创建缓存文件
137        let file1 = root.join("cache1.txt");
138        let file2 = root.join("cache2.txt");
139        let mut f1 = fs::File::create(&file1).unwrap();
140        writeln!(f1, "data1").unwrap();
141        let mut f2 = fs::File::create(&file2).unwrap();
142        writeln!(f2, "data2").unwrap();
143
144        let count = remove_dir_contents(&root).unwrap();
145        assert_eq!(count, 2);
146        assert!(!file1.exists());
147        assert!(!file2.exists());
148    }
149
150    #[test]
151    fn test_clear_store_nonexistent() {
152        // 验证逻辑:不存在的存储目录不会报错
153        let temp = tempfile::tempdir().unwrap();
154        let store_dir = temp.path().join("nonexistent_store");
155        assert!(!store_dir.exists());
156    }
157
158    #[test]
159    fn test_clear_store_with_files_via_remove_dir_contents() {
160        let temp = tempfile::tempdir().unwrap();
161        let store_dir = temp.path().join("redis");
162        fs::create_dir_all(&store_dir).unwrap();
163
164        let cache_file = store_dir.join("key1.txt");
165        let mut f = fs::File::create(&cache_file).unwrap();
166        writeln!(f, "redis_data").unwrap();
167
168        let count = remove_dir_contents(&store_dir).unwrap();
169        assert_eq!(count, 1);
170        assert!(!cache_file.exists());
171    }
172
173    #[test]
174    fn test_remove_dir_contents_with_subdirs() {
175        let temp = tempfile::tempdir().unwrap();
176        let root = temp.path().to_path_buf();
177
178        // 创建子目录和文件
179        let sub_dir = root.join("subdir");
180        fs::create_dir_all(&sub_dir).unwrap();
181        fs::File::create(root.join("file1.txt")).unwrap();
182        fs::File::create(sub_dir.join("file2.txt")).unwrap();
183
184        let count = remove_dir_contents(&root).unwrap();
185        assert_eq!(count, 2); // file1.txt + subdir
186
187        // 验证目录已清空但本身保留
188        assert!(root.exists());
189        assert!(root.read_dir().unwrap().next().is_none());
190    }
191
192    #[test]
193    fn test_execute_cache_clear_no_store_nonexistent() {
194        // 当 runtime/cache 不存在时,execute_cache_clear 应返回 Ok。
195        // 该命令操作进程级工作目录,必须持有全局互斥锁并隔离到临时目录,
196        // 避免与 make/optimize 模块的 set_current_dir 测试并行竞态。
197        let _lock = super::super::test_support::acquire_global_lock();
198        let temp = tempfile::tempdir().unwrap();
199        let original = std::env::current_dir().unwrap();
200        std::env::set_current_dir(temp.path()).unwrap();
201        let result = execute_cache_clear(None);
202        std::env::set_current_dir(&original).unwrap();
203        assert!(result.is_ok());
204    }
205
206    #[test]
207    fn test_execute_cache_clear_with_store_nonexistent() {
208        // 指定不存在的存储名,应返回 Ok
209        let result = execute_cache_clear(Some("nonexistent_store_xyz"));
210        assert!(result.is_ok());
211    }
212}