1use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
10use crate::error::Result;
11use std::path::PathBuf;
12
13pub struct LogsCleaner {
15 home: PathBuf,
16}
17
18impl LogsCleaner {
19 pub fn new() -> Option<Self> {
21 let home = dirs::home_dir()?;
22 Some(Self { home })
23 }
24
25 pub fn detect(&self) -> Result<Vec<CleanableItem>> {
27 let mut items = Vec::new();
28
29 items.extend(self.detect_user_logs()?);
31
32 items.extend(self.detect_dev_logs()?);
34
35 items.extend(self.detect_crash_reports()?);
37
38 items.extend(self.detect_package_manager_logs()?);
40
41 Ok(items)
42 }
43
44 fn detect_user_logs(&self) -> Result<Vec<CleanableItem>> {
46 let mut items = Vec::new();
47
48 #[cfg(target_os = "macos")]
49 {
50 let logs_path = self.home.join("Library/Logs");
52 if logs_path.exists() {
53 if let Ok(entries) = std::fs::read_dir(&logs_path) {
54 for entry in entries.filter_map(|e| e.ok()) {
55 let path = entry.path();
56 let name = path.file_name()
57 .map(|n| n.to_string_lossy().to_string())
58 .unwrap_or_default();
59
60 if name == "DiagnosticReports" || name == "com.apple.xpc.launchd" {
62 continue;
63 }
64
65 let (size, file_count) = if path.is_dir() {
66 calculate_dir_size(&path)?
67 } else if path.is_file() {
68 (std::fs::metadata(&path)?.len(), 1)
69 } else {
70 continue;
71 };
72
73 if size < 10_000_000 {
74 continue;
76 }
77
78 items.push(CleanableItem {
79 name: format!("Logs: {}", name),
80 category: "Logs".to_string(),
81 subcategory: "Application Logs".to_string(),
82 icon: "๐",
83 path,
84 size,
85 file_count: Some(file_count),
86 last_modified: get_mtime(&entry.path()),
87 description: "Application log files. Usually safe to delete.",
88 safe_to_delete: SafetyLevel::Safe,
89 clean_command: None,
90 });
91 }
92 }
93 }
94 }
95
96 #[cfg(target_os = "linux")]
97 {
98 let local_share = self.home.join(".local/share");
100 if local_share.exists() {
101 if let Ok(entries) = std::fs::read_dir(&local_share) {
102 for entry in entries.filter_map(|e| e.ok()) {
103 let logs_path = entry.path().join("logs");
104 if logs_path.exists() {
105 let (size, file_count) = calculate_dir_size(&logs_path)?;
106 if size > 10_000_000 {
107 let name = entry.file_name().to_string_lossy().to_string();
108 items.push(CleanableItem {
109 name: format!("Logs: {}", name),
110 category: "Logs".to_string(),
111 subcategory: "Application Logs".to_string(),
112 icon: "๐",
113 path: logs_path,
114 size,
115 file_count: Some(file_count),
116 last_modified: None,
117 description: "Application log files.",
118 safe_to_delete: SafetyLevel::Safe,
119 clean_command: None,
120 });
121 }
122 }
123 }
124 }
125 }
126 }
127
128 Ok(items)
129 }
130
131 fn detect_dev_logs(&self) -> Result<Vec<CleanableItem>> {
133 let mut items = Vec::new();
134
135 let dev_log_paths = [
136 ("Library/Logs/Homebrew", "Homebrew Logs", "๐บ"),
138 (".git/logs", "Git Logs", "๐"),
140 (".npm/_logs", "npm Logs", "๐ฆ"),
142 (".yarn/logs", "Yarn Logs", "๐งถ"),
144 (".pip/log", "pip Logs", "๐"),
146 (".gradle/daemon", "Gradle Daemon Logs", "๐"),
148 (".cargo/.package-cache", "Cargo Logs", "๐ฆ"),
150 ];
151
152 for (rel_path, name, icon) in dev_log_paths {
153 let path = self.home.join(rel_path);
154 if !path.exists() {
155 continue;
156 }
157
158 let (size, file_count) = if path.is_dir() {
159 calculate_dir_size(&path)?
160 } else {
161 (std::fs::metadata(&path)?.len(), 1)
162 };
163
164 if size < 5_000_000 {
165 continue;
167 }
168
169 items.push(CleanableItem {
170 name: name.to_string(),
171 category: "Logs".to_string(),
172 subcategory: "Dev Tool Logs".to_string(),
173 icon,
174 path,
175 size,
176 file_count: Some(file_count),
177 last_modified: None,
178 description: "Development tool log files. Safe to delete.",
179 safe_to_delete: SafetyLevel::Safe,
180 clean_command: None,
181 });
182 }
183
184 Ok(items)
185 }
186
187 fn detect_crash_reports(&self) -> Result<Vec<CleanableItem>> {
189 let mut items = Vec::new();
190
191 #[cfg(target_os = "macos")]
192 {
193 let crash_paths = [
194 ("Library/Logs/DiagnosticReports", "Crash Reports"),
195 ("Library/Logs/CrashReporter", "Crash Reporter"),
196 ];
197
198 for (rel_path, name) in crash_paths {
199 let path = self.home.join(rel_path);
200 if !path.exists() {
201 continue;
202 }
203
204 let (size, file_count) = calculate_dir_size(&path)?;
205 if size < 5_000_000 {
206 continue;
207 }
208
209 items.push(CleanableItem {
210 name: name.to_string(),
211 category: "Logs".to_string(),
212 subcategory: "Crash Reports".to_string(),
213 icon: "๐ฅ",
214 path,
215 size,
216 file_count: Some(file_count),
217 last_modified: None,
218 description: "Application crash reports. Safe to delete old ones.",
219 safe_to_delete: SafetyLevel::SafeWithCost,
220 clean_command: None,
221 });
222 }
223 }
224
225 Ok(items)
226 }
227
228 fn detect_package_manager_logs(&self) -> Result<Vec<CleanableItem>> {
230 let mut items = Vec::new();
231
232 let npm_logs = self.home.join(".npm/_logs");
234 if npm_logs.exists() {
235 let (size, file_count) = calculate_dir_size(&npm_logs)?;
236 if size > 1_000_000 {
237 items.push(CleanableItem {
238 name: "npm Debug Logs".to_string(),
239 category: "Logs".to_string(),
240 subcategory: "Package Manager".to_string(),
241 icon: "๐ฆ",
242 path: npm_logs,
243 size,
244 file_count: Some(file_count),
245 last_modified: None,
246 description: "npm installation and error logs. Safe to delete.",
247 safe_to_delete: SafetyLevel::Safe,
248 clean_command: None,
249 });
250 }
251 }
252
253 Ok(items)
254 }
255}
256
257impl Default for LogsCleaner {
258 fn default() -> Self {
259 Self::new().expect("LogsCleaner requires home directory")
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_logs_cleaner_creation() {
269 let cleaner = LogsCleaner::new();
270 assert!(cleaner.is_some());
271 }
272
273 #[test]
274 fn test_logs_detection() {
275 if let Some(cleaner) = LogsCleaner::new() {
276 let items = cleaner.detect().unwrap();
277 println!("Found {} log items", items.len());
278 for item in &items {
279 println!(" {} {} ({} bytes)", item.icon, item.name, item.size);
280 }
281 }
282 }
283}