1use std::fs::{self, ReadDir};
2use std::io;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
6pub enum FormatUnit {
7 Bytes,
8 Kilo,
9 Mega,
10 Giga,
11 Human,
12}
13
14#[derive(Debug, Clone)]
15pub struct ParseFormatUnitError(pub String);
16
17impl std::fmt::Display for ParseFormatUnitError {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 write!(f, "{}", self.0)
20 }
21}
22
23impl std::error::Error for ParseFormatUnitError {}
24
25impl std::str::FromStr for FormatUnit {
26 type Err = ParseFormatUnitError;
27
28 fn from_str(s: &str) -> Result<Self, Self::Err> {
29 match s.to_ascii_lowercase().as_str() {
30 "bytes" | "b" => Ok(FormatUnit::Bytes),
31 "kilo" | "kb" | "k" => Ok(FormatUnit::Kilo),
32 "mega" | "mb" | "m" => Ok(FormatUnit::Mega),
33 "giga" | "gb" | "g" => Ok(FormatUnit::Giga),
34 "human" | "h" | "auto" => Ok(FormatUnit::Human),
35 _ => Err(ParseFormatUnitError(format!("invalid format unit: {s}"))),
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
41pub struct DuOptions {
42 pub depth: usize,
43 pub unit: FormatUnit,
44}
45
46impl Default for DuOptions {
47 fn default() -> Self {
48 Self {
49 depth: usize::MAX,
50 unit: FormatUnit::Human,
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Entry {
57 pub path: PathBuf,
58 pub size: u64,
59}
60
61pub fn du<P: AsRef<Path>>(path: P, opts: &DuOptions) -> io::Result<Vec<Entry>> {
62 let mut results = Vec::new();
63 du_inner(path.as_ref(), 0, opts, &mut results)?;
64 Ok(results)
65}
66
67fn du_inner(
68 path: &Path,
69 current: usize,
70 opts: &DuOptions,
71 results: &mut Vec<Entry>,
72) -> io::Result<u64> {
73 let metadata = fs::symlink_metadata(path)?;
74 if metadata.is_file() {
75 let size = metadata.len();
76 if current <= opts.depth {
77 results.push(Entry {
78 path: path.to_path_buf(),
79 size,
80 });
81 }
82 return Ok(size);
83 }
84
85 let mut total = 0u64;
86 if metadata.is_dir() {
87 let entries: ReadDir = fs::read_dir(path)?;
88 for entry in entries {
89 let entry = entry?;
90 total += du_inner(&entry.path(), current + 1, opts, results)?;
91 }
92 if current <= opts.depth {
93 results.push(Entry {
94 path: path.to_path_buf(),
95 size: total,
96 });
97 }
98 }
99 Ok(total)
100}
101
102pub fn format_size(bytes: u64, unit: FormatUnit) -> String {
103 match unit {
104 FormatUnit::Bytes => bytes.to_string(),
105 FormatUnit::Kilo => format!("{:.1}KB", bytes as f64 / 1024.0),
106 FormatUnit::Mega => format!("{:.1}MB", bytes as f64 / 1024.0 / 1024.0),
107 FormatUnit::Giga => format!("{:.1}GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0),
108 FormatUnit::Human => {
109 const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
110 let mut size = bytes as f64;
111 let mut unit_idx = 0usize;
112 while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
113 size /= 1024.0;
114 unit_idx += 1;
115 }
116 format!("{:.1}{}", size, UNITS[unit_idx])
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use tempfile::tempdir;
125
126 #[test]
127 fn test_du_includes_files() {
128 let dir = tempdir().unwrap();
129 let file_path = dir.path().join("file.txt");
130 fs::write(&file_path, b"hello").unwrap();
131
132 let opts = DuOptions {
133 depth: 0,
134 unit: FormatUnit::Bytes,
135 };
136 let results = du(&file_path, &opts).unwrap();
137 assert_eq!(results.len(), 1);
138 assert_eq!(results[0].size, 5);
139 }
140
141 #[test]
142 fn test_du_nested_depth() {
143 let dir = tempdir().unwrap();
144 let sub = dir.path().join("sub");
145 fs::create_dir(&sub).unwrap();
146 fs::write(sub.join("a"), b"1234567890").unwrap();
147 fs::write(dir.path().join("b"), b"1234").unwrap();
148
149 let opts = DuOptions {
150 depth: 1,
151 unit: FormatUnit::Bytes,
152 };
153 let mut results = du(dir.path(), &opts).unwrap();
154 results.sort_by(|a, b| a.path.cmp(&b.path));
155 assert_eq!(results.len(), 3); let root = results.iter().find(|e| e.path == dir.path()).unwrap();
157 assert_eq!(root.size, 14);
158 }
159
160 #[test]
161 fn test_format_size_units() {
162 assert_eq!(format_size(1024, FormatUnit::Human), "1.0KB");
163 assert_eq!(format_size(1024, FormatUnit::Kilo), "1.0KB");
164 assert_eq!(format_size(1024 * 1024, FormatUnit::Mega), "1.0MB");
165 }
166}