use std::collections::BTreeSet;
use std::fs;
#[cfg(unix)]
use std::os::unix::prelude::*;
use std::path::Path;
use std::path::PathBuf;
#[cfg(feature = "glob")]
use globwalk::GlobWalkerBuilder;
use crate::{XXError, XXResult};
pub use std::fs::*;
pub fn open<P: AsRef<Path>>(path: P) -> XXResult<fs::File> {
let path = path.as_ref();
debug!("open: {path:?}");
fs::File::open(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))
}
pub fn create<P: AsRef<Path>>(path: P) -> XXResult<fs::File> {
let path = path.as_ref();
debug!("create: {path:?}");
fs::File::create(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))
}
pub fn read_to_string<P: AsRef<Path>>(path: P) -> XXResult<String> {
debug!("read_to_string: {:?}", path.as_ref());
let path = path.as_ref();
let contents =
fs::read_to_string(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(contents)
}
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> XXResult<()> {
debug!("write: {:?}", path.as_ref());
let path = path.as_ref();
if let Some(parent) = path.parent() {
mkdirp(parent)?;
}
fs::write(path, contents).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(())
}
pub fn mkdirp<P: AsRef<Path>>(path: P) -> XXResult<()> {
let path = path.as_ref();
if path.exists() {
return Ok(());
}
debug!("mkdirp: {path:?}");
fs::create_dir_all(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(())
}
pub fn mv<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> XXResult<()> {
let from = from.as_ref();
let to = to.as_ref();
debug!("mv: {from:?} -> {to:?}");
if let Some(parent) = to.parent() {
mkdirp(parent)?;
}
fs::rename(from, to).map_err(|err| XXError::FileError(err, from.to_path_buf()))?;
Ok(())
}
pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> XXResult<()> {
let path = path.as_ref();
if path.exists() {
debug!("remove_dir_all: {path:?}");
fs::remove_dir_all(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
}
Ok(())
}
pub fn touch_dir<P: AsRef<Path>>(dir: P) -> XXResult<()> {
let dir = dir.as_ref().to_path_buf();
trace!("touch {}", dir.display());
mkdirp(&dir)?;
let now = filetime::FileTime::now();
filetime::set_file_times(&dir, now, now).map_err(|err| XXError::FileError(err, dir.clone()))?;
Ok(())
}
pub fn ls<P: AsRef<Path>>(path: P) -> XXResult<Vec<PathBuf>> {
let path = path.as_ref().to_path_buf();
debug!("ls: {:?}", &path);
let entries = fs::read_dir(&path).map_err(|err| XXError::FileError(err, path.clone()))?;
let mut files = BTreeSet::new();
for entry in entries {
let entry = entry.map_err(|err| XXError::FileError(err, path.clone()))?;
files.insert(entry.path());
}
Ok(files.into_iter().collect())
}
#[cfg(feature = "glob")]
pub fn glob<P: Into<PathBuf>>(input: P) -> XXResult<Vec<PathBuf>> {
let input = input.into();
debug!("glob: {:?}", input);
let root = input
.ancestors()
.skip(1)
.find(|a| !"*[{?".chars().any(|c| a.to_str().unwrap().contains(c)))
.unwrap()
.to_path_buf();
let pattern = input.strip_prefix(&root).unwrap();
let files = if pattern.to_string_lossy().contains('*') {
GlobWalkerBuilder::new(root, pattern.to_string_lossy())
.follow_links(true)
.build()
.map_err(|err| XXError::GlobwalkError(err, input))?
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.map(|e| e.into_path())
.collect()
} else {
vec![root.join(pattern)]
};
Ok(files)
}
pub fn display_path<P: AsRef<Path>>(path: P) -> String {
let home = homedir::my_home().unwrap_or_default();
let home = home.unwrap_or("/".into()).to_string_lossy().to_string();
let path = path.as_ref();
match path.starts_with(&home) && home != "/" {
true => path.to_string_lossy().replacen(&home, "~", 1),
false => path.display().to_string(),
}
}
#[cfg(unix)]
pub fn chmod<P: AsRef<Path>>(path: P, mode: u32) -> XXResult<()> {
let path = path.as_ref();
debug!("chmod: {mode:o} {path:?}");
fs::set_permissions(path, fs::Permissions::from_mode(mode))
.map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(())
}
pub fn find_up<FN: AsRef<str>>(from: &Path, filenames: &[FN]) -> Option<PathBuf> {
let mut current = from.to_path_buf();
loop {
for filename in filenames {
let path = current.join(filename.as_ref());
if path.exists() {
return Some(path);
}
}
if !current.pop() {
return None;
}
}
}
pub fn find_up_all<FN: AsRef<str>>(from: &Path, filenames: &[FN]) -> Vec<PathBuf> {
let mut current = from.to_path_buf();
let mut paths = vec![];
loop {
for filename in filenames {
let path = current.join(filename.as_ref());
if path.exists() {
paths.push(path);
}
}
if !current.pop() {
return paths;
}
}
}
#[cfg(unix)]
pub fn make_executable<P: AsRef<Path>>(path: P) -> XXResult<()> {
let path = path.as_ref();
let metadata = fs::metadata(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
let mode = metadata.permissions().mode();
if mode != 0o111 {
chmod(path, mode | 0o111)?;
}
Ok(())
}
#[cfg(windows)]
pub fn make_executable<P: AsRef<Path>>(_path: P) -> XXResult<()> {
Ok(())
}
pub fn append<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> XXResult<()> {
use std::io::Write;
let path = path.as_ref();
debug!("append: {path:?}");
if let Some(parent) = path.parent() {
mkdirp(parent)?;
}
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
file.write_all(contents.as_ref())
.map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(())
}
pub fn copy_dir_all<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> XXResult<()> {
let from = from.as_ref();
let to = to.as_ref();
debug!("copy_dir_all: {from:?} -> {to:?}");
mkdirp(to)?;
for entry in fs::read_dir(from).map_err(|err| XXError::FileError(err, from.to_path_buf()))? {
let entry = entry.map_err(|err| XXError::FileError(err, from.to_path_buf()))?;
let path = entry.path();
let dest = to.join(entry.file_name());
if path.is_dir() {
copy_dir_all(&path, &dest)?;
} else {
fs::copy(&path, &dest).map_err(|err| XXError::FileError(err, path.clone()))?;
}
}
Ok(())
}
pub fn size<P: AsRef<Path>>(path: P) -> XXResult<u64> {
let path = path.as_ref();
let metadata = fs::metadata(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(metadata.len())
}
pub fn is_empty_dir<P: AsRef<Path>>(path: P) -> XXResult<bool> {
let path = path.as_ref();
let mut entries =
fs::read_dir(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(entries.next().is_none())
}
pub fn which<S: AsRef<str>>(name: S) -> Option<PathBuf> {
let name = name.as_ref();
let path = Path::new(name);
if path.is_absolute() && path.exists() {
return Some(path.to_path_buf());
}
if let Ok(path_env) = std::env::var("PATH") {
for dir in std::env::split_paths(&path_env) {
let full_path = dir.join(name);
if full_path.exists() {
return Some(full_path);
}
#[cfg(windows)]
{
for ext in &["exe", "bat", "cmd"] {
let with_ext = dir.join(format!("{}.{}", name, ext));
if with_ext.exists() {
return Some(with_ext);
}
}
}
}
}
None
}
pub fn read<P: AsRef<Path>>(path: P) -> XXResult<Vec<u8>> {
let path = path.as_ref();
debug!("read: {:?}", path);
fs::read(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))
}
pub fn touch_file<P: AsRef<Path>>(path: P) -> XXResult<()> {
let path = path.as_ref();
debug!("touch_file: {:?}", path);
if let Some(parent) = path.parent() {
mkdirp(parent)?;
}
if !path.exists() {
fs::File::create(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
}
let now = filetime::FileTime::now();
filetime::set_file_times(path, now, now)
.map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(())
}
pub fn remove_file<P: AsRef<Path>>(path: P) -> XXResult<()> {
let path = path.as_ref();
if path.exists() {
debug!("remove_file: {:?}", path);
fs::remove_file(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
}
Ok(())
}
pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> XXResult<u64> {
let from = from.as_ref();
let to = to.as_ref();
debug!("copy: {:?} -> {:?}", from, to);
if let Some(parent) = to.parent() {
mkdirp(parent)?;
}
fs::copy(from, to).map_err(|err| XXError::FileError(err, from.to_path_buf()))
}
#[cfg(unix)]
pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> XXResult<()> {
let original = original.as_ref();
let link = link.as_ref();
debug!("symlink: {:?} -> {:?}", link, original);
if let Some(parent) = link.parent() {
mkdirp(parent)?;
}
std::os::unix::fs::symlink(original, link)
.map_err(|err| XXError::FileError(err, link.to_path_buf()))
}
#[cfg(windows)]
pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> XXResult<()> {
let original = original.as_ref();
let link = link.as_ref();
debug!("symlink: {:?} -> {:?}", link, original);
if let Some(parent) = link.parent() {
mkdirp(parent)?;
}
let is_dir = if original.exists() {
original.is_dir()
} else {
let path_str = original.to_string_lossy();
path_str.ends_with('/') || path_str.ends_with('\\') || original.extension().is_none()
};
if is_dir {
std::os::windows::fs::symlink_dir(original, link)
.map_err(|err| XXError::FileError(err, link.to_path_buf()))
} else {
std::os::windows::fs::symlink_file(original, link)
.map_err(|err| XXError::FileError(err, link.to_path_buf()))
}
}
pub fn is_symlink<P: AsRef<Path>>(path: P) -> bool {
path.as_ref().is_symlink()
}
pub fn resolve_symlink<P: AsRef<Path>>(path: P) -> XXResult<PathBuf> {
let path = path.as_ref();
fs::read_link(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))
}
pub fn display_rel_path<P: AsRef<Path>>(path: P) -> String {
let path = path.as_ref();
if let Ok(cwd) = std::env::current_dir()
&& let Ok(rel) = path.strip_prefix(&cwd)
{
let rel_str = format!("./{}", rel.display());
let abs_str = path.display().to_string();
if rel_str.len() < abs_str.len() {
return rel_str;
}
}
display_path(path)
}
pub fn split_file_name<P: AsRef<Path>>(path: P) -> (String, Option<String>) {
let path = path.as_ref();
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
if name.starts_with('.') && !name[1..].contains('.') {
return (name.to_string(), None);
}
if let Some(dot_pos) = name.rfind('.') {
if dot_pos == 0 {
(name.to_string(), None)
} else {
let (stem, ext) = name.split_at(dot_pos);
(stem.to_string(), Some(ext.to_string()))
}
} else {
(name.to_string(), None)
}
}
#[cfg(unix)]
pub fn is_executable<P: AsRef<Path>>(path: P) -> bool {
let path = path.as_ref();
if let Ok(metadata) = fs::metadata(path) {
let mode = metadata.permissions().mode();
mode & 0o111 != 0
} else {
false
}
}
#[cfg(windows)]
pub fn is_executable<P: AsRef<Path>>(path: P) -> bool {
let path = path.as_ref();
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com")
} else {
false
}
}
pub fn canonicalize<P: AsRef<Path>>(path: P) -> XXResult<PathBuf> {
let path = path.as_ref();
fs::canonicalize(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))
}
pub fn same_file<P: AsRef<Path>, Q: AsRef<Path>>(path1: P, path2: Q) -> XXResult<bool> {
let path1 = path1.as_ref();
let path2 = path2.as_ref();
#[cfg(unix)]
{
let meta1 =
fs::metadata(path1).map_err(|err| XXError::FileError(err, path1.to_path_buf()))?;
let meta2 =
fs::metadata(path2).map_err(|err| XXError::FileError(err, path2.to_path_buf()))?;
use std::os::unix::fs::MetadataExt;
Ok(meta1.dev() == meta2.dev() && meta1.ino() == meta2.ino())
}
#[cfg(windows)]
{
fs::metadata(path1).map_err(|err| XXError::FileError(err, path1.to_path_buf()))?;
fs::metadata(path2).map_err(|err| XXError::FileError(err, path2.to_path_buf()))?;
match (fs::canonicalize(path1), fs::canonicalize(path2)) {
(Ok(c1), Ok(c2)) => Ok(c1 == c2),
_ => Ok(path1 == path2),
}
}
}
pub fn modified_time<P: AsRef<Path>>(path: P) -> XXResult<std::time::Duration> {
let path = path.as_ref();
let metadata = fs::metadata(path).map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
let modified = metadata
.modified()
.map_err(|err| XXError::FileError(err, path.to_path_buf()))?;
Ok(modified
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_str_eq;
use test_log::test;
use crate::test;
use super::*;
#[test]
fn test_read_to_string() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("test.txt");
write(&path, "Hello, world!").unwrap();
assert_str_eq!(read_to_string(&path).unwrap(), "Hello, world!");
}
#[test]
fn test_read_file_not_found() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("test.txt");
let err = read_to_string(path).unwrap_err();
assert_eq!(
err.to_string().split_once('\n').unwrap().0,
"No such file or directory (os error 2)"
);
}
#[cfg(feature = "glob")]
#[test]
fn test_glob() {
let tmpdir = test::tempdir();
let dir = tmpdir.path().join("dir");
fs::create_dir(&dir).unwrap();
let file1 = dir.join("file1.txt");
let file2 = dir.join("file2.txt");
write(&file1, "Hello, world!").unwrap();
write(&file2, "Goodbye, world!").unwrap();
let files = glob(dir.join("*.txt")).unwrap();
assert_eq!(files.len(), 2);
assert!(files.contains(&file1));
assert!(files.contains(&file2));
}
#[cfg(unix)]
#[test]
fn test_chmod() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("test.txt");
write(&path, "Hello, world!").unwrap();
chmod(&path, 0o755).unwrap();
let metadata = fs::metadata(&path).unwrap();
let mode = metadata.permissions().mode();
assert_eq!(format!("{mode:o}"), "100755");
}
#[cfg(unix)]
#[test]
fn test_make_executable() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("test.txt");
write(&path, "Hello, world!").unwrap();
chmod(&path, 0o644).unwrap();
make_executable(&path).unwrap();
let metadata = fs::metadata(&path).unwrap();
let mode = metadata.permissions().mode();
assert_eq!(format!("{mode:o}"), "100755");
}
#[test]
fn test_append() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("append_test.txt");
append(&path, "Line 1\n").unwrap();
assert_str_eq!(read_to_string(&path).unwrap(), "Line 1\n");
append(&path, "Line 2\n").unwrap();
assert_str_eq!(read_to_string(&path).unwrap(), "Line 1\nLine 2\n");
}
#[test]
fn test_append_no_parent() {
let tmpdir = test::tempdir();
let original_dir = std::env::current_dir().unwrap();
#[allow(unused_unsafe)]
unsafe {
std::env::set_current_dir(&tmpdir).unwrap();
}
append("test.txt", "content").unwrap();
assert_str_eq!(read_to_string("test.txt").unwrap(), "content");
#[allow(unused_unsafe)]
unsafe {
std::env::set_current_dir(original_dir).unwrap();
}
}
#[test]
fn test_copy_dir_all() {
let tmpdir = test::tempdir();
let src_dir = tmpdir.path().join("src");
let dest_dir = tmpdir.path().join("dest");
mkdirp(src_dir.join("subdir")).unwrap();
write(src_dir.join("file1.txt"), "content1").unwrap();
write(src_dir.join("subdir/file2.txt"), "content2").unwrap();
copy_dir_all(&src_dir, &dest_dir).unwrap();
assert_str_eq!(
read_to_string(dest_dir.join("file1.txt")).unwrap(),
"content1"
);
assert_str_eq!(
read_to_string(dest_dir.join("subdir/file2.txt")).unwrap(),
"content2"
);
}
#[test]
fn test_is_empty_dir() {
let tmpdir = test::tempdir();
let empty_dir = tmpdir.path().join("empty");
let non_empty_dir = tmpdir.path().join("non_empty");
mkdirp(&empty_dir).unwrap();
mkdirp(&non_empty_dir).unwrap();
assert!(is_empty_dir(&empty_dir).unwrap());
write(non_empty_dir.join("file.txt"), "content").unwrap();
assert!(!is_empty_dir(&non_empty_dir).unwrap());
}
#[test]
fn test_which() {
#[cfg(unix)]
{
assert!(which("sh").is_some());
}
#[cfg(windows)]
{
assert!(which("cmd").is_some());
}
assert!(which("definitely_not_a_real_command_xyz123").is_none());
}
#[test]
fn test_size() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("size_test.txt");
write(&path, "12345").unwrap();
assert_eq!(size(&path).unwrap(), 5);
write(&path, "1234567890").unwrap();
assert_eq!(size(&path).unwrap(), 10);
}
#[test]
fn test_read_bytes() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("bytes_test.bin");
let data = vec![0x00, 0x01, 0x02, 0xFF];
write(&path, &data).unwrap();
assert_eq!(read(&path).unwrap(), data);
}
#[test]
fn test_read_bytes_not_found() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("nonexistent.bin");
let result = read(&path);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, crate::XXError::FileError(_, _)));
}
#[test]
#[cfg(unix)]
fn test_read_bytes_no_permission() {
use std::os::unix::fs::PermissionsExt;
let tmpdir = test::tempdir();
let path = tmpdir.path().join("no_read.bin");
write(&path, b"secret").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
let result = read(&path);
assert!(result.is_err());
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[test]
fn test_touch_file() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("touch_test.txt");
touch_file(&path).unwrap();
assert!(path.exists());
let mtime1 = modified_time(&path).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
touch_file(&path).unwrap();
let mtime2 = modified_time(&path).unwrap();
assert!(mtime2 >= mtime1);
}
#[test]
fn test_remove_file() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("remove_test.txt");
write(&path, "content").unwrap();
assert!(path.exists());
remove_file(&path).unwrap();
assert!(!path.exists());
remove_file(&path).unwrap();
}
#[test]
fn test_copy_file() {
let tmpdir = test::tempdir();
let from = tmpdir.path().join("copy_from.txt");
let to = tmpdir.path().join("copy_to.txt");
write(&from, "copy content").unwrap();
let bytes = copy(&from, &to).unwrap();
assert_eq!(bytes, 12);
assert_str_eq!(read_to_string(&to).unwrap(), "copy content");
}
#[cfg(unix)]
#[test]
fn test_symlink() {
let tmpdir = test::tempdir();
let original = tmpdir.path().join("original.txt");
let link = tmpdir.path().join("link.txt");
write(&original, "original content").unwrap();
symlink(&original, &link).unwrap();
assert!(is_symlink(&link));
assert!(!is_symlink(&original));
assert_eq!(resolve_symlink(&link).unwrap(), original);
assert_str_eq!(read_to_string(&link).unwrap(), "original content");
}
#[cfg(unix)]
#[test]
fn test_is_executable() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("script.sh");
write(&path, "#!/bin/sh\necho hello").unwrap();
assert!(!is_executable(&path));
make_executable(&path).unwrap();
assert!(is_executable(&path));
}
#[test]
fn test_same_file() {
let tmpdir = test::tempdir();
let file1 = tmpdir.path().join("file1.txt");
let file2 = tmpdir.path().join("file2.txt");
write(&file1, "content").unwrap();
write(&file2, "content").unwrap();
assert!(same_file(&file1, &file1).unwrap());
assert!(!same_file(&file1, &file2).unwrap());
}
#[test]
fn test_canonicalize() {
let path = canonicalize(".").unwrap();
assert!(path.is_absolute());
}
#[test]
fn test_modified_time() {
let tmpdir = test::tempdir();
let path = tmpdir.path().join("mtime_test.txt");
write(&path, "content").unwrap();
let mtime = modified_time(&path).unwrap();
assert!(mtime.as_secs() > 0);
}
}