use crate::handle::Handle;
use crate::meta::DirEntry;
use crate::{Error, Result};
use std::path::Path;
impl Handle {
pub fn mkdir(&self, path: impl AsRef<Path>) -> Result<()> {
let path = self.resolve_path(path.as_ref())?;
std::fs::create_dir(&path).map_err(Error::Io)
}
pub fn mkdir_all(&self, path: impl AsRef<Path>) -> Result<()> {
let path = self.resolve_path(path.as_ref())?;
let mut to_create: Vec<std::path::PathBuf> = Vec::new();
let mut current = path.as_path();
loop {
match current.metadata() {
Ok(m) if m.is_dir() => break, Ok(_) => {
return Err(Error::InvalidPath {
path: current.to_owned(),
reason: "a non-directory already exists at this path".into(),
});
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
to_create.push(current.to_owned());
match current.parent() {
Some(p) => current = p,
None => break,
}
}
Err(e) => return Err(Error::Io(e)),
}
}
to_create.reverse(); let mut completed: Vec<String> = Vec::new();
for dir in &to_create {
match std::fs::create_dir(dir) {
Ok(()) => {
completed.push(dir.display().to_string());
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
completed.push(dir.display().to_string());
}
Err(e) => {
return Err(Error::PartialDirectoryOp {
failed_step: format!("create_dir({}): {}", dir.display(), e),
completed_steps: completed,
});
}
}
}
Ok(())
}
pub fn rmdir(&self, path: impl AsRef<Path>) -> Result<()> {
let path = self.resolve_path(path.as_ref())?;
match std::fs::remove_dir(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::Io(e)),
}
}
pub fn rmdir_all(&self, path: impl AsRef<Path>) -> Result<()> {
let path = self.resolve_path(path.as_ref())?;
match std::fs::remove_dir_all(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::PartialDirectoryOp {
failed_step: format!("remove_dir_all({}): {}", path.display(), e),
completed_steps: Vec::new(),
}),
}
}
pub fn list(&self, path: impl AsRef<Path>) -> Result<Vec<DirEntry>> {
let path = self.resolve_path(path.as_ref())?;
let rd = std::fs::read_dir(&path).map_err(Error::Io)?;
let mut entries = Vec::new();
for item in rd {
let entry = item.map_err(Error::Io)?;
entries.push(DirEntry::from_std(entry));
}
Ok(entries)
}
pub fn is_dir(&self, path: impl AsRef<Path>) -> Result<bool> {
let path = self.resolve_path(path.as_ref())?;
match std::fs::metadata(&path) {
Ok(m) => Ok(m.is_dir()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(Error::Io(e)),
}
}
pub fn is_file(&self, path: impl AsRef<Path>) -> Result<bool> {
self.exists(path)
}
pub fn scan(&self, path: impl AsRef<Path>) -> Result<Vec<DirEntry>> {
let root = self.resolve_path(path.as_ref())?;
let mut out: Vec<DirEntry> = Vec::new();
scan_into(&root, false, &mut out)?;
Ok(out)
}
pub fn scan_all(&self, path: impl AsRef<Path>) -> Result<Vec<DirEntry>> {
let root = self.resolve_path(path.as_ref())?;
let mut out: Vec<DirEntry> = Vec::new();
scan_into(&root, true, &mut out)?;
Ok(out)
}
pub fn find(&self, path: impl AsRef<Path>, pattern: &str) -> Result<Vec<std::path::PathBuf>> {
let root = self.resolve_path(path.as_ref())?;
if pattern.contains("..") || std::path::Path::new(pattern).is_absolute() {
return Err(Error::InvalidPath {
path: std::path::PathBuf::from(pattern),
reason: "glob pattern must not escape the base directory".into(),
});
}
let combined = root.join(pattern);
let combined_str = combined.to_str().ok_or_else(|| Error::InvalidPath {
path: combined.clone(),
reason: "non-UTF-8 path component".into(),
})?;
let expanded = expand_braces(combined_str);
let mut out: Vec<std::path::PathBuf> = Vec::new();
let mut seen: std::collections::HashSet<std::path::PathBuf> =
std::collections::HashSet::new();
for sub in &expanded {
let paths = glob::glob(sub).map_err(|e| Error::GlobPatternInvalid {
reason: e.to_string(),
})?;
for entry in paths {
match entry {
Ok(p) => {
if seen.insert(p.clone()) {
out.push(p);
}
}
Err(e) => return Err(Error::Io(e.into_error())),
}
}
}
Ok(out)
}
pub fn count(&self, path: impl AsRef<Path>) -> Result<usize> {
let entries = self.scan(path)?;
Ok(entries.iter().filter(|e| e.is_file).count())
}
pub fn count_all(&self, path: impl AsRef<Path>) -> Result<usize> {
let entries = self.scan_all(path)?;
Ok(entries.iter().filter(|e| e.is_file).count())
}
}
const MAX_BRACE_EXPANSIONS: usize = 1024;
fn expand_braces(pattern: &str) -> Vec<String> {
let mut out = Vec::with_capacity(1);
expand_braces_into(pattern, &mut out);
out
}
fn expand_braces_into(pattern: &str, out: &mut Vec<String>) {
if out.len() >= MAX_BRACE_EXPANSIONS {
return;
}
let bytes = pattern.as_bytes();
let Some(open) = bytes.iter().position(|&b| b == b'{') else {
out.push(pattern.to_string());
return;
};
let Some(close_offset) = bytes[open + 1..].iter().position(|&b| b == b'}') else {
out.push(pattern.to_string());
return;
};
let close = open + 1 + close_offset;
let prefix = &pattern[..open];
let group = &pattern[open + 1..close];
let suffix = &pattern[close + 1..];
for alt in group.split(',') {
if out.len() >= MAX_BRACE_EXPANSIONS {
return;
}
let with_alt = format!("{prefix}{alt}{suffix}");
expand_braces_into(&with_alt, out);
}
}
fn scan_into(root: &Path, recursive: bool, out: &mut Vec<DirEntry>) -> Result<()> {
let rd = std::fs::read_dir(root).map_err(|e| {
if out.is_empty() {
Error::Io(e)
} else {
Error::PartialDirectoryOp {
failed_step: format!("read_dir({}): {}", root.display(), e),
completed_steps: out.iter().map(|x| x.path.display().to_string()).collect(),
}
}
})?;
for item in rd {
let entry = item.map_err(Error::Io)?;
let de = DirEntry::from_std(entry);
let is_dir = de.is_dir;
let path = de.path.clone();
out.push(de);
if recursive && is_dir {
scan_into(&path, true, out)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::builder::Builder;
use crate::method::Method;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn tmp_path(suffix: &str) -> std::path::PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"fsys_crud_dir_{}_{}_{}",
std::process::id(),
n,
suffix
))
}
struct TmpDir(std::path::PathBuf);
impl Drop for TmpDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn handle() -> crate::handle::Handle {
Builder::new()
.method(Method::Sync)
.build()
.expect("build handle")
}
#[test]
fn test_mkdir_creates_directory() {
let dir = tmp_path("mkdir");
let _g = TmpDir(dir.clone());
let h = handle();
h.mkdir(&dir).expect("mkdir");
assert!(dir.is_dir());
}
#[test]
fn test_mkdir_fails_if_exists() {
let dir = tmp_path("mkdir_exists");
let _g = TmpDir(dir.clone());
std::fs::create_dir(&dir).expect("create");
assert!(handle().mkdir(&dir).is_err());
}
#[test]
fn test_mkdir_all_creates_nested() {
let root = tmp_path("mkdir_all");
let _g = TmpDir(root.clone());
let nested = root.join("a").join("b").join("c");
handle().mkdir_all(&nested).expect("mkdir_all");
assert!(nested.is_dir());
}
#[test]
fn test_mkdir_all_idempotent() {
let dir = tmp_path("mkdir_all_idem");
let _g = TmpDir(dir.clone());
std::fs::create_dir(&dir).expect("create");
handle().mkdir_all(&dir).expect("mkdir_all on existing");
}
#[test]
fn test_rmdir_removes_empty() {
let dir = tmp_path("rmdir");
std::fs::create_dir(&dir).expect("create");
handle().rmdir(&dir).expect("rmdir");
assert!(!dir.exists());
}
#[test]
fn test_rmdir_idempotent() {
let dir = tmp_path("rmdir_idem");
handle().rmdir(&dir).expect("rmdir on non-existent");
}
#[test]
fn test_rmdir_all_removes_tree() {
let root = tmp_path("rmdir_all");
std::fs::create_dir_all(root.join("sub")).expect("create tree");
handle().rmdir_all(&root).expect("rmdir_all");
assert!(!root.exists());
}
#[test]
fn test_list_returns_entries() {
let root = tmp_path("list");
let _g = TmpDir(root.clone());
std::fs::create_dir(&root).expect("create root");
std::fs::write(root.join("file.txt"), b"x").expect("write");
std::fs::create_dir(root.join("subdir")).expect("create subdir");
let entries = handle().list(&root).expect("list");
assert_eq!(entries.len(), 2);
}
#[test]
fn test_expand_braces_no_braces_returns_input() {
assert_eq!(super::expand_braces("*.log"), vec!["*.log".to_string()]);
}
#[test]
fn test_expand_braces_single_group_two_alternatives() {
let out = super::expand_braces("{a,b}*.log");
assert_eq!(out, vec!["a*.log".to_string(), "b*.log".to_string()]);
}
#[test]
fn test_expand_braces_single_group_three_alternatives() {
let out = super::expand_braces("pre-{x,y,z}");
assert_eq!(
out,
vec![
"pre-x".to_string(),
"pre-y".to_string(),
"pre-z".to_string(),
]
);
}
#[test]
fn test_expand_braces_multiple_groups_cartesian() {
let out = super::expand_braces("{a,b}-{1,2}");
assert_eq!(
out,
vec![
"a-1".to_string(),
"a-2".to_string(),
"b-1".to_string(),
"b-2".to_string(),
]
);
}
#[test]
fn test_is_dir_reflects_state() {
let dir = tmp_path("is_dir");
let _g = TmpDir(dir.clone());
let h = handle();
assert!(!h.is_dir(&dir).expect("is_dir before create"));
std::fs::create_dir(&dir).expect("create");
assert!(h.is_dir(&dir).expect("is_dir after create"));
}
}