use std::{collections::VecDeque, fs::Permissions, path::{Path, PathBuf}}; use anyhow::Result; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; pub async fn calculate_size(path: &Path) -> u64 { let mut total = 0; let mut stack = VecDeque::from([path.to_path_buf()]); while let Some(path) = stack.pop_front() { let Ok(meta) = fs::symlink_metadata(&path).await else { continue; }; if !meta.is_dir() { total += meta.len(); continue; } let Ok(mut it) = fs::read_dir(path).await else { continue; }; while let Ok(Some(entry)) = it.next_entry().await { let Ok(meta) = entry.metadata().await else { continue; }; if meta.is_dir() { stack.push_back(entry.path()); } else { total += meta.len(); } } } total } pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver> { let (tx, rx) = mpsc::channel(1); let (tick_tx, mut tick_rx) = oneshot::channel(); tokio::spawn({ let (from, to) = (from.to_path_buf(), to.to_path_buf()); async move { _ = match fs::copy(from, to).await { Ok(len) => tick_tx.send(Ok(len)), Err(e) => tick_tx.send(Err(e)), }; } }); tokio::spawn({ let tx = tx.clone(); let to = to.to_path_buf(); async move { let mut last = 0; let mut exit = None; loop { select! { res = &mut tick_rx => exit = Some(res.unwrap()), _ = tx.closed() => break, _ = time::sleep(time::Duration::from_secs(1)) => (), } match exit { Some(Ok(len)) => { if len > last { tx.send(Ok(len - last)).await.ok(); } tx.send(Ok(0)).await.ok(); break; } Some(Err(e)) => { tx.send(Err(e)).await.ok(); break; } None => {} } let len = fs::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0); if len > last { tx.send(Ok(len - last)).await.ok(); last = len; } } } }); rx } // Convert a file mode to a string representation #[allow(clippy::collapsible_else_if)] pub fn permissions(permissions: Permissions) -> Option { #[cfg(windows)] return None; #[cfg(unix)] Some({ use std::os::unix::prelude::PermissionsExt; use libc::{S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFSOCK, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR}; #[cfg(target_os = "macos")] let m = permissions.mode() as u16; #[cfg(target_os = "freebsd")] let m = permissions.mode() as u16; #[cfg(target_os = "netbsd")] let m = permissions.mode(); #[cfg(target_os = "linux")] let m = permissions.mode(); let mut s = String::with_capacity(10); // File type s.push(match m & S_IFMT { S_IFBLK => 'b', S_IFCHR => 'c', S_IFDIR => 'd', S_IFIFO => 'p', S_IFLNK => 'l', S_IFSOCK => 's', _ => '-', }); // Owner s.push(if m & S_IRUSR != 0 { 'r' } else { '-' }); s.push(if m & S_IWUSR != 0 { 'w' } else { '-' }); s.push(if m & S_IXUSR != 0 { if m & S_ISUID != 0 { 's' } else { 'x' } } else { if m & S_ISUID != 0 { 'S' } else { '-' } }); // Group s.push(if m & S_IRGRP != 0 { 'r' } else { '-' }); s.push(if m & S_IWGRP != 0 { 'w' } else { '-' }); s.push(if m & S_IXGRP != 0 { if m & S_ISGID != 0 { 's' } else { 'x' } } else { if m & S_ISGID != 0 { 'S' } else { '-' } }); // Other s.push(if m & S_IROTH != 0 { 'r' } else { '-' }); s.push(if m & S_IWOTH != 0 { 'w' } else { '-' }); s.push(if m & S_IXOTH != 0 { if m & S_ISVTX != 0 { 't' } else { 'x' } } else { if m & S_ISVTX != 0 { 'T' } else { '-' } }); s }) } // Find the max common root of a list of files // e.g. /a/b/c, /a/b/d -> /a/b // /aa/bb/cc, /aa/dd/ee -> /aa pub fn max_common_root(files: &[impl AsRef]) -> PathBuf { if files.is_empty() { return PathBuf::new(); } let mut it = files.iter().map(|p| p.as_ref().parent().unwrap_or(Path::new("")).components()); let mut root = it.next().unwrap().collect::(); for components in it { let mut new_root = PathBuf::new(); for (a, b) in root.components().zip(components) { if a != b { break; } new_root.push(a); } root = new_root; } root } #[cfg(unix)] #[test] fn test_max_common_root() { assert_eq!(max_common_root(&[] as &[PathBuf]).as_os_str(), ""); assert_eq!(max_common_root(&["".into()] as &[PathBuf]).as_os_str(), ""); assert_eq!(max_common_root(&["a".into()] as &[PathBuf]).as_os_str(), ""); assert_eq!(max_common_root(&["/a".into()] as &[PathBuf]).as_os_str(), "/"); assert_eq!(max_common_root(&["/a/b".into()] as &[PathBuf]).as_os_str(), "/a"); assert_eq!( max_common_root(&["/a/b/c".into(), "/a/b/d".into()] as &[PathBuf]).as_os_str(), "/a/b" ); assert_eq!( max_common_root(&["/aa/bb/cc".into(), "/aa/dd/ee".into()] as &[PathBuf]).as_os_str(), "/aa" ); assert_eq!( max_common_root( &["/aa/bb/cc".into(), "/aa/bb/cc/dd/ee".into(), "/aa/bb/cc/ff".into()] as &[PathBuf] ) .as_os_str(), "/aa/bb" ); }