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 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();
mkdirp(path.parent().unwrap())?;
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);
mkdirp(to.parent().unwrap())?;
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(())
}
#[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(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");
}
}