Skip to main content

null_e/cleaners/
logs.rs

1//! Logs cleanup module
2//!
3//! Handles cleanup of various log files:
4//! - System logs
5//! - Application logs
6//! - Development tool logs
7//! - Crash reports
8
9use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
10use crate::error::Result;
11use std::path::PathBuf;
12
13/// Logs cleaner
14pub struct LogsCleaner {
15    home: PathBuf,
16}
17
18impl LogsCleaner {
19    /// Create a new logs cleaner
20    pub fn new() -> Option<Self> {
21        let home = dirs::home_dir()?;
22        Some(Self { home })
23    }
24
25    /// Detect all log cleanable items
26    pub fn detect(&self) -> Result<Vec<CleanableItem>> {
27        let mut items = Vec::new();
28
29        // User logs
30        items.extend(self.detect_user_logs()?);
31
32        // Development tool logs
33        items.extend(self.detect_dev_logs()?);
34
35        // Crash reports
36        items.extend(self.detect_crash_reports()?);
37
38        // npm/yarn logs
39        items.extend(self.detect_package_manager_logs()?);
40
41        Ok(items)
42    }
43
44    /// Detect user-level logs
45    fn detect_user_logs(&self) -> Result<Vec<CleanableItem>> {
46        let mut items = Vec::new();
47
48        #[cfg(target_os = "macos")]
49        {
50            // ~/Library/Logs
51            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                        // Skip certain system-critical logs
61                        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                            // Skip if less than 10MB
75                            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            // ~/.local/share/*/logs or ~/.config/*/logs
99            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    /// Detect development tool logs
132    fn detect_dev_logs(&self) -> Result<Vec<CleanableItem>> {
133        let mut items = Vec::new();
134
135        let dev_log_paths = [
136            // Homebrew
137            ("Library/Logs/Homebrew", "Homebrew Logs", "๐Ÿบ"),
138            // Git
139            (".git/logs", "Git Logs", "๐Ÿ“š"),
140            // npm
141            (".npm/_logs", "npm Logs", "๐Ÿ“ฆ"),
142            // Yarn
143            (".yarn/logs", "Yarn Logs", "๐Ÿงถ"),
144            // pip
145            (".pip/log", "pip Logs", "๐Ÿ"),
146            // Gradle
147            (".gradle/daemon", "Gradle Daemon Logs", "๐Ÿ˜"),
148            // Cargo
149            (".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                // Skip if less than 5MB
166                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    /// Detect crash reports
188    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    /// Detect package manager logs
229    fn detect_package_manager_logs(&self) -> Result<Vec<CleanableItem>> {
230        let mut items = Vec::new();
231
232        // npm debug logs (npm-debug.log files can accumulate)
233        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}