use serde::Serialize;
use std::{
fs::{self, metadata, symlink_metadata},
path::{Path, PathBuf},
};
use tempfile::{self, tempdir};
#[derive(Debug, Serialize)]
#[non_exhaustive]
pub struct DiskEntry {
pub path: PathBuf,
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<DiskEntry>>,
}
pub fn is_dir<P: AsRef<Path>>(path: P) -> crate::api::Result<bool> {
metadata(path).map(|md| md.is_dir()).map_err(Into::into)
}
fn is_symlink<P: AsRef<Path>>(path: P) -> crate::api::Result<bool> {
let path = path.as_ref();
symlink_metadata(path)
.map(|md| md.is_symlink())
.map_err(|e| crate::api::Error::PathIo(e, path.to_path_buf()))
}
pub fn read_dir<P: AsRef<Path>>(path: P, recursive: bool) -> crate::api::Result<Vec<DiskEntry>> {
read_dir_with_options(path, recursive, ReadDirOptions { scope: None })
}
#[derive(Clone, Copy)]
pub(crate) struct ReadDirOptions<'a> {
pub scope: Option<&'a crate::FsScope>,
}
pub(crate) fn read_dir_with_options<P: AsRef<Path>>(
path: P,
recursive: bool,
options: ReadDirOptions<'_>,
) -> crate::api::Result<Vec<DiskEntry>> {
let mut files_and_dirs: Vec<DiskEntry> = vec![];
let path = path.as_ref();
for entry in fs::read_dir(path).map_err(|e| crate::api::Error::PathIo(e, path.to_path_buf()))? {
let path = entry?.path();
if let Ok(flag) = is_dir(&path) {
files_and_dirs.push(DiskEntry {
path: path.clone(),
children: if flag {
Some(
if recursive
&& (!is_symlink(&path)? || options.scope.map(|s| s.is_allowed(&path)).unwrap_or(true))
{
read_dir_with_options(&path, true, options)?
} else {
vec![]
},
)
} else {
None
},
name: path
.file_name()
.map(|name| name.to_string_lossy())
.map(|name| name.to_string()),
});
}
}
Result::Ok(files_and_dirs)
}
pub fn with_temp_dir<F: FnOnce(&tempfile::TempDir)>(callback: F) -> crate::api::Result<()> {
let dir = tempdir()?;
callback(&dir);
dir.close()?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use quickcheck_macros::quickcheck;
use std::{ffi::OsStr, path::PathBuf};
#[quickcheck]
fn qc_is_dir(f: String) -> bool {
if is_dir(f.clone()).is_ok() {
PathBuf::from(f).is_dir()
} else {
true
}
}
fn name_from_path(path: PathBuf) -> Option<String> {
path
.file_name()
.map(|name| name.to_string_lossy())
.map(|name| name.to_string())
}
#[test]
fn check_read_dir_recursively() {
let dir = PathBuf::from("test/api/");
let mut file_one = dir.clone();
file_one.push("test.txt");
let mut file_two = dir.clone();
file_two.push("test_binary");
let res = read_dir(dir, true);
assert!(res.is_ok());
if let Ok(vec) = res {
assert_eq!(vec.len(), 2);
let first = &vec[0];
let second = &vec[1];
if first.path.extension() == Some(OsStr::new("txt")) {
assert_eq!(first.path, file_one);
assert!(first.children.is_none());
assert_eq!(first.name, name_from_path(file_one));
assert_eq!(second.path, file_two);
assert!(second.children.is_none());
assert_eq!(second.name, name_from_path(file_two));
} else {
assert_eq!(first.path, file_two);
assert!(first.children.is_none());
assert_eq!(first.name, name_from_path(file_two));
assert_eq!(second.path, file_one);
assert!(second.children.is_none());
assert_eq!(second.name, name_from_path(file_one));
}
}
}
#[test]
fn check_read_dir() {
let dir = PathBuf::from("test/api/");
let res = read_dir(dir, false);
assert!(res.is_ok());
if let Ok(vec) = res {
assert_eq!(vec.len(), 2);
let first = &vec[0];
let second = &vec[1];
if first.path.extension() == Some(OsStr::new("txt")) {
assert_eq!(first.path, PathBuf::from("test/api/test.txt"));
assert!(first.children.is_none());
assert_eq!(first.name, Some("test.txt".to_string()));
assert_eq!(second.path, PathBuf::from("test/api/test_binary"));
assert!(second.children.is_none());
assert_eq!(second.name, Some("test_binary".to_string()));
} else {
assert_eq!(second.path, PathBuf::from("test/api/test.txt"));
assert!(second.children.is_none());
assert_eq!(second.name, Some("test.txt".to_string()));
assert_eq!(first.path, PathBuf::from("test/api/test_binary"));
assert!(first.children.is_none());
assert_eq!(first.name, Some("test_binary".to_string()));
}
}
}
#[test]
fn check_test_dir() {
let callback = |td: &tempfile::TempDir| {
println!("{td:?}");
};
let res = with_temp_dir(callback);
assert!(res.is_ok());
}
}