Skip to content

Commit 46f50cf

Browse files
authored
add FDA checks and actionable clean failures (#9)
1 parent 4712149 commit 46f50cf

18 files changed

Lines changed: 527 additions & 52 deletions

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["crates/null-e-core", "crates/null-e-cli", "tauri"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "0.4.0"
6+
version = "0.4.1"
77
edition = "2021"
88
authors = ["us"]
99
license = "WTFPL"

tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ serde_json = { workspace = true }
1818
tokio = { workspace = true }
1919
tracing = { workspace = true }
2020
uuid = { workspace = true }
21+
dirs = { workspace = true }
2122

2223
[build-dependencies]
2324
tauri-build = { version = "2", features = [] }

tauri/src/commands/cache.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ pub async fn detect_caches(
1717
}
1818

1919
#[tauri::command]
20-
pub async fn clean_cache(_state: tauri::State<'_, AppState>, id: String) -> Result<(), String> {
20+
pub async fn clean_cache(_state: tauri::State<'_, AppState>, id: String) -> Result<u64, String> {
2121
tokio::task::spawn_blocking(move || {
2222
let caches = null_e_core::caches::detect_caches().map_err(|e| e.to_string())?;
2323
let cache = caches
2424
.iter()
2525
.find(|c| c.id == id)
2626
.ok_or_else(|| format!("Cache '{}' not found", id))?;
27-
null_e_core::caches::clean_cache(cache, true).map_err(|e| e.to_string())?;
28-
Ok::<(), String>(())
27+
let result = null_e_core::caches::clean_cache(cache, true).map_err(|e| e.to_string())?;
28+
Ok::<u64, String>(result.bytes_freed)
2929
})
3030
.await
3131
.map_err(|e| e.to_string())?

tauri/src/commands/clean.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use crate::dto::{CleanConfigDto, CleanProgressDto, CleanSummaryDto};
1+
use crate::dto::{CleanConfigDto, CleanFailureDto, CleanProgressDto, CleanSummaryDto};
22
use crate::state::AppState;
3+
use null_e_core::error::NullEError;
34
use null_e_core::trash::{delete_path, DeleteMethod};
45
use std::path::PathBuf;
56
use tauri::{AppHandle, Emitter};
@@ -29,6 +30,7 @@ pub async fn start_clean(
2930
let mut succeeded = 0usize;
3031
let mut failed = 0usize;
3132
let mut bytes_freed = 0u64;
33+
let mut failures = Vec::new();
3234

3335
for (i, target) in targets.iter().enumerate() {
3436
let path = PathBuf::from(target);
@@ -46,8 +48,14 @@ pub async fn start_clean(
4648
succeeded += 1;
4749
bytes_freed += freed;
4850
}
49-
Err(_) => {
51+
Err(err) => {
5052
failed += 1;
53+
let (reason, is_tcc) = classify_delete_error(&err);
54+
failures.push(CleanFailureDto {
55+
path: target.clone(),
56+
reason,
57+
is_tcc,
58+
});
5159
}
5260
}
5361
}
@@ -58,6 +66,11 @@ pub async fn start_clean(
5866
failed,
5967
bytes_freed,
6068
used_trash: method == DeleteMethod::Trash,
69+
method_label: match method {
70+
DeleteMethod::Trash => "Trash".to_string(),
71+
DeleteMethod::Permanent | DeleteMethod::DryRun => "Deleted".to_string(),
72+
},
73+
failures,
6174
};
6275
let _ = app_handle.emit("clean:complete", &summary);
6376
});
@@ -72,3 +85,23 @@ pub async fn cancel_clean(state: tauri::State<'_, AppState>) -> Result<(), Strin
7285
}
7386
Ok(())
7487
}
88+
89+
fn classify_delete_error(err: &NullEError) -> (String, bool) {
90+
match err {
91+
NullEError::Io(io_err) => {
92+
let is_tcc = io_err.raw_os_error() == Some(1);
93+
(io_err.to_string(), is_tcc)
94+
}
95+
NullEError::PermissionDenied(path) => {
96+
(format!("Permission denied: {}", path.display()), false)
97+
}
98+
NullEError::Trash(message) => {
99+
let lower = message.to_lowercase();
100+
let is_tcc = lower.contains("operation not permitted")
101+
|| lower.contains("eperm")
102+
|| lower.contains("os error 1");
103+
(message.clone(), is_tcc)
104+
}
105+
_ => (err.to_string(), false),
106+
}
107+
}

tauri/src/commands/system.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use crate::dto::DiskInfoDto;
1+
use crate::dto::{DiskInfoDto, FdaStatusDto};
22
use crate::state::AppState;
3+
use std::fs;
4+
use std::io::ErrorKind;
35
use std::process::Command;
46

57
#[tauri::command]
@@ -39,3 +41,41 @@ pub async fn get_disk_info(_state: tauri::State<'_, AppState>) -> Result<DiskInf
3941
pub async fn get_app_version(_state: tauri::State<'_, AppState>) -> Result<String, String> {
4042
Ok(null_e_core::VERSION.to_string())
4143
}
44+
45+
#[tauri::command]
46+
pub async fn check_fda_status(_state: tauri::State<'_, AppState>) -> Result<FdaStatusDto, String> {
47+
Ok(check_fda_status_inner())
48+
}
49+
50+
#[cfg(target_os = "macos")]
51+
fn check_fda_status_inner() -> FdaStatusDto {
52+
let platform = std::env::consts::OS.to_string();
53+
let Some(home) = dirs::home_dir() else {
54+
return FdaStatusDto {
55+
status: "unknown".to_string(),
56+
platform,
57+
};
58+
};
59+
60+
let tcc_dir = home.join("Library/Application Support/com.apple.TCC");
61+
let status = match fs::read_dir(tcc_dir) {
62+
Ok(_) => "granted",
63+
Err(err) if err.kind() == ErrorKind::PermissionDenied || err.raw_os_error() == Some(1) => {
64+
"not_granted"
65+
}
66+
Err(_) => "unknown",
67+
};
68+
69+
FdaStatusDto {
70+
status: status.to_string(),
71+
platform,
72+
}
73+
}
74+
75+
#[cfg(not(target_os = "macos"))]
76+
fn check_fda_status_inner() -> FdaStatusDto {
77+
FdaStatusDto {
78+
status: "granted".to_string(),
79+
platform: std::env::consts::OS.to_string(),
80+
}
81+
}

tauri/src/dto.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ pub struct CleanSummaryDto {
126126
pub failed: usize,
127127
pub bytes_freed: u64,
128128
pub used_trash: bool,
129+
pub method_label: String,
130+
pub failures: Vec<CleanFailureDto>,
131+
}
132+
133+
#[derive(Debug, Clone, Serialize, Deserialize)]
134+
pub struct CleanFailureDto {
135+
pub path: String,
136+
pub reason: String,
137+
pub is_tcc: bool,
129138
}
130139

131140
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -196,3 +205,9 @@ pub struct DiskInfoDto {
196205
pub available: u64,
197206
pub mount_point: String,
198207
}
208+
209+
#[derive(Debug, Clone, Serialize, Deserialize)]
210+
pub struct FdaStatusDto {
211+
pub status: String,
212+
pub platform: String,
213+
}

tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub fn run() {
3232
commands::config::save_config,
3333
commands::system::get_disk_info,
3434
commands::system::get_app_version,
35+
commands::system::check_fda_status,
3536
])
3637
.setup(|app| {
3738
// System tray menu

tauri/tauri.conf.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
33
"productName": "null-e",
4-
"version": "0.4.0",
4+
"version": "0.4.1",
55
"identifier": "com.null-e.disk-cleaner",
66
"build": {
77
"frontendDist": "../ui/dist",
@@ -63,7 +63,7 @@
6363
},
6464
"plugins": {
6565
"shell": {
66-
"open": true
66+
"open": "^(https?://.+|mailto:.+|tel:.+|x-apple\\.systempreferences:.+)$"
6767
},
6868
"updater": {
6969
"endpoints": [

ui/src/App.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,24 @@ import { useUiStore } from '@/stores/ui-store';
1111
import { useScanStore } from '@/stores/scan-store';
1212
import { useTheme } from '@/hooks/useTheme';
1313
import { useScanProgress } from '@/hooks/useScanProgress';
14+
import { useFdaCheck } from '@/hooks/useFdaCheck';
1415

1516
export function App() {
1617
useTheme();
17-
useScanProgress();
1818

1919
const disclaimerAccepted = useUiStore((s) => s.disclaimerAccepted);
20+
21+
if (!disclaimerAccepted) {
22+
return <DisclaimerModal />;
23+
}
24+
25+
return <AppMain />;
26+
}
27+
28+
function AppMain() {
29+
useScanProgress();
30+
useFdaCheck();
31+
2032
const appState = useUiStore((s) => s.appState);
2133
const result = useScanStore((s) => s.result);
2234
const scanError = useScanStore((s) => s.error);
@@ -35,10 +47,6 @@ export function App() {
3547
}
3648
}, [scanError, appState]);
3749

38-
if (!disclaimerAccepted) {
39-
return <DisclaimerModal />;
40-
}
41-
4250
return (
4351
<div className="flex flex-col h-full">
4452
<AppBar />

0 commit comments

Comments
 (0)