Skip to main content

cleansys_core/
utils.rs

1use anyhow::{Context, Result};
2use colored::*;
3use std::io::{self, Write};
4use std::process::Command;
5#[cfg(unix)]
6use users::get_effective_uid;
7
8/// Check if the program is running with root privileges (Unix) or an
9/// elevated/Administrator token (Windows).
10#[cfg(unix)]
11pub fn check_root() -> bool {
12    get_effective_uid() == 0
13}
14
15/// Check if the current process token is elevated (running "as Administrator").
16#[cfg(windows)]
17pub fn check_root() -> bool {
18    is_elevated::is_elevated()
19}
20
21#[cfg(not(any(unix, windows)))]
22pub fn check_root() -> bool {
23    false
24}
25
26/// Whether this platform's elevation model matches the interactive
27/// sudo-password-prompt flow (Unix: `sudo -S`). Windows uses UAC/Administrator
28/// tokens instead, which front-ends should present very differently (there is
29/// no password to type — the user must relaunch the process elevated).
30pub const fn supports_sudo_prompt() -> bool {
31    cfg!(unix)
32}
33
34/// Prompt for sudo elevation if not already root
35/// Returns true if elevation succeeded or already root, false otherwise
36#[cfg(unix)]
37pub fn elevate_if_needed() -> Result<bool> {
38    if check_root() {
39        return Ok(true);
40    }
41
42    print_warning("System cleaners require root privileges.");
43    println!("You can either:");
44    println!("  1. Run this command again with sudo");
45    println!("  2. Enter your password to elevate now");
46    print!("\nWould you like to elevate now? [Y/n]: ");
47    io::stdout().flush()?;
48
49    let mut response = String::new();
50    io::stdin().read_line(&mut response)?;
51
52    match response.trim().to_lowercase().as_str() {
53        "n" | "no" => {
54            print_warning("Skipping system cleaners. Only user cleaners will run.");
55            Ok(false)
56        }
57        _ => {
58            // Try to validate sudo access by running a simple command
59            print!("Authenticating... ");
60            io::stdout().flush()?;
61
62            let status = Command::new("sudo")
63                .args(["-v"])
64                .status()
65                .context("Failed to execute sudo")?;
66
67            if status.success() {
68                println!("{}", "✓ Authentication successful".green());
69                Ok(true)
70            } else {
71                print_error("Authentication failed. Skipping system cleaners.");
72                Ok(false)
73            }
74        }
75    }
76}
77
78#[cfg(windows)]
79pub fn elevate_if_needed() -> Result<bool> {
80    if check_root() {
81        return Ok(true);
82    }
83    print_warning(
84        "Some system cleaners require Administrator privileges. Restart CleanSys as Administrator (right-click → 'Run as administrator') to use them.",
85    );
86    Ok(false)
87}
88
89#[cfg(not(any(unix, windows)))]
90pub fn elevate_if_needed() -> Result<bool> {
91    print_warning("System cleaners are only available on Unix-like systems and Windows.");
92    Ok(false)
93}
94
95/// Execute a command with sudo if not already root
96/// This function handles terminal raw mode properly for TUI applications
97/// It assumes sudo credentials are already cached (via password dialog or sudo -v)
98#[cfg(unix)]
99pub fn execute_with_sudo(command: &str, args: &[&str]) -> Result<std::process::Output> {
100    use std::process::Stdio;
101
102    if check_root() {
103        // Already root, execute directly
104        Command::new(command)
105            .args(args)
106            .output()
107            .context(format!("Failed to execute command: {}", command))
108    } else {
109        // Use sudo with non-interactive mode and cached credentials
110        // The -n flag prevents sudo from prompting for a password
111        let mut sudo_args = vec!["-n", command];
112        sudo_args.extend_from_slice(args);
113
114        Command::new("sudo")
115            .args(sudo_args)
116            .stdin(Stdio::null())
117            .output()
118            .context(format!("Failed to execute command with sudo: {}", command))
119    }
120}
121
122#[cfg(not(unix))]
123pub fn execute_with_sudo(command: &str, args: &[&str]) -> Result<std::process::Output> {
124    Command::new(command)
125        .args(args)
126        .output()
127        .context(format!("Failed to execute command: {}", command))
128}
129
130/// Print a header with a colorful banner
131pub fn print_header(text: &str) {
132    let width = 60;
133    let padding = (width - text.len()) / 2;
134    let line = "=".repeat(width);
135
136    println!("\n{}", line.bright_blue());
137    println!(
138        "{}{}{}",
139        " ".repeat(padding),
140        text.bright_white().bold(),
141        " ".repeat(padding)
142    );
143    println!("{}\n", line.bright_blue());
144}
145
146/// Print a success message
147pub fn print_success(message: &str) {
148    println!("{} {}", "✓".green().bold(), message);
149}
150
151/// Print a warning message
152pub fn print_warning(message: &str) {
153    println!("{} {}", "!".yellow().bold(), message);
154}
155
156/// Print an error message
157pub fn print_error(message: &str) {
158    eprintln!("{} {}", "✗".red().bold(), message);
159}
160
161/// Ask for user confirmation
162pub fn confirm(prompt: &str, default: bool) -> Result<bool> {
163    let yes_no = if default { "[Y/n]" } else { "[y/N]" };
164    print!("{} {} ", prompt, yes_no);
165    io::stdout().flush()?;
166
167    let mut response = String::new();
168    io::stdin().read_line(&mut response)?;
169
170    match response.trim().to_lowercase().as_str() {
171        "y" | "yes" => Ok(true),
172        "n" | "no" => Ok(false),
173        "" => Ok(default),
174        _ => {
175            print_warning("Invalid response. Please enter 'y' or 'n'.");
176            confirm(prompt, default)
177        }
178    }
179}
180
181/// Format bytes into human-readable sizes
182pub fn format_size(bytes: u64) -> String {
183    const KB: u64 = 1024;
184    const MB: u64 = KB * 1024;
185    const GB: u64 = MB * 1024;
186
187    if bytes >= GB {
188        format!("{:.2} GB", bytes as f64 / GB as f64)
189    } else if bytes >= MB {
190        format!("{:.2} MB", bytes as f64 / MB as f64)
191    } else if bytes >= KB {
192        format!("{:.2} KB", bytes as f64 / KB as f64)
193    } else {
194        format!("{} bytes", bytes)
195    }
196}
197
198/// Get the size of a directory or file in bytes.
199///
200/// Implemented as a pure-Rust recursive walk (no shell-out to `du`), so it
201/// works identically on Linux, macOS, and Windows — the previous `du -sb`
202/// implementation relied on a GNU-only flag and silently reported `0` on
203/// BSD/macOS `du`. Symlinks are not followed (their own size is counted,
204/// not the target's), matching `du`'s default behaviour and avoiding
205/// infinite loops on cyclic symlinks.
206pub fn get_size(path: &str) -> Result<u64> {
207    const MAX_DEPTH: u32 = 512;
208    Ok(dir_size(std::path::Path::new(path), 0, MAX_DEPTH))
209}
210
211fn dir_size(path: &std::path::Path, depth: u32, max_depth: u32) -> u64 {
212    let metadata = match std::fs::symlink_metadata(path) {
213        Ok(m) => m,
214        Err(_) => return 0,
215    };
216
217    if metadata.file_type().is_symlink() {
218        return 0;
219    }
220
221    if metadata.is_file() {
222        return metadata.len();
223    }
224
225    if metadata.is_dir() {
226        // Guard against pathological/cyclic directory structures: give up on
227        // descending further rather than risk a stack overflow. In practice
228        // no real cache/temp/trash directory this tool targets comes close
229        // to this depth.
230        if depth >= max_depth {
231            log::warn!(
232                "get_size: max recursion depth ({max_depth}) reached at {:?}; size may be underestimated",
233                path
234            );
235            return 0;
236        }
237
238        let mut total = 0u64;
239        if let Ok(entries) = std::fs::read_dir(path) {
240            for entry in entries.flatten() {
241                total = total.saturating_add(dir_size(&entry.path(), depth + 1, max_depth));
242            }
243        }
244        return total;
245    }
246
247    0
248}