use {
std::path::{Path, PathBuf},
crate::{
CODE_NAME,
FilePermissions,
Result,
filter::Filter,
},
};
#[cfg(not(feature="tokio"))]
use std::fs::{self, ReadDir};
#[cfg(feature="tokio")]
use tokio::fs::{self, ReadDir};
#[derive(Debug)]
pub struct FileDiscovery<F> {
root: PathBuf,
filter: F,
recursive: bool,
current: ReadDir,
sub_dirs: Option<Vec<PathBuf>>,
max_depth: Option<usize>,
}
macro_rules! make_file_discovery { ($dir: ident, $recursive: ident, $filter: ident, $max_depth: ident) => {{
Ok(Self {
root: async_call!(fs::canonicalize($dir.as_ref()))?,
filter: $filter,
recursive: $recursive,
current: async_call!(fs::read_dir($dir))?,
sub_dirs: None,
max_depth: $max_depth,
})
}}}
macro_rules! next { ($self: ident) => {{
loop {
#[cfg(not(feature="tokio"))]
let next = $self.current.next();
#[cfg(feature="tokio")]
let next = $self.current.next_entry().await.transpose();
match next {
Some(Ok(entry)) => {
match async_call!(entry.file_type()) {
Ok(file_type) => {
let path = entry.path();
let is_symlink = file_type.is_symlink();
if file_type.is_dir() || (is_symlink && path.is_dir()) {
if $self.recursive == false || is_symlink {
continue;
}
if let Some(max_depth) = $self.max_depth.as_ref() {
match async_call!(fs::canonicalize(&path)) {
Ok(path) => match depth_from(&$self.root, &path) {
Ok(depth) => if &depth >= max_depth {
continue;
},
Err(err) => return Some(Err(err)),
},
Err(err) => return Some(Err(err)),
};
}
if async_call!($self.filter.accept(&path)) == false {
continue;
}
match $self.sub_dirs.as_mut() {
Some(sub_dirs) => sub_dirs.push(path),
None => $self.sub_dirs = Some(vec!(path)),
};
} else if file_type.is_file() || (is_symlink && path.is_file()) {
if async_call!($self.filter.accept(&path)) == false {
continue;
}
return Some(Ok(path));
}
},
Err(err) => return Some(Err(err)),
};
},
Some(Err(err)) => return Some(Err(err)),
None => match $self.sub_dirs.as_mut() {
None => return None,
Some(sub_dirs) => match sub_dirs.len() {
0 => return None,
_ => match async_call!(fs::read_dir(sub_dirs.remove(0))) {
Ok(new) => $self.current = new,
Err(err) => return Some(Err(err)),
},
},
},
};
}
}}}
impl<'a> FileDiscovery<Filter<'a>> {
#[cfg(not(feature="tokio"))]
#[doc(cfg(not(feature="tokio")))]
pub fn make<P>(dir: P, recursive: bool, filter: Filter<'a>, max_depth: Option<usize>) -> Result<Self> where P: AsRef<Path> {
make_file_discovery!(dir, recursive, filter, max_depth)
}
#[cfg(feature="tokio")]
#[doc(cfg(feature="tokio"))]
pub async fn make<P>(dir: P, recursive: bool, filter: Filter<'a>, max_depth: Option<usize>) -> Result<Self> where P: AsRef<Path> {
make_file_discovery!(dir, recursive, filter, max_depth)
}
}
#[cfg(feature="tokio")]
#[doc(cfg(feature="tokio"))]
impl FileDiscovery<Filter<'_>> {
pub async fn next(&mut self) -> Option<Result<PathBuf>> {
next!(self)
}
pub async fn count(mut self) -> Option<usize> {
let mut result = usize::MIN;
while let Some(path) = self.next().await {
if path.is_ok() {
result = match result.checked_add(1) {
None => return None,
Some(n) => n,
};
}
}
Some(result)
}
}
#[cfg(not(feature="tokio"))]
#[doc(cfg(not(feature="tokio")))]
impl Iterator for FileDiscovery<Filter<'_>> {
type Item = Result<PathBuf>;
fn next(&mut self) -> Option<Self::Item> {
next!(self)
}
}
fn depth_from<P, Q>(root_dir: P, path: Q) -> Result<usize> where P: AsRef<Path>, Q: AsRef<Path> {
let root_dir = root_dir.as_ref();
let path = path.as_ref();
let mut depth: usize = 0;
let mut found_root = false;
for a in path.ancestors().skip(1) {
if a == root_dir {
found_root = true;
break;
} else {
match depth.checked_add(1) {
Some(new_depth) => depth = new_depth,
None => return Err(err!("Directory level of {path:?} is too deep: {depth}")),
};
}
}
if found_root {
Ok(depth)
} else {
Err(err!("{path:?} was expected to be inside of {root_dir:?}, but not"))
}
}
#[cfg(not(feature="tokio"))]
#[doc(cfg(not(feature="tokio")))]
pub fn find_files<'a, P>(dir: P, recursive: bool, filter: Filter<'a>) -> Result<FileDiscovery<Filter<'a>>> where P: AsRef<Path> {
FileDiscovery::make(dir, recursive, filter, None)
}
#[cfg(feature="tokio")]
#[doc(cfg(feature="tokio"))]
pub async fn find_files<'a, P>(dir: P, recursive: bool, filter: Filter<'a>) -> Result<FileDiscovery<Filter<'a>>> where P: AsRef<Path> {
FileDiscovery::make(dir, recursive, filter, None).await
}
#[cfg(not(feature="tokio"))]
macro_rules! exists { ($p: expr) => { $p.exists() }}
#[cfg(feature="tokio")]
macro_rules! exists { ($p: expr) => { fs::try_exists($p).await? }}
macro_rules! on_same_unix_device { ($first: ident, $second: ident) => {{
use std::os::unix::fs::MetadataExt;
let first = $first.as_ref();
let second = $second.as_ref();
if exists!(first) == false || exists!(second) == false {
Ok(false)
} else {
Ok(async_call!(fs::metadata(first))?.dev() == async_call!(fs::metadata(second))?.dev())
}
}}}
#[cfg(all(not(feature="tokio"), unix))]
#[doc(cfg(all(not(feature="tokio"), unix)))]
pub fn on_same_unix_device<P, Q>(first: P, second: Q) -> Result<bool> where P: AsRef<Path>, Q: AsRef<Path> {
on_same_unix_device!(first, second)
}
#[cfg(all(feature="tokio", unix))]
#[doc(cfg(all(feature="tokio", unix)))]
pub async fn on_same_unix_device<P, Q>(first: P, second: Q) -> Result<bool> where P: AsRef<Path>, Q: AsRef<Path> {
on_same_unix_device!(first, second)
}
#[cfg(all(not(feature="tokio"), unix))]
#[doc(cfg(all(not(feature="tokio"), unix)))]
#[test]
fn test_on_same_unix_device() -> Result<()> {
assert!(on_same_unix_device(file!(), PathBuf::from(file!()).parent().unwrap().join("lib.rs"))?);
assert!(on_same_unix_device(file!(), std::env::temp_dir())? == false);
Ok(())
}
macro_rules! write_file { ($target: ident, $file_permissions: ident, $data: ident, $tmp_file_suffix: ident) => {{
let target = $target.as_ref();
let tmp_file_suffix = $tmp_file_suffix.as_ref().trim();
if tmp_file_suffix.is_empty() {
return Err(err!("Temporary file suffix is empty"));
}
let tmp_file = match (target.parent(), target.file_name().map(|n| n.to_str())) {
(Some(dir), Some(Some(file_name))) => dir.join(format!("{file_name}.{tmp_file_suffix}.{CODE_NAME}.tmp")),
_ => return Err(err!("Failed to get host directory and/or file name of {target:?}")),
};
macro_rules! job { () => {{
#[cfg(unix)]
let file_permissions = match $file_permissions {
Some(file_permissions) => Some(file_permissions),
None => if exists!(target) {
Some(async_call!(fs::metadata(target))?.permissions().try_into()?)
} else {
None
},
};
async_call!(fs::write(&tmp_file, $data))?;
#[cfg(unix)]
if let Some(file_permissions) = file_permissions {
async_call!(file_permissions.set(&tmp_file))?;
}
async_call!(fs::rename(&tmp_file, target))
}}}
match job!() {
Ok(()) => Ok(()),
Err(err) => {
if exists!(&tmp_file) {
async_call!(fs::remove_file(tmp_file))?;
}
Err(err)
},
}
}}}
#[cfg(not(feature="tokio"))]
#[doc(cfg(not(feature="tokio")))]
pub fn write_file<P, B, S>(target: P, file_permissions: Option<FilePermissions>, data: B, tmp_file_suffix: S) -> Result<()>
where P: AsRef<Path>, B: AsRef<[u8]>, S: AsRef<str> {
write_file!(target, file_permissions, data, tmp_file_suffix)
}
#[cfg(feature="tokio")]
#[doc(cfg(feature="tokio"))]
pub async fn write_file<P, B, S>(target: P, file_permissions: Option<FilePermissions>, data: B, tmp_file_suffix: S) -> Result<()>
where P: AsRef<Path>, B: AsRef<[u8]>, S: AsRef<str> {
write_file!(target, file_permissions, data, tmp_file_suffix)
}