1use rayon::prelude::*;
2use serde::Serialize;
3use std::error::Error;
4use std::ffi::OsStr;
5use std::fs;
6use std::path::Path;
7
8mod ffi;
9pub mod projects;
10pub mod tui;
11
12pub use projects::{
13 analyze, clean, dir_size, scan, Project, ProjectAnalysis, ProjectType, ScanError, ScanOptions,
14};
15
16#[derive(Serialize, Clone)]
17pub struct DiskItem {
18 pub name: String,
19 pub disk_size: u64,
20 pub children: Option<Vec<DiskItem>>,
21}
22
23impl DiskItem {
24 pub fn from_analyze(
25 path: &Path,
26 apparent: bool,
27 root_dev: u64,
28 ) -> Result<Self, Box<dyn Error>> {
29 let name = path
30 .file_name()
31 .unwrap_or(&OsStr::new("."))
32 .to_string_lossy()
33 .to_string();
34
35 let file_info = FileInfo::from_path(path, apparent)?;
36
37 match file_info {
38 FileInfo::Directory { volume_id } => {
39 if volume_id != root_dev {
40 return Err("Filesystem boundary crossed".into());
41 }
42
43 let sub_entries = fs::read_dir(path)?
44 .filter_map(Result::ok)
45 .collect::<Vec<_>>();
46
47 let mut sub_items = sub_entries
48 .par_iter()
49 .filter_map(|entry| {
50 DiskItem::from_analyze(&entry.path(), apparent, root_dev).ok()
51 })
52 .collect::<Vec<_>>();
53
54 sub_items.sort_unstable_by(|a, b| a.disk_size.cmp(&b.disk_size).reverse());
55
56 Ok(DiskItem {
57 name,
58 disk_size: sub_items.iter().map(|di| di.disk_size).sum(),
59 children: Some(sub_items),
60 })
61 }
62 FileInfo::File { size, .. } => Ok(DiskItem {
63 name,
64 disk_size: size,
65 children: None,
66 }),
67 }
68 }
69
70 pub fn from_shallow_scan(
78 path: &Path,
79 apparent: bool,
80 root_dev: u64,
81 ) -> Result<Self, Box<dyn Error>> {
82 Self::from_analyze(path, apparent, root_dev)
83 }
84}
85
86pub enum FileInfo {
87 File { size: u64, volume_id: u64 },
88 Directory { volume_id: u64 },
89}
90
91impl FileInfo {
92 #[cfg(unix)]
93 pub fn from_path(path: &Path, apparent: bool) -> Result<Self, Box<dyn Error>> {
94 use std::os::unix::fs::MetadataExt;
95
96 let md = path.symlink_metadata()?;
97 if md.is_dir() {
98 Ok(FileInfo::Directory {
99 volume_id: md.dev(),
100 })
101 } else {
102 let size = if apparent {
103 md.len()
104 } else {
105 md.blocks() * 512
106 };
107 Ok(FileInfo::File {
108 size,
109 volume_id: md.dev(),
110 })
111 }
112 }
113
114 #[cfg(windows)]
115 pub fn from_path(path: &Path, apparent: bool) -> Result<Self, Box<dyn Error>> {
116 use winapi_util::{file, Handle};
117 const FILE_ATTRIBUTE_DIRECTORY: u64 = 0x10;
118
119 let h = Handle::from_path_any(path)?;
120 let md = file::information(h)?;
121
122 if md.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 {
123 Ok(FileInfo::Directory {
124 volume_id: md.volume_serial_number(),
125 })
126 } else {
127 let size = if apparent {
128 md.file_size()
129 } else {
130 ffi::compressed_size(path)?
131 };
132 Ok(FileInfo::File {
133 size,
134 volume_id: md.volume_serial_number(),
135 })
136 }
137 }
138}
139
140#[cfg(all(test, unix))]
141mod tests {
142 use super::FileInfo;
143 use std::error::Error;
144 use std::fs::{self, File};
145 use std::time::{SystemTime, UNIX_EPOCH};
146
147 #[test]
148 fn apparent_size_uses_logical_file_length() -> Result<(), Box<dyn Error>> {
149 let dir = std::env::temp_dir().join(format!(
150 "disk-cleaner-{}",
151 SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos()
152 ));
153 fs::create_dir(&dir)?;
154 let path = dir.join("sparse.bin");
155 let file = File::create(&path)?;
156 let sparse_len = 1024 * 1024;
157 file.set_len(sparse_len)?;
158 drop(file);
159
160 let apparent_size = match FileInfo::from_path(&path, true)? {
161 FileInfo::File { size, .. } => size,
162 FileInfo::Directory { .. } => panic!("test path should be a file"),
163 };
164 let disk_size = match FileInfo::from_path(&path, false)? {
165 FileInfo::File { size, .. } => size,
166 FileInfo::Directory { .. } => panic!("test path should be a file"),
167 };
168
169 fs::remove_file(&path)?;
170 fs::remove_dir(&dir)?;
171
172 assert_eq!(apparent_size, sparse_len);
173 assert!(disk_size <= apparent_size);
174 Ok(())
175 }
176}