1use std::fs;
5use std::io::{self, BufRead, Write};
6use std::path::{Path, PathBuf};
7
8use xil_core::log;
9
10#[derive(Clone, Copy, PartialEq, Eq, Debug)]
11pub enum Kind {
12 Dir,
13 File,
14}
15
16#[derive(Clone, Debug)]
17pub struct Item {
18 pub kind: Kind,
19 pub path: PathBuf,
20 pub label: &'static str,
21}
22
23impl Item {
24 pub fn dir(path: PathBuf, label: &'static str) -> Item {
25 Item {
26 kind: Kind::Dir,
27 path,
28 label,
29 }
30 }
31 pub fn file(path: PathBuf, label: &'static str) -> Item {
32 Item {
33 kind: Kind::File,
34 path,
35 label,
36 }
37 }
38
39 pub fn file_count(&self) -> u64 {
41 if !self.path.exists() {
42 return 0;
43 }
44 match self.kind {
45 Kind::File => 1,
46 Kind::Dir => walk_files(&self.path).len() as u64,
47 }
48 }
49
50 pub fn total_bytes(&self) -> u64 {
51 if !self.path.exists() {
52 return 0;
53 }
54 match self.kind {
55 Kind::File => fs::metadata(&self.path).map(|m| m.len()).unwrap_or(0),
56 Kind::Dir => walk_files(&self.path)
57 .iter()
58 .filter_map(|p| fs::metadata(p).ok())
59 .map(|m| m.len())
60 .sum(),
61 }
62 }
63}
64
65fn walk_files(dir: &Path) -> Vec<PathBuf> {
66 let mut out = Vec::new();
67 let mut stack = vec![dir.to_path_buf()];
68 while let Some(d) = stack.pop() {
69 let Ok(rd) = fs::read_dir(&d) else { continue };
70 for e in rd.filter_map(Result::ok) {
71 let p = e.path();
72 if p.is_dir() {
73 stack.push(p);
74 } else if p.is_file() {
75 out.push(p);
76 }
77 }
78 }
79 out
80}
81
82pub fn fmt_bytes(n: u64) -> String {
84 const KB: f64 = 1024.0;
85 if n < 1024 {
86 format!("{n} B")
87 } else if n < 1024u64.pow(2) {
88 format!("{:.1} KB", n as f64 / KB)
89 } else if n < 1024u64.pow(3) {
90 format!("{:.1} MB", n as f64 / (KB * KB))
91 } else {
92 format!("{:.2} GB", n as f64 / (KB * KB * KB))
93 }
94}
95
96pub fn rel_display(path: &Path, root: &Path) -> String {
98 path.strip_prefix(root)
99 .map(|r| r.display().to_string())
100 .unwrap_or_else(|_| path.display().to_string())
101}
102
103pub fn delete(items: &[Item]) -> anyhow::Result<u64> {
105 let mut removed = 0;
106 for item in items {
107 if !item.path.exists() {
108 continue;
109 }
110 match item.kind {
111 Kind::Dir => {
112 let fc = item.file_count();
113 fs::remove_dir_all(&item.path)?;
114 removed += fc;
115 }
116 Kind::File => {
117 fs::remove_file(&item.path)?;
118 removed += 1;
119 }
120 }
121 log::info(&format!(" removed {}", item.path.display()));
122 }
123 Ok(removed)
124}
125
126pub fn input(prompt: &str) -> String {
129 let mut so = io::stdout().lock();
130 let _ = so.write_all(prompt.as_bytes());
131 let _ = so.flush();
132 let mut line = String::new();
133 let _ = io::stdin().lock().read_line(&mut line);
134 line.trim().to_string()
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn fmt_bytes_matches_python_rounding() {
143 assert_eq!(fmt_bytes(0), "0 B");
144 assert_eq!(fmt_bytes(1023), "1023 B");
145 assert_eq!(fmt_bytes(1024), "1.0 KB");
146 assert_eq!(fmt_bytes(1280), "1.2 KB"); assert_eq!(fmt_bytes(1536), "1.5 KB");
148 assert_eq!(fmt_bytes(1024 * 1024), "1.0 MB");
149 assert_eq!(fmt_bytes(3 * 1024 * 1024 * 1024), "3.00 GB");
150 }
151
152 #[test]
153 fn counts_and_bytes_walk_recursively() {
154 let tmp = tempfile::tempdir().unwrap();
155 let d = tmp.path().join("d");
156 fs::create_dir_all(d.join("sub")).unwrap();
157 fs::write(d.join("a"), "12345").unwrap();
158 fs::write(d.join("sub/.hidden"), "123").unwrap();
159 let item = Item::dir(d.clone(), "");
160 assert_eq!(item.file_count(), 2);
161 assert_eq!(item.total_bytes(), 8);
162 let f = Item::file(d.join("a"), "");
163 assert_eq!(f.file_count(), 1);
164 assert_eq!(f.total_bytes(), 5);
165 assert_eq!(Item::file(d.join("missing"), "").file_count(), 0);
166 }
167}