everything_ipc/folder/
size.rs1use std::{
13 cell::UnsafeCell,
14 io,
15 path::{Path, PathBuf},
16 time::Duration,
17};
18
19use bon::{bon, builder};
20use rapidhash::{HashMapExt, RapidHashMap as HashMap};
21use thiserror::Error;
22use tracing::{debug, info, warn};
23use widestring::U16CString;
24use windows::{Win32::Storage::FileSystem::GetDiskFreeSpaceExW, core::PCWSTR};
25
26use crate::{
27 search,
28 wm::{self, EverythingClient, RequestFlags, SearchFlags},
29};
30
31#[derive(Error, Debug)]
32pub enum Error {
33 #[error("path is relative")]
34 RelativePath,
35
36 #[error("folder not found")]
37 NotFound,
38
39 #[error(transparent)]
40 Io(#[from] io::Error),
41
42 #[error(transparent)]
43 Ipc(#[from] wm::IpcError),
44}
45
46#[cfg(any(doc, not(feature = "drop-join-thread")))]
47thread_local! {
48 static CLIENT: UnsafeCell<FolderSizeClient> = const { UnsafeCell::new(FolderSizeClient::new()) };
49}
50
51#[cfg(any(doc, not(feature = "drop-join-thread")))]
72#[builder]
73pub fn get_folder_size(
74 #[builder(start_fn)] path: &Path,
75 timeout: Option<Duration>,
76 parent_max_size: Option<&mut u64>,
77 #[builder(default)] eager_get_links: bool,
78) -> Result<u64, Error> {
79 CLIENT.with(|cell| {
80 let client = unsafe { &mut *cell.get() };
81 client
82 .get_folder_size(path)
83 .maybe_timeout(timeout)
84 .maybe_parent_max_size(parent_max_size)
85 .eager_get_links(eager_get_links)
86 .call()
87 })
88}
89
90#[derive(Default)]
92pub struct FolderSizeClient {
93 everything: Option<EverythingClient>,
94 last_parent: PathBuf,
95 result_map: Option<HashMap<String, u64>>,
97}
98
99#[bon]
100impl FolderSizeClient {
101 pub const fn new() -> Self {
102 Self {
103 everything: None,
104 last_parent: PathBuf::new(),
105 result_map: None,
106 }
107 }
108
109 #[builder]
128 pub fn get_folder_size(
129 &mut self,
130 #[builder(start_fn)] path: &Path,
131 timeout: Option<Duration>,
132 parent_max_size: Option<&mut u64>,
133 #[builder(default)] eager_get_links: bool,
134 ) -> Result<u64, Error> {
135 debug_assert_eq!(search::normalize_path_ev(path), path);
136
137 let parent = match path.parent() {
139 Some(p) if p.as_os_str().is_empty() => return Err(Error::RelativePath),
140 Some(p) => p,
141 None => {
142 if path.as_os_str().len() == 3 {
144 let path_u16 = U16CString::from_os_str(path).unwrap();
145 let mut size = 0u64;
146 if unsafe {
147 GetDiskFreeSpaceExW(PCWSTR(path_u16.as_ptr()), None, Some(&mut size), None)
148 }
149 .is_ok()
150 {
151 return Ok(size);
152 }
153 }
154 return Err(Error::RelativePath);
155 }
156 };
157
158 let everything = match self.everything.as_mut() {
161 Some(everything) => everything,
162 None => self.everything.insert(EverythingClient::new()?),
163 };
164
165 let needs_query = self.last_parent != parent;
167
168 if needs_query {
169 self.last_parent = parent.to_path_buf();
171 self.result_map = None;
172
173 let search_query = format!(r#"folder:infolder:"{}""#, parent.display());
175 let query_list = everything
176 .query_wait(&search_query)
177 .search_flags(SearchFlags::empty())
178 .request_flags(RequestFlags::FileName | RequestFlags::Size)
179 .maybe_timeout(timeout)
180 .call()
181 .inspect_err(|e| warn!(%e, ?parent, "query failed"))?;
182 info!(len = query_list.len(), "query");
183
184 let mut result_map = HashMap::with_capacity(query_list.len());
186 for item in query_list.iter() {
187 if let (Some(filename), Some(mut file_size)) = (
188 item.get_str(RequestFlags::FileName),
189 item.get_size(RequestFlags::Size),
190 ) {
191 let filename_str = filename.to_string_lossy();
192
193 if eager_get_links && file_size == 0 {
195 let path = parent.join(&filename_str);
196 match search::canonicalize_path_ev(&path) {
197 Ok(realpath) if realpath != path => {
199 debug!(dir = filename_str, ?realpath);
200 match everything
201 .get_folder_size(&realpath)
202 .maybe_timeout(timeout)
203 .call()
204 {
205 Ok(size) => {
206 file_size = size;
207 }
208 e => warn!(?e, ?realpath, "query realpath failed"),
209 }
210 }
211 Ok(_) => (),
212 Err(e) => warn!(%e, ?path, "realpath failed"),
213 }
214 }
215
216 result_map.insert(filename_str, file_size);
217 }
218 }
219 self.result_map = Some(result_map);
220 }
221
222 let filename = path
225 .file_name()
226 .and_then(|f| f.to_str())
227 .ok_or(Error::RelativePath)?
228 .to_string();
229
230 match self.result_map.as_ref().and_then(|m| {
231 if let Some(max_size) = parent_max_size {
232 *max_size = m.values().max().copied().unwrap_or_default();
233 }
234
235 m.get(&filename).copied()
236 }) {
237 Some(0) if eager_get_links => {
239 return Ok(0);
240 }
241 Some(0) if !eager_get_links => {
243 let realpath = search::canonicalize_path_ev(path)?;
244 if realpath != path {
245 debug!(?realpath);
246 let size = everything
248 .get_folder_size(&realpath)
249 .maybe_timeout(timeout)
250 .call()?;
251
252 self.result_map.as_mut().unwrap().insert(filename, size);
255
256 return Ok(size);
257 }
258 return Ok(0);
260 }
261 Some(size) => return Ok(size),
262 None => {
263 debug!(filename, map = ?self.result_map);
264 }
265 }
266
267 Err(Error::NotFound)
269 }
270}
271
272#[cfg(not(feature = "drop-join-thread"))]
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test_log::test]
278 #[test_log(default_log_filter = "trace")]
279 fn get_folder_size_root() {
280 let r = get_folder_size(Path::new(r"C:\")).call();
281 dbg!(&r);
282 assert!(r.unwrap() > 0);
284 }
285
286 #[test_log::test]
287 #[test_log(default_log_filter = "trace")]
288 fn get_folder_size_ev() {
289 let r = get_folder_size(Path::new(r"C:\Windows")).call();
290 dbg!(&r);
291 assert!(r.unwrap() > 0);
292
293 let r = get_folder_size(Path::new(r"C:\Users")).call();
294 dbg!(&r);
295 assert!(r.unwrap() > 0);
296 }
297
298 #[test_log::test]
299 #[test_log(default_log_filter = "trace")]
300 fn get_folder_size_ev_max() {
301 let mut max_size: u64 = 0;
302 let r = get_folder_size(Path::new(r"C:\Windows"))
303 .parent_max_size(&mut max_size)
304 .call();
305 dbg!(&r, max_size);
306 assert!(r.unwrap() > 0);
307
308 let mut max_size2: u64 = 0;
309 let r = get_folder_size(Path::new(r"C:\Users"))
310 .parent_max_size(&mut max_size2)
311 .call();
312 dbg!(&r, max_size2);
313 assert!(r.unwrap() > 0);
314
315 assert_eq!(max_size, max_size2);
316 }
317
318 #[test_log::test]
319 #[test_log(default_log_filter = "trace")]
320 fn get_folder_size_ev_realpath() {
321 let r = get_folder_size(Path::new(r"C:\Documents and Settings"))
323 .call()
324 .unwrap();
325 dbg!(&r);
326 assert!(r > 0);
327 let r1 = get_folder_size(Path::new(r"C:\Documents and Settings"))
328 .call()
329 .unwrap();
330 dbg!(&r1);
331 assert_eq!(r, r1);
332
333 let r2 = get_folder_size(Path::new(r"C:\Users")).call().unwrap();
334 dbg!(&r2);
335 assert!(r2 > 0);
336 assert_eq!(r, r2);
337 }
338
339 #[test_log::test]
340 #[test_log(default_log_filter = "trace")]
341 fn get_folder_size_ev_realpath_eager() {
342 let mut max_size: u64 = 0;
344 let r = get_folder_size(Path::new(r"C:\Documents and Settings"))
345 .parent_max_size(&mut max_size)
346 .eager_get_links(true)
347 .call()
348 .unwrap();
349 info!(r, max_size);
350 assert!(r > 0);
351 let r1 = get_folder_size(Path::new(r"C:\Documents and Settings"))
352 .parent_max_size(&mut max_size)
353 .eager_get_links(true)
354 .call()
355 .unwrap();
356 info!(r1, max_size);
357 assert_eq!(r, r1);
358
359 let r2 = get_folder_size(Path::new(r"C:\Users"))
360 .parent_max_size(&mut max_size)
361 .eager_get_links(true)
362 .call()
363 .unwrap();
364 info!(r2, max_size);
365 assert!(r2 > 0);
366 assert_eq!(r, r2);
367 }
368}