1use anyhow::{bail, Result};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5const PROTECTED_PREFIXES: &[&str] = &[
6 "/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/proc", "/root",
7 "/run", "/sbin", "/sys", "/usr", "/var/lib", "/var/log",
8];
9
10const NEVER_CLEAN_FRAGMENTS: &[&str] = &[
11 "/.steam/",
12 "/Steam/",
13 "/.local/share/Steam/",
14 "/lutris/",
15 "/.local/share/lutris/",
16 "/.local/share/flatpak/",
17 "/.var/app/",
18 "/snap/",
19 "/.local/share/snap/",
20 "/var/lib/snapd/",
21];
22
23pub fn is_game_or_store_dir(path: &Path) -> bool {
24 let s = path.to_string_lossy();
25 NEVER_CLEAN_FRAGMENTS.iter().any(|f| s.contains(f))
26}
27
28pub fn is_protected(path: &Path) -> bool {
29 let canon = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
30 if canon.components().count() <= 1 {
31 return true;
32 }
33 PROTECTED_PREFIXES.iter().any(|p| canon.starts_with(Path::new(p)))
34}
35
36pub struct CleanPlan {
37 pub keep: Vec<PathBuf>,
38 pub remove: Vec<PathBuf>,
39 pub total_size: u64,
40 pub blocked: Vec<PathBuf>,
41}
42
43pub fn build_plan(candidates: Vec<(PathBuf, u64)>) -> CleanPlan {
44 let mut remove = Vec::new();
45 let mut blocked = Vec::new();
46 let mut total_size = 0u64;
47 for (path, size) in candidates {
48 if is_protected(&path) {
49 blocked.push(path);
50 } else {
51 total_size += size;
52 remove.push(path);
53 }
54 }
55 CleanPlan { keep: Vec::new(), remove, total_size, blocked }
56}
57
58pub fn execute_plan(plan: &CleanPlan, dry_run: bool) -> Result<usize> {
59 if dry_run {
60 return Ok(plan.remove.len());
61 }
62 if !plan.blocked.is_empty() {
63 bail!("{} protected paths were excluded from this plan and must not be removed", plan.blocked.len());
64 }
65 let refs: Vec<&Path> = plan.remove.iter().map(|p| p.as_path()).collect();
66 super::trash::move_to_trash(&refs)
67}
68
69pub fn dedup_with_hardlink(keep: &Path, remove: &Path) -> Result<()> {
70 if is_protected(keep) || is_protected(remove) {
71 bail!("refusing to touch a protected path");
72 }
73 fs::remove_file(remove)?;
74 fs::hard_link(keep, remove)?;
75 Ok(())
76}