diskforge_core/
cleaner.rs1use std::fs;
2use std::io::ErrorKind;
3use std::path::Path;
4
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum CleanError {
9 #[error("path does not exist: {0}")]
10 NotFound(String),
11 #[error("permission denied: {0}")]
12 PermissionDenied(String),
13 #[error("io error removing {path}: {source}")]
14 Io {
15 path: String,
16 source: std::io::Error,
17 },
18}
19
20#[derive(Debug)]
22pub struct CleanedItem {
23 pub path: String,
24 pub size: u64,
25 pub success: bool,
26 pub error: Option<String>,
27}
28
29pub fn run_cleanup_command(command: &str, size: u64) -> CleanedItem {
31 let result = std::process::Command::new("sh")
32 .args(["-c", command])
33 .output();
34 match result {
35 Ok(output) if output.status.success() => CleanedItem {
36 path: command.to_string(),
37 size,
38 success: true,
39 error: None,
40 },
41 Ok(output) => CleanedItem {
42 path: command.to_string(),
43 size: 0,
44 success: false,
45 error: Some(String::from_utf8_lossy(&output.stderr).trim().to_string()),
46 },
47 Err(e) => CleanedItem {
48 path: command.to_string(),
49 size: 0,
50 success: false,
51 error: Some(e.to_string()),
52 },
53 }
54}
55
56pub fn remove_file(path: &Path, size: u64) -> CleanedItem {
58 let path_str = path.to_string_lossy().to_string();
59
60 if !path.exists() {
61 return CleanedItem {
62 path: path_str,
63 size: 0,
64 success: false,
65 error: Some("path does not exist".into()),
66 };
67 }
68
69 match fs::remove_file(path) {
70 Ok(()) => CleanedItem {
71 path: path_str,
72 size,
73 success: true,
74 error: None,
75 },
76 Err(e) if e.kind() == ErrorKind::PermissionDenied => elevated_remove(&path_str, size),
77 Err(e) => CleanedItem {
78 path: path_str,
79 size: 0,
80 success: false,
81 error: Some(e.to_string()),
82 },
83 }
84}
85
86pub fn remove_path(path: &Path, size: u64) -> CleanedItem {
88 if path.is_file() {
89 remove_file(path, size)
90 } else {
91 remove_dir(path, size)
92 }
93}
94
95pub fn remove_dir(path: &Path, size: u64) -> CleanedItem {
97 let path_str = path.to_string_lossy().to_string();
98
99 if !path.exists() {
100 return CleanedItem {
101 path: path_str,
102 size: 0,
103 success: false,
104 error: Some("path does not exist".into()),
105 };
106 }
107
108 match fs::remove_dir_all(path) {
109 Ok(()) => CleanedItem {
110 path: path_str,
111 size,
112 success: true,
113 error: None,
114 },
115 Err(e) if e.kind() == ErrorKind::PermissionDenied => elevated_remove(&path_str, size),
116 Err(e) => CleanedItem {
117 path: path_str,
118 size: 0,
119 success: false,
120 error: Some(e.to_string()),
121 },
122 }
123}
124
125fn elevated_remove(path: &str, size: u64) -> CleanedItem {
128 let escaped = path.replace('\'', "'\\''");
130 let script = format!("do shell script \"rm -rf '{escaped}'\" with administrator privileges");
131
132 let result = std::process::Command::new("osascript")
133 .args(["-e", &script])
134 .output();
135
136 match result {
137 Ok(output) if output.status.success() => CleanedItem {
138 path: path.to_string(),
139 size,
140 success: true,
141 error: None,
142 },
143 Ok(output) => {
144 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
145 let error = if stderr.contains("User canceled") || stderr.contains("-128") {
147 "permission denied (auth cancelled)".to_string()
148 } else {
149 format!("elevated removal failed: {stderr}")
150 };
151 CleanedItem {
152 path: path.to_string(),
153 size: 0,
154 success: false,
155 error: Some(error),
156 }
157 }
158 Err(e) => CleanedItem {
159 path: path.to_string(),
160 size: 0,
161 success: false,
162 error: Some(format!("failed to request privileges: {e}")),
163 },
164 }
165}