use reflink_copy::reflink_or_copy;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::fs::{self, DirEntry, File, FileType, OpenOptions};
use std::io::{ErrorKind, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use tracing::{instrument, trace};
#[cfg(feature = "editor-config")]
pub use crate::fs_editor::*;
pub use crate::fs_error::FsError;
#[cfg(feature = "fs-lock")]
pub use crate::fs_lock::*;
fn read_dir_iter(path: &Path) -> Result<Option<fs::ReadDir>, FsError> {
match fs::read_dir(path) {
Ok(entries) => Ok(Some(entries)),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(None),
Err(error) => Err(FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
}),
}
}
fn extract_entry_and_type(
path: &Path,
entry: std::io::Result<DirEntry>,
) -> Result<Option<(DirEntry, FileType)>, FsError> {
let entry = entry.map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})?;
let Ok(file_type) = entry.file_type() else {
return Ok(None);
};
Ok(Some((entry, file_type)))
}
#[inline]
#[instrument(skip(data))]
pub fn append_file<D: AsRef<[u8]>>(path: impl AsRef<Path> + Debug, data: D) -> Result<(), FsError> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
trace!(file = ?path, "Appending file");
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|error| FsError::Write {
path: path.to_path_buf(),
error: Box::new(error),
})?;
file.write_all(data.as_ref())
.map_err(|error| FsError::Write {
path: path.to_path_buf(),
error: Box::new(error),
})?;
Ok(())
}
#[inline]
#[instrument]
pub fn copy_file<S: AsRef<Path> + Debug, D: AsRef<Path> + Debug>(
from: S,
to: D,
) -> Result<(), FsError> {
let from = from.as_ref();
let to = to.as_ref();
if let Some(parent) = to.parent() {
create_dir_all(parent)?;
}
trace!(from = ?from, to = ?to, "Copying file");
fs::copy(from, to).map_err(|error| FsError::Copy {
from: from.to_path_buf(),
to: to.to_path_buf(),
error: Box::new(error),
})?;
Ok(())
}
#[inline]
#[instrument]
pub fn copy_dir_all<F: AsRef<Path> + Debug, T: AsRef<Path> + Debug>(
from_root: F,
to_root: T,
) -> Result<(), FsError> {
let from_root = from_root.as_ref();
let to_root = to_root.as_ref();
let Some(entries) = read_dir_iter(from_root)? else {
return Ok(());
};
trace!(
from = ?from_root,
to = ?to_root,
"Copying directory"
);
for entry in entries {
let Some((entry, file_type)) = extract_entry_and_type(from_root, entry)? else {
continue;
};
let from_path = entry.path();
let to_path = to_root.join(entry.file_name());
if file_type.is_file() {
copy_file(from_path, to_path)?;
} else if file_type.is_dir() {
copy_dir_all(from_path, to_path)?;
}
}
Ok(())
}
#[inline]
#[instrument]
pub fn create_file<T: AsRef<Path> + Debug>(path: T) -> Result<File, FsError> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
trace!(file = ?path, "Creating file");
File::create(path).map_err(|error| FsError::Create {
path: path.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument]
pub fn create_file_if_missing<T: AsRef<Path> + Debug>(path: T) -> Result<File, FsError> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
trace!(file = ?path, "Creating file without truncating");
#[allow(clippy::suspicious_open_options)]
OpenOptions::new()
.write(true)
.create(true)
.open(path)
.map_err(|error| FsError::Create {
path: path.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument]
pub fn create_dir_all<T: AsRef<Path> + Debug>(path: T) -> Result<(), FsError> {
let path = path.as_ref();
if path.as_os_str().is_empty() || path.exists() {
return Ok(());
}
match fs::create_dir_all(path) {
Ok(()) => {
trace!(dir = ?path, "Creating directory");
}
Err(error) if error.kind() == ErrorKind::AlreadyExists => {}
Err(error) => {
return Err(FsError::Create {
path: path.to_path_buf(),
error: Box::new(error),
});
}
};
Ok(())
}
#[inline]
pub fn file_name<T: AsRef<Path>>(path: T) -> String {
path.as_ref()
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("<unknown>")
.to_owned()
}
#[inline]
pub fn find_upwards<F, P>(name: F, start_dir: P) -> Option<PathBuf>
where
F: AsRef<OsStr> + Debug,
P: AsRef<Path> + Debug,
{
find_upwards_until(name, start_dir, PathBuf::from("/"))
}
#[inline]
#[instrument]
pub fn find_upwards_until<F, S, E>(name: F, start_dir: S, end_dir: E) -> Option<PathBuf>
where
F: AsRef<OsStr> + Debug,
S: AsRef<Path> + Debug,
E: AsRef<Path> + Debug,
{
let name = name.as_ref();
let mut dir = start_dir.as_ref();
let end_dir = end_dir.as_ref();
trace!(
file = name.to_str(),
dir = ?dir,
"Traversing upwards to find a file/root"
);
loop {
let findable = dir.join(name);
if findable.exists() {
return Some(findable);
}
if dir == end_dir {
return None;
}
match dir.parent() {
Some(parent_dir) => dir = parent_dir,
None => return None,
}
}
}
#[inline]
pub fn find_upwards_root<F, P>(name: F, start_dir: P) -> Option<PathBuf>
where
F: AsRef<OsStr> + Debug,
P: AsRef<Path> + Debug,
{
find_upwards_root_until(name, start_dir, PathBuf::from("/"))
}
#[inline]
pub fn find_upwards_root_until<F, S, E>(name: F, start_dir: S, end_dir: E) -> Option<PathBuf>
where
F: AsRef<OsStr> + Debug,
S: AsRef<Path> + Debug,
E: AsRef<Path> + Debug,
{
find_upwards_until(name, start_dir, end_dir)
.and_then(|p| p.parent().map(|parent| parent.to_path_buf()))
}
#[cfg(unix)]
pub fn is_executable<T: AsRef<Path>>(path: T) -> bool {
use std::os::unix::fs::PermissionsExt;
fs::metadata(path.as_ref())
.is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
}
#[cfg(windows)]
pub fn is_executable<T: AsRef<Path>>(path: T) -> bool {
path.as_ref().extension().is_some_and(|ext| ext == "exe")
}
#[inline]
#[instrument]
pub fn is_stale<T: AsRef<Path> + Debug>(
path: T,
accessed: bool,
duration: Duration,
) -> Result<bool, FsError> {
stale(path, accessed, duration, SystemTime::now()).map(|res| res.is_some())
}
#[inline]
#[instrument]
pub fn metadata<T: AsRef<Path> + Debug>(path: T) -> Result<fs::Metadata, FsError> {
let path = path.as_ref();
trace!(file = ?path, "Reading file metadata");
fs::metadata(path).map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument]
pub fn open_file<T: AsRef<Path> + Debug>(path: T) -> Result<File, FsError> {
let path = path.as_ref();
trace!(file = ?path, "Opening file in read-only mode");
File::open(path).map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument]
pub fn open_file_for_appending<T: AsRef<Path> + Debug>(path: T) -> Result<File, FsError> {
let path = path.as_ref();
trace!(file = ?path, "Opening file in append mode");
let file = OpenOptions::new()
.read(true)
.append(true)
.open(path)
.map_err(|error| FsError::Write {
path: path.to_path_buf(),
error: Box::new(error),
})?;
Ok(file)
}
#[inline]
#[instrument]
pub fn open_file_for_writing<T: AsRef<Path> + Debug>(path: T) -> Result<File, FsError> {
let path = path.as_ref();
trace!(file = ?path, "Opening file in write mode");
let file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(|error| FsError::Write {
path: path.to_path_buf(),
error: Box::new(error),
})?;
Ok(file)
}
#[inline]
#[instrument]
pub fn read_dir<T: AsRef<Path> + Debug>(path: T) -> Result<Vec<DirEntry>, FsError> {
let path = path.as_ref();
let mut results = vec![];
let Some(entries) = read_dir_iter(path)? else {
return Ok(results);
};
trace!(dir = ?path, "Reading directory");
for entry in entries {
results.push(entry.map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})?);
}
Ok(results)
}
#[inline]
#[instrument]
pub fn read_dir_all<T: AsRef<Path> + Debug>(path: T) -> Result<Vec<DirEntry>, FsError> {
let entries = read_dir(path)?;
let mut results = vec![];
for entry in entries {
if let Ok(file_type) = entry.file_type() {
if file_type.is_dir() {
results.extend(read_dir_all(entry.path())?);
} else {
results.push(entry);
}
}
}
Ok(results)
}
#[inline]
#[instrument]
pub fn read_file<T: AsRef<Path> + Debug>(path: T) -> Result<String, FsError> {
let path = path.as_ref();
trace!(file = ?path, "Reading file");
fs::read_to_string(path).map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument]
pub fn read_file_bytes<T: AsRef<Path> + Debug>(path: T) -> Result<Vec<u8>, FsError> {
let path = path.as_ref();
trace!(file = ?path, "Reading bytes of file");
fs::read(path).map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument]
pub fn reflink_file<S: AsRef<Path> + Debug, D: AsRef<Path> + Debug>(
from: S,
to: D,
) -> Result<(), FsError> {
let from = from.as_ref();
let to = to.as_ref();
if let Some(parent) = to.parent() {
create_dir_all(parent)?;
}
trace!(from = ?from, to = ?to, "Reflinking file");
reflink_or_copy(from, to).map_err(|error| FsError::Copy {
from: from.to_path_buf(),
to: to.to_path_buf(),
error: Box::new(error),
})?;
Ok(())
}
#[inline]
pub fn remove<T: AsRef<Path> + Debug>(path: T) -> Result<(), FsError> {
let path = path.as_ref();
let Ok(file_type) = path.symlink_metadata().map(|meta| meta.file_type()) else {
return Ok(());
};
if file_type.is_symlink() {
remove_link(path)?;
} else if file_type.is_file() {
remove_file(path)?;
} else if file_type.is_dir() {
remove_dir_all(path)?;
}
Ok(())
}
#[inline]
#[instrument]
pub fn remove_link<T: AsRef<Path> + Debug>(path: T) -> Result<(), FsError> {
let path = path.as_ref();
if let Ok(metadata) = path.symlink_metadata()
&& metadata.is_symlink()
{
match fs::remove_file(path) {
Ok(()) => {
trace!(file = ?path, "Removing symlink");
}
Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => {
return Err(FsError::Remove {
path: path.to_path_buf(),
error: Box::new(error),
});
}
};
}
Ok(())
}
#[inline]
#[instrument]
pub fn remove_file<T: AsRef<Path> + Debug>(path: T) -> Result<(), FsError> {
let path = path.as_ref();
if path.exists() {
match fs::remove_file(path) {
Ok(()) => {
trace!(file = ?path, "Removing file");
}
Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => {
return Err(FsError::Remove {
path: path.to_path_buf(),
error: Box::new(error),
});
}
};
}
Ok(())
}
#[inline]
#[instrument]
pub fn remove_file_if_stale<T: AsRef<Path> + Debug>(
path: T,
duration: Duration,
) -> Result<Option<u64>, FsError> {
let path = path.as_ref();
if let Some((size, _)) = stale(path, true, duration, SystemTime::now())? {
remove_file(path)?;
return Ok(Some(size));
}
Ok(None)
}
#[inline]
#[instrument]
pub fn remove_dir_all<T: AsRef<Path> + Debug>(path: T) -> Result<(), FsError> {
let path = path.as_ref();
match fs::remove_dir_all(path) {
Ok(()) => {
trace!(dir = ?path, "Removing directory");
}
Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => {
return Err(FsError::Remove {
path: path.to_path_buf(),
error: Box::new(error),
});
}
};
Ok(())
}
#[inline]
#[instrument]
pub fn remove_dir_all_except<T: AsRef<Path> + Debug>(
path: T,
exceptions: Vec<PathBuf>,
) -> Result<(), FsError> {
let base_dir = path.as_ref();
trace!(dir = ?base_dir, exceptions = ?exceptions, "Removing directory with exceptions");
fn traverse(base_dir: &Path, traverse_dir: &Path, exclude: &[PathBuf]) -> Result<(), FsError> {
let Some(entries) = read_dir_iter(traverse_dir)? else {
return Ok(());
};
for entry in entries {
let Some((entry, file_type)) = extract_entry_and_type(traverse_dir, entry)? else {
continue;
};
let abs_path = entry.path();
let rel_path = abs_path.strip_prefix(base_dir).unwrap_or(&abs_path);
let is_excluded = exclude
.iter()
.any(|ex| rel_path == ex || rel_path.starts_with(ex));
let contains_excluded_path = exclude.iter().any(|ex| ex.starts_with(rel_path));
if is_excluded {
continue;
} else if contains_excluded_path && file_type.is_dir() {
traverse(base_dir, &abs_path, exclude)?;
} else if file_type.is_dir() {
remove_dir_all(abs_path)?;
} else if file_type.is_symlink() {
remove_link(abs_path)?;
} else if file_type.is_file() {
remove_file(abs_path)?;
}
}
Ok(())
}
traverse(base_dir, base_dir, &exceptions)?;
Ok(())
}
pub struct RemoveDirContentsResult {
pub files_deleted: usize,
pub bytes_saved: u64,
}
#[instrument]
pub fn remove_dir_stale_contents<P: AsRef<Path> + Debug>(
dir: P,
duration: Duration,
) -> Result<RemoveDirContentsResult, FsError> {
let mut files_deleted: usize = 0;
let mut bytes_saved: u64 = 0;
let dir = dir.as_ref();
trace!(
dir = ?dir,
"Removing stale contents from directory"
);
fn traverse(
dir: &Path,
duration: Duration,
files_deleted: &mut usize,
bytes_saved: &mut u64,
) -> Result<(), FsError> {
let Some(entries) = read_dir_iter(dir)? else {
return Ok(());
};
for entry in entries {
let Some((entry, file_type)) = extract_entry_and_type(dir, entry)? else {
continue;
};
let path = entry.path();
if file_type.is_dir() {
traverse(&path, duration, files_deleted, bytes_saved)?;
} else if file_type.is_file()
&& let Ok(Some(size)) = remove_file_if_stale(path, duration)
{
*files_deleted += 1;
*bytes_saved += size;
}
}
Ok(())
}
traverse(dir, duration, &mut files_deleted, &mut bytes_saved)?;
Ok(RemoveDirContentsResult {
files_deleted,
bytes_saved,
})
}
#[inline]
#[instrument]
pub fn rename<F: AsRef<Path> + Debug, T: AsRef<Path> + Debug>(
from: F,
to: T,
) -> Result<(), FsError> {
let from = from.as_ref();
let to = to.as_ref();
if let Some(parent) = to.parent() {
create_dir_all(parent)?;
}
trace!(from = ?from, to = ?to, "Renaming file");
fs::rename(from, to).map_err(|error| FsError::Rename {
from: from.to_path_buf(),
to: to.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument]
pub fn stale<T: AsRef<Path> + Debug>(
path: T,
accessed: bool,
duration: Duration,
current_time: SystemTime,
) -> Result<Option<(u64, SystemTime)>, FsError> {
let path = path.as_ref();
if let Ok(meta) = metadata(path) {
let mut time = meta.modified().or_else(|_| meta.created());
if accessed && let Ok(accessed_time) = meta.accessed() {
time = Ok(accessed_time);
}
if let Ok(check_time) = time
&& check_time < (current_time - duration)
{
return Ok(Some((meta.len(), check_time)));
}
}
Ok(None)
}
#[inline]
#[instrument]
pub fn truncate_file_handle<T: AsRef<Path> + Debug>(
path: T,
file: &mut File,
) -> Result<(), FsError> {
let path = path.as_ref();
file.set_len(0).map_err(|error| FsError::Write {
path: path.to_owned(),
error: Box::new(error),
})?;
file.seek(SeekFrom::Start(0))
.map_err(|error| FsError::Write {
path: path.to_owned(),
error: Box::new(error),
})?;
Ok(())
}
#[cfg(unix)]
#[inline]
#[instrument]
pub fn update_perms<T: AsRef<Path> + Debug>(path: T, mode: Option<u32>) -> Result<(), FsError> {
use std::os::unix::fs::PermissionsExt;
let path = path.as_ref();
let mode = mode.unwrap_or(0o755);
trace!(file = ?path, mode = format!("{:#02o}", mode), "Updating file permissions");
fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|error| {
FsError::Perms {
path: path.to_path_buf(),
error: Box::new(error),
}
})?;
Ok(())
}
#[cfg(not(unix))]
#[inline]
pub fn update_perms<T: AsRef<Path>>(_path: T, _mode: Option<u32>) -> Result<(), FsError> {
Ok(())
}
#[inline]
#[instrument(skip(data))]
pub fn write_file<D: AsRef<[u8]>>(path: impl AsRef<Path> + Debug, data: D) -> Result<(), FsError> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
trace!(file = ?path, "Writing file");
fs::write(path, data).map_err(|error| FsError::Write {
path: path.to_path_buf(),
error: Box::new(error),
})
}