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