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 println!(
54 "{{\"freed_bytes\":{total},\"archives_bytes\":{archives},\"git_bytes\":{git}}}"
55 );
56 } else {
57 output::print_info(&format!(
58 "cleaned {} (archives: {}, git: {})",
59 format_bytes(total),
60 format_bytes(archives),
61 format_bytes(git),
62 ));
63 }
64
65 Ok(0)
66}
67
68fn run_info(json: bool) -> Result<i32, MarsError> {
69 let cache = GlobalCache::new()?;
70
71 let archives = dir_size(&cache.archives_dir());
72 let git = dir_size(&cache.git_dir());
73 let total = archives + git;
74 let path = cache.root.display().to_string();
75
76 if json {
77 println!(
78 "{{\"path\":\"{path}\",\"total_bytes\":{total},\"archives_bytes\":{archives},\"git_bytes\":{git}}}"
79 );
80 } else {
81 println!("path: {path}");
82 println!("total: {}", format_bytes(total));
83 println!("archives: {}", format_bytes(archives));
84 println!("git: {}", format_bytes(git));
85 }
86
87 Ok(0)
88}
89
90fn dir_size(path: &std::path::Path) -> u64 {
92 if !path.exists() {
93 return 0;
94 }
95 walkdir::WalkDir::new(path)
96 .into_iter()
97 .filter_map(|e| e.ok())
98 .filter(|e| e.file_type().is_file())
99 .filter_map(|e| e.metadata().ok())
100 .map(|m| m.len())
101 .sum()
102}
103
104fn remove_dir_contents(path: &std::path::Path) -> Result<(), MarsError> {
106 if !path.exists() {
107 return Ok(());
108 }
109 for entry in std::fs::read_dir(path)? {
110 let entry = entry?;
111 let entry_path = entry.path();
112 if entry_path.is_dir() {
113 std::fs::remove_dir_all(&entry_path)?;
114 } else {
115 std::fs::remove_file(&entry_path)?;
116 }
117 }
118 Ok(())
119}
120
121fn format_bytes(bytes: u64) -> String {
123 if bytes < 1024 {
124 format!("{bytes} B")
125 } else if bytes < 1024 * 1024 {
126 format!("{:.1} KB", bytes as f64 / 1024.0)
127 } else if bytes < 1024 * 1024 * 1024 {
128 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
129 } else {
130 format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn format_bytes_ranges() {
140 assert_eq!(format_bytes(0), "0 B");
141 assert_eq!(format_bytes(512), "512 B");
142 assert_eq!(format_bytes(1024), "1.0 KB");
143 assert_eq!(format_bytes(1536), "1.5 KB");
144 assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
145 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
146 }
147
148 #[test]
149 fn dir_size_empty() {
150 let dir = tempfile::TempDir::new().unwrap();
151 assert_eq!(dir_size(dir.path()), 0);
152 }
153
154 #[test]
155 fn dir_size_with_files() {
156 let dir = tempfile::TempDir::new().unwrap();
157 std::fs::write(dir.path().join("a.txt"), "hello").unwrap();
158 std::fs::write(dir.path().join("b.txt"), "world!").unwrap();
159 assert_eq!(dir_size(dir.path()), 11); }
161
162 #[test]
163 fn dir_size_nonexistent() {
164 assert_eq!(dir_size(std::path::Path::new("/nonexistent/path")), 0);
165 }
166
167 #[test]
168 fn remove_dir_contents_clears_files() {
169 let dir = tempfile::TempDir::new().unwrap();
170 std::fs::write(dir.path().join("a.txt"), "hello").unwrap();
171 std::fs::create_dir_all(dir.path().join("sub")).unwrap();
172 std::fs::write(dir.path().join("sub").join("b.txt"), "world").unwrap();
173
174 remove_dir_contents(dir.path()).unwrap();
175
176 assert!(dir.path().exists()); assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0); }
179
180 #[test]
181 fn remove_dir_contents_nonexistent_ok() {
182 assert!(remove_dir_contents(std::path::Path::new("/nonexistent")).is_ok());
183 }
184}