tldr_cli/commands/daemon/
cache_clear.rs1use std::fs;
18use std::path::{Path, PathBuf};
19
20use clap::Args;
21use serde::Serialize;
22
23use crate::output::OutputFormat;
24
25use super::error::DaemonResult;
26use super::ipc::send_command;
27use super::types::DaemonCommand;
28
29#[derive(Debug, Clone, Args)]
35pub struct CacheClearArgs {
36 #[arg(long, short = 'p', default_value = ".")]
38 pub project: PathBuf,
39}
40
41#[derive(Debug, Clone, Serialize)]
47pub struct CacheClearOutput {
48 pub status: String,
50 pub files_removed: usize,
52 pub bytes_freed: u64,
54 pub size_freed_human: String,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub message: Option<String>,
59}
60
61impl CacheClearArgs {
66 pub fn run(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
68 let runtime = tokio::runtime::Runtime::new()?;
70 runtime.block_on(self.run_async(format, quiet))
71 }
72
73 async fn run_async(&self, format: OutputFormat, quiet: bool) -> anyhow::Result<()> {
75 let project = self.project.canonicalize().unwrap_or_else(|_| {
77 std::env::current_dir()
78 .unwrap_or_else(|_| PathBuf::from("."))
79 .join(&self.project)
80 });
81
82 self.try_stop_daemon(&project).await;
85
86 let (files_removed, bytes_freed) = self.clear_cache_files(&project)?;
88
89 let output = if files_removed == 0 {
90 CacheClearOutput {
91 status: "ok".to_string(),
92 files_removed: 0,
93 bytes_freed: 0,
94 size_freed_human: "0 B".to_string(),
95 message: Some("No cache directory found".to_string()),
96 }
97 } else {
98 CacheClearOutput {
99 status: "ok".to_string(),
100 files_removed,
101 bytes_freed,
102 size_freed_human: format_bytes(bytes_freed),
103 message: Some(format!("Cache cleared: {} file(s) removed", files_removed)),
104 }
105 };
106
107 self.print_output(&output, format, quiet)
108 }
109
110 async fn try_stop_daemon(&self, project: &Path) {
112 let cmd = DaemonCommand::Shutdown;
113 let _ = send_command(project, &cmd).await;
115 }
116
117 fn clear_cache_files(&self, project: &Path) -> DaemonResult<(usize, u64)> {
119 let cache_dir = project.join(".tldr").join("cache");
120
121 if !cache_dir.exists() {
122 return Ok((0, 0));
123 }
124
125 let mut files_removed = 0;
126 let mut bytes_freed = 0u64;
127
128 let entries: Vec<_> = fs::read_dir(&cache_dir)?
130 .filter_map(|e| e.ok())
131 .filter(|e| e.metadata().map(|m| m.is_file()).unwrap_or(false))
132 .collect();
133
134 for entry in entries {
136 let path = entry.path();
137 if let Ok(metadata) = entry.metadata() {
138 bytes_freed += metadata.len();
139 }
140 if fs::remove_file(&path).is_ok() {
141 files_removed += 1;
142 }
143 }
144
145 Ok((files_removed, bytes_freed))
146 }
147
148 fn print_output(
150 &self,
151 output: &CacheClearOutput,
152 format: OutputFormat,
153 quiet: bool,
154 ) -> anyhow::Result<()> {
155 if quiet {
156 return Ok(());
157 }
158
159 match format {
160 OutputFormat::Json | OutputFormat::Compact => {
161 println!("{}", serde_json::to_string_pretty(output)?);
162 }
163 OutputFormat::Text | OutputFormat::Sarif | OutputFormat::Dot => {
164 if output.files_removed == 0 {
165 println!("No cache directory found");
166 } else {
167 println!(
168 "Cache cleared: {} file(s) removed ({})",
169 output.files_removed, output.size_freed_human
170 );
171 }
172 }
173 }
174
175 Ok(())
176 }
177}
178
179fn format_bytes(bytes: u64) -> String {
185 const KB: u64 = 1024;
186 const MB: u64 = KB * 1024;
187 const GB: u64 = MB * 1024;
188
189 if bytes >= GB {
190 format!("{:.1} GB", bytes as f64 / GB as f64)
191 } else if bytes >= MB {
192 format!("{:.1} MB", bytes as f64 / MB as f64)
193 } else if bytes >= KB {
194 format!("{:.1} KB", bytes as f64 / KB as f64)
195 } else {
196 format!("{} B", bytes)
197 }
198}
199
200#[cfg(test)]
205mod tests {
206 use super::*;
207 use tempfile::TempDir;
208
209 #[test]
210 fn test_cache_clear_args_default() {
211 let args = CacheClearArgs {
212 project: PathBuf::from("."),
213 };
214 assert_eq!(args.project, PathBuf::from("."));
215 }
216
217 #[test]
218 fn test_format_bytes() {
219 assert_eq!(format_bytes(0), "0 B");
220 assert_eq!(format_bytes(500), "500 B");
221 assert_eq!(format_bytes(1024), "1.0 KB");
222 assert_eq!(format_bytes(1536), "1.5 KB");
223 assert_eq!(format_bytes(1048576), "1.0 MB");
224 assert_eq!(format_bytes(1073741824), "1.0 GB");
225 }
226
227 #[test]
228 fn test_cache_clear_output_serialization() {
229 let output = CacheClearOutput {
230 status: "ok".to_string(),
231 files_removed: 26,
232 bytes_freed: 1048576,
233 size_freed_human: "1.0 MB".to_string(),
234 message: Some("Cache cleared: 26 file(s) removed".to_string()),
235 };
236
237 let json = serde_json::to_string(&output).unwrap();
238 assert!(json.contains("ok"));
239 assert!(json.contains("26"));
240 assert!(json.contains("1048576"));
241 assert!(json.contains("1.0 MB"));
242 }
243
244 #[test]
245 fn test_cache_clear_output_empty() {
246 let output = CacheClearOutput {
247 status: "ok".to_string(),
248 files_removed: 0,
249 bytes_freed: 0,
250 size_freed_human: "0 B".to_string(),
251 message: Some("No cache directory found".to_string()),
252 };
253
254 let json = serde_json::to_string(&output).unwrap();
255 assert!(json.contains("No cache directory found"));
256 }
257
258 #[test]
259 fn test_clear_cache_files_no_cache_dir() {
260 let temp = TempDir::new().unwrap();
261 let args = CacheClearArgs {
262 project: temp.path().to_path_buf(),
263 };
264
265 let result = args.clear_cache_files(temp.path());
266 assert!(result.is_ok());
267 let (files, bytes) = result.unwrap();
268 assert_eq!(files, 0);
269 assert_eq!(bytes, 0);
270 }
271
272 #[test]
273 fn test_clear_cache_files_with_files() {
274 let temp = TempDir::new().unwrap();
275 let cache_dir = temp.path().join(".tldr").join("cache");
276 fs::create_dir_all(&cache_dir).unwrap();
277
278 fs::write(cache_dir.join("salsa_cache.bin"), "test data 1").unwrap();
280 fs::write(cache_dir.join("call_graph.json"), r#"{"edges":[]}"#).unwrap();
281 fs::write(cache_dir.join("test.pkl"), "pickle data").unwrap();
282
283 let args = CacheClearArgs {
284 project: temp.path().to_path_buf(),
285 };
286
287 let result = args.clear_cache_files(temp.path());
288 assert!(result.is_ok());
289 let (files, bytes) = result.unwrap();
290 assert_eq!(files, 3);
291 assert!(bytes > 0);
292
293 assert!(!cache_dir.join("salsa_cache.bin").exists());
295 assert!(!cache_dir.join("call_graph.json").exists());
296 assert!(!cache_dir.join("test.pkl").exists());
297 }
298
299 #[test]
300 fn test_clear_cache_files_preserves_directory() {
301 let temp = TempDir::new().unwrap();
302 let cache_dir = temp.path().join(".tldr").join("cache");
303 fs::create_dir_all(&cache_dir).unwrap();
304 fs::write(cache_dir.join("test.bin"), "data").unwrap();
305
306 let args = CacheClearArgs {
307 project: temp.path().to_path_buf(),
308 };
309
310 args.clear_cache_files(temp.path()).unwrap();
311
312 assert!(cache_dir.exists());
314 }
315
316 #[tokio::test]
317 async fn test_cache_clear_no_cache() {
318 let temp = TempDir::new().unwrap();
319 let args = CacheClearArgs {
320 project: temp.path().to_path_buf(),
321 };
322
323 let result = args.run_async(OutputFormat::Json, true).await;
325 assert!(result.is_ok());
326 }
327
328 #[tokio::test]
329 async fn test_cache_clear_with_files() {
330 let temp = TempDir::new().unwrap();
331 let cache_dir = temp.path().join(".tldr").join("cache");
332 fs::create_dir_all(&cache_dir).unwrap();
333 fs::write(cache_dir.join("test.bin"), "test data").unwrap();
334
335 let args = CacheClearArgs {
336 project: temp.path().to_path_buf(),
337 };
338
339 let result = args.run_async(OutputFormat::Json, true).await;
340 assert!(result.is_ok());
341
342 assert!(!cache_dir.join("test.bin").exists());
344 }
345}