1use crate::error::MarsError;
4use crate::source::GlobalCache;
5
6use super::output;
7
8#[derive(Debug, clap::Args)]
10pub struct CacheArgs {
11 #[command(subcommand)]
12 pub command: CacheCommand,
13}
14
15#[derive(Debug, clap::Subcommand)]
16pub enum CacheCommand {
17 Clean(CacheCleanArgs),
19
20 Info(CacheInfoArgs),
22}
23
24#[derive(Debug, clap::Args)]
26pub struct CacheCleanArgs {}
27
28#[derive(Debug, clap::Args)]
30pub struct CacheInfoArgs {}
31
32pub fn run(args: &CacheArgs, json: bool) -> Result<i32, MarsError> {
34 match &args.command {
35 CacheCommand::Clean(_) => run_clean(json),
36 CacheCommand::Info(_) => run_info(json),
37 }
38}
39
40fn run_clean(json: bool) -> Result<i32, MarsError> {
41 let cache = GlobalCache::new()?;
42
43 let archives = dir_size(&cache.archives_dir());
44 let git = dir_size(&cache.git_dir());
45
46 remove_dir_contents(&cache.archives_dir())?;
48 remove_dir_contents(&cache.git_dir())?;
49
50 let total = archives + git;
51
52 if json {
53 let payload = serde_json::json!({
54 "freed_bytes": total,
55 "archives_bytes": archives,
56 "git_bytes": git,
57 });
58 println!("{}", payload);
59 } else {
60 output::print_info(&format!(
61 "cleaned {} (archives: {}, git: {})",
62 format_bytes(total),
63 format_bytes(archives),
64 format_bytes(git),
65 ));
66 }
67
68 Ok(0)
69}
70
71fn run_info(json: bool) -> Result<i32, MarsError> {
72 let cache = GlobalCache::new()?;
73
74 let archives = dir_size(&cache.archives_dir());
75 let git = dir_size(&cache.git_dir());
76 let total = archives + git;
77 let path = cache.root.display().to_string();
78
79 if json {
80 let payload = serde_json::json!({
81 "path": path,
82 "total_bytes": total,
83 "archives_bytes": archives,
84 "git_bytes": git,
85 });
86 println!("{}", payload);
87 } else {
88 println!("path: {path}");
89 println!("total: {}", format_bytes(total));
90 println!("archives: {}", format_bytes(archives));
91 println!("git: {}", format_bytes(git));
92 }
93
94 Ok(0)
95}
96
97fn dir_size(path: &std::path::Path) -> u64 {
99 if !path.exists() {
100 return 0;
101 }
102 walkdir::WalkDir::new(path)
103 .into_iter()
104 .filter_map(|e| e.ok())
105 .filter(|e| e.file_type().is_file())
106 .filter_map(|e| e.metadata().ok())
107 .map(|m| m.len())
108 .sum()
109}
110
111fn remove_dir_contents(path: &std::path::Path) -> Result<(), MarsError> {
113 if !path.exists() {
114 return Ok(());
115 }
116 for entry in std::fs::read_dir(path)? {
117 let entry = entry?;
118 let entry_path = entry.path();
119 if entry_path.is_dir() {
120 std::fs::remove_dir_all(&entry_path)?;
121 } else {
122 std::fs::remove_file(&entry_path)?;
123 }
124 }
125 Ok(())
126}
127
128fn format_bytes(bytes: u64) -> String {
130 if bytes < 1024 {
131 format!("{bytes} B")
132 } else if bytes < 1024 * 1024 {
133 format!("{:.1} KB", bytes as f64 / 1024.0)
134 } else if bytes < 1024 * 1024 * 1024 {
135 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
136 } else {
137 format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn format_bytes_ranges() {
147 assert_eq!(format_bytes(0), "0 B");
148 assert_eq!(format_bytes(512), "512 B");
149 assert_eq!(format_bytes(1024), "1.0 KB");
150 assert_eq!(format_bytes(1536), "1.5 KB");
151 assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
152 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
153 }
154
155 #[test]
156 fn dir_size_empty() {
157 let dir = tempfile::TempDir::new().unwrap();
158 assert_eq!(dir_size(dir.path()), 0);
159 }
160
161 #[test]
162 fn dir_size_with_files() {
163 let dir = tempfile::TempDir::new().unwrap();
164 std::fs::write(dir.path().join("a.txt"), "hello").unwrap();
165 std::fs::write(dir.path().join("b.txt"), "world!").unwrap();
166 assert_eq!(dir_size(dir.path()), 11); }
168
169 #[test]
170 fn dir_size_nonexistent() {
171 assert_eq!(dir_size(std::path::Path::new("/nonexistent/path")), 0);
172 }
173
174 #[test]
175 fn remove_dir_contents_clears_files() {
176 let dir = tempfile::TempDir::new().unwrap();
177 std::fs::write(dir.path().join("a.txt"), "hello").unwrap();
178 std::fs::create_dir_all(dir.path().join("sub")).unwrap();
179 std::fs::write(dir.path().join("sub").join("b.txt"), "world").unwrap();
180
181 remove_dir_contents(dir.path()).unwrap();
182
183 assert!(dir.path().exists()); assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0); }
186
187 #[test]
188 fn remove_dir_contents_nonexistent_ok() {
189 assert!(remove_dir_contents(std::path::Path::new("/nonexistent")).is_ok());
190 }
191}