use std::{
cell::UnsafeCell,
io,
path::{Path, PathBuf},
time::Duration,
};
use bon::{bon, builder};
use rapidhash::{HashMapExt, RapidHashMap as HashMap};
use thiserror::Error;
use tracing::{debug, info, warn};
use widestring::U16CString;
use windows::{Win32::Storage::FileSystem::GetDiskFreeSpaceExW, core::PCWSTR};
use crate::{
search,
wm::{self, EverythingClient, RequestFlags, SearchFlags},
};
#[derive(Error, Debug)]
pub enum Error {
#[error("path is relative")]
RelativePath,
#[error("folder not found")]
NotFound,
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Ipc(#[from] wm::IpcError),
}
#[cfg(any(doc, not(feature = "drop-join-thread")))]
thread_local! {
static CLIENT: UnsafeCell<FolderSizeClient> = const { UnsafeCell::new(FolderSizeClient::new()) };
}
#[cfg(any(doc, not(feature = "drop-join-thread")))]
#[builder]
pub fn get_folder_size(
#[builder(start_fn)] path: &Path,
timeout: Option<Duration>,
parent_max_size: Option<&mut u64>,
#[builder(default)] eager_get_links: bool,
) -> Result<u64, Error> {
CLIENT.with(|cell| {
let client = unsafe { &mut *cell.get() };
client
.get_folder_size(path)
.maybe_timeout(timeout)
.maybe_parent_max_size(parent_max_size)
.eager_get_links(eager_get_links)
.call()
})
}
#[derive(Default)]
pub struct FolderSizeClient {
everything: Option<EverythingClient>,
last_parent: PathBuf,
result_map: Option<HashMap<String, u64>>,
}
#[bon]
impl FolderSizeClient {
pub const fn new() -> Self {
Self {
everything: None,
last_parent: PathBuf::new(),
result_map: None,
}
}
#[builder]
pub fn get_folder_size(
&mut self,
#[builder(start_fn)] path: &Path,
timeout: Option<Duration>,
parent_max_size: Option<&mut u64>,
#[builder(default)] eager_get_links: bool,
) -> Result<u64, Error> {
debug_assert_eq!(search::normalize_path_ev(path), path);
let parent = match path.parent() {
Some(p) if p.as_os_str().is_empty() => return Err(Error::RelativePath),
Some(p) => p,
None => {
if path.as_os_str().len() == 3 {
let path_u16 = U16CString::from_os_str(path).unwrap();
let mut size = 0u64;
if unsafe {
GetDiskFreeSpaceExW(PCWSTR(path_u16.as_ptr()), None, Some(&mut size), None)
}
.is_ok()
{
return Ok(size);
}
}
return Err(Error::RelativePath);
}
};
let everything = match self.everything.as_mut() {
Some(everything) => everything,
None => self.everything.insert(EverythingClient::new()?),
};
let needs_query = self.last_parent != parent;
if needs_query {
self.last_parent = parent.to_path_buf();
self.result_map = None;
let search_query = format!(r#"folder:infolder:"{}""#, parent.display());
let query_list = everything
.query_wait(&search_query)
.search_flags(SearchFlags::empty())
.request_flags(RequestFlags::FileName | RequestFlags::Size)
.maybe_timeout(timeout)
.call()
.inspect_err(|e| warn!(%e, ?parent, "query failed"))?;
info!(len = query_list.len(), "query");
let mut result_map = HashMap::with_capacity(query_list.len());
for item in query_list.iter() {
if let (Some(filename), Some(mut file_size)) = (
item.get_str(RequestFlags::FileName),
item.get_size(RequestFlags::Size),
) {
let filename_str = filename.to_string_lossy();
if eager_get_links && file_size == 0 {
let path = parent.join(&filename_str);
match search::canonicalize_path_ev(&path) {
Ok(realpath) if realpath != path => {
debug!(dir = filename_str, ?realpath);
match everything
.get_folder_size(&realpath)
.maybe_timeout(timeout)
.call()
{
Ok(size) => {
file_size = size;
}
e => warn!(?e, ?realpath, "query realpath failed"),
}
}
Ok(_) => (),
Err(e) => warn!(%e, ?path, "realpath failed"),
}
}
result_map.insert(filename_str, file_size);
}
}
self.result_map = Some(result_map);
}
let filename = path
.file_name()
.and_then(|f| f.to_str())
.ok_or(Error::RelativePath)?
.to_string();
match self.result_map.as_ref().and_then(|m| {
if let Some(max_size) = parent_max_size {
*max_size = m.values().max().copied().unwrap_or_default();
}
m.get(&filename).copied()
}) {
Some(0) if eager_get_links => {
return Ok(0);
}
Some(0) if !eager_get_links => {
let realpath = search::canonicalize_path_ev(path)?;
if realpath != path {
debug!(?realpath);
let size = everything
.get_folder_size(&realpath)
.maybe_timeout(timeout)
.call()?;
self.result_map.as_mut().unwrap().insert(filename, size);
return Ok(size);
}
return Ok(0);
}
Some(size) => return Ok(size),
None => {
debug!(filename, map = ?self.result_map);
}
}
Err(Error::NotFound)
}
}
#[cfg(not(feature = "drop-join-thread"))]
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test]
#[test_log(default_log_filter = "trace")]
fn get_folder_size_root() {
let r = get_folder_size(Path::new(r"C:\")).call();
dbg!(&r);
assert!(r.unwrap() > 0);
}
#[test_log::test]
#[test_log(default_log_filter = "trace")]
fn get_folder_size_ev() {
let r = get_folder_size(Path::new(r"C:\Windows")).call();
dbg!(&r);
assert!(r.unwrap() > 0);
let r = get_folder_size(Path::new(r"C:\Users")).call();
dbg!(&r);
assert!(r.unwrap() > 0);
}
#[test_log::test]
#[test_log(default_log_filter = "trace")]
fn get_folder_size_ev_max() {
let mut max_size: u64 = 0;
let r = get_folder_size(Path::new(r"C:\Windows"))
.parent_max_size(&mut max_size)
.call();
dbg!(&r, max_size);
assert!(r.unwrap() > 0);
let mut max_size2: u64 = 0;
let r = get_folder_size(Path::new(r"C:\Users"))
.parent_max_size(&mut max_size2)
.call();
dbg!(&r, max_size2);
assert!(r.unwrap() > 0);
assert_eq!(max_size, max_size2);
}
#[test_log::test]
#[test_log(default_log_filter = "trace")]
fn get_folder_size_ev_realpath() {
let r = get_folder_size(Path::new(r"C:\Documents and Settings"))
.call()
.unwrap();
dbg!(&r);
assert!(r > 0);
let r1 = get_folder_size(Path::new(r"C:\Documents and Settings"))
.call()
.unwrap();
dbg!(&r1);
assert_eq!(r, r1);
let r2 = get_folder_size(Path::new(r"C:\Users")).call().unwrap();
dbg!(&r2);
assert!(r2 > 0);
assert_eq!(r, r2);
}
#[test_log::test]
#[test_log(default_log_filter = "trace")]
fn get_folder_size_ev_realpath_eager() {
let mut max_size: u64 = 0;
let r = get_folder_size(Path::new(r"C:\Documents and Settings"))
.parent_max_size(&mut max_size)
.eager_get_links(true)
.call()
.unwrap();
info!(r, max_size);
assert!(r > 0);
let r1 = get_folder_size(Path::new(r"C:\Documents and Settings"))
.parent_max_size(&mut max_size)
.eager_get_links(true)
.call()
.unwrap();
info!(r1, max_size);
assert_eq!(r, r1);
let r2 = get_folder_size(Path::new(r"C:\Users"))
.parent_max_size(&mut max_size)
.eager_get_links(true)
.call()
.unwrap();
info!(r2, max_size);
assert!(r2 > 0);
assert_eq!(r, r2);
}
}