Skip to main content

sz_rust_cli/cmd/
cache.rs

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