use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use crate::core::error::CruftError;
pub const SIZE_TIMEOUT: Duration = Duration::from_secs(60);
pub async fn get_folder_size(path: PathBuf) -> Result<u64, CruftError> {
let total = Arc::new(AtomicU64::new(0));
let pending = Arc::new(AtomicUsize::new(1));
let (done_tx, done_rx) = oneshot::channel();
let done = Arc::new(Mutex::new(Some(done_tx)));
let cancel = CancellationToken::new();
let ctx = WalkCtx {
total: total.clone(),
pending: pending.clone(),
done: done.clone(),
cancel: cancel.clone(),
};
spawn_walk(path.clone(), ctx);
let result = match tokio::time::timeout(SIZE_TIMEOUT, done_rx).await {
Ok(_) => Ok(total.load(Ordering::SeqCst)),
Err(_) => Err(CruftError::SizeTimeout(path)),
};
cancel.cancel();
result
}
#[derive(Clone)]
struct WalkCtx {
total: Arc<AtomicU64>,
pending: Arc<AtomicUsize>,
done: Arc<Mutex<Option<oneshot::Sender<()>>>>,
cancel: CancellationToken,
}
fn spawn_walk(path: PathBuf, ctx: WalkCtx) {
tokio::spawn(async move {
walk(path, ctx).await;
});
}
async fn walk(path: PathBuf, ctx: WalkCtx) {
if ctx.cancel.is_cancelled() {
decrement(&ctx);
return;
}
let mut rd = match tokio::fs::read_dir(&path).await {
Ok(r) => r,
Err(_) => {
decrement(&ctx);
return;
}
};
let mut current: u64 = 0;
let mut subdirs: Vec<PathBuf> = Vec::new();
loop {
if ctx.cancel.is_cancelled() {
break;
}
let entry = match rd.next_entry().await {
Ok(Some(e)) => e,
Ok(None) => break,
Err(_) => continue,
};
let ft = match entry.file_type().await {
Ok(t) => t,
Err(_) => continue,
};
if ft.is_symlink() {
continue;
}
if ft.is_dir() {
current = current.saturating_add(4096);
subdirs.push(entry.path());
} else if let Ok(meta) = entry.metadata().await {
current = current.saturating_add(real_size(&meta));
}
}
ctx.total.fetch_add(current, Ordering::SeqCst);
if !subdirs.is_empty() && !ctx.cancel.is_cancelled() {
ctx.pending.fetch_add(subdirs.len(), Ordering::SeqCst);
for sub in subdirs {
spawn_walk(sub, ctx.clone());
}
}
decrement(&ctx);
}
fn decrement(ctx: &WalkCtx) {
if ctx.pending.fetch_sub(1, Ordering::SeqCst) == 1
&& let Ok(mut g) = ctx.done.lock()
&& let Some(tx) = g.take()
{
let _ = tx.send(());
}
}
#[cfg(unix)]
fn real_size(m: &std::fs::Metadata) -> u64 {
use std::os::unix::fs::MetadataExt;
m.blocks().saturating_mul(512)
}
#[cfg(not(unix))]
fn real_size(m: &std::fs::Metadata) -> u64 {
m.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timeout_constant_is_60_seconds() {
assert_eq!(SIZE_TIMEOUT, Duration::from_secs(60));
}
#[cfg(unix)]
#[test]
fn real_size_uses_blocks_on_unix() {
let meta = std::fs::metadata(file!()).unwrap();
let s = real_size(&meta);
assert!(s % 512 == 0, "expected multiple of 512, got {s}");
}
}