use std::cmp;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::fs::{self, File, OpenOptions};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use tracing::{instrument, trace};
pub use crate::fs_error::FsError;
#[cfg(feature = "fs-lock")]
pub use crate::fs_lock::*;
#[inline]
#[instrument(skip(data))]
pub fn append_file<D: AsRef<[u8]>>(path: impl AsRef<Path> + Debug, data: D) -> Result<(), FsError> {
use std::io::Write;
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 mut dirs = vec![];
trace!(
from = ?from_root,
to = ?to_root,
"Copying directory"
);
for entry in read_dir(from_root)? {
if let Ok(file_type) = entry.file_type() {
let path = entry.path();
let rel_path = path.strip_prefix(from_root).unwrap();
if file_type.is_file() {
copy_file(&path, to_root.join(rel_path))?;
} else if file_type.is_dir() {
dirs.push(rel_path.to_path_buf());
}
}
}
for dir in dirs {
copy_dir_all(from_root.join(&dir), to_root.join(dir))?;
}
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() {
return Ok(());
}
if !path.exists() {
trace!(dir = ?path, "Creating directory");
fs::create_dir_all(path).map_err(|error| FsError::Create {
path: path.to_path_buf(),
error: Box::new(error),
})?;
}
Ok(())
}
#[instrument(skip(content))]
pub fn detect_indentation<T: AsRef<str>>(content: T) -> String {
let mut spaces = 0;
let mut tabs = 0;
let mut lowest_space_width = 0;
let mut lowest_tab_width = 0;
fn count_line_indent(line: &str, indent: char) -> usize {
let mut line_count = 0;
let mut line_check = line;
while let Some(inner) = line_check.strip_prefix(indent) {
line_count += 1;
line_check = inner;
}
line_count
}
for line in content.as_ref().lines() {
if line.starts_with(' ') {
let line_spaces = count_line_indent(line, ' ');
if line_spaces % 2 == 1 {
continue;
}
spaces += 1;
if lowest_space_width == 0 || line_spaces < lowest_space_width {
lowest_space_width = line_spaces;
}
} else if line.starts_with('\t') {
let line_tabs = count_line_indent(line, '\t');
tabs += 1;
if lowest_tab_width == 0 || line_tabs < lowest_tab_width {
lowest_tab_width = line_tabs;
}
} else {
continue;
}
}
if tabs > spaces {
"\t".repeat(cmp::max(lowest_tab_width, 1))
} else {
" ".repeat(cmp::max(lowest_space_width, 2))
}
}
#[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 dir = start_dir.as_ref();
let name = name.as_ref();
let findable = dir.join(name);
trace!(
file = name.to_str(),
dir = ?dir,
"Traversing upwards to find a file/root"
);
if findable.exists() {
return Some(findable);
}
if dir == end_dir.as_ref() {
return None;
}
match dir.parent() {
Some(parent_dir) => find_upwards_until(name, parent_dir, end_dir),
None => 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).map(|p| p.parent().unwrap().to_path_buf())
}
#[cfg(feature = "editor-config")]
pub struct EditorConfigProps {
pub eof: String,
pub indent: String,
}
#[cfg(feature = "editor-config")]
impl EditorConfigProps {
pub fn apply_eof(&self, data: &mut String) {
if !self.eof.is_empty() && !data.ends_with(&self.eof) {
data.push_str(&self.eof);
}
}
}
#[cfg(feature = "editor-config")]
#[instrument]
pub fn get_editor_config_props<T: AsRef<Path> + Debug>(
path: T,
) -> Result<EditorConfigProps, FsError> {
use ec4rs::property::*;
let path = path.as_ref();
let editor_config = ec4rs::properties_of(path).unwrap_or_default();
let tab_width = editor_config
.get::<TabWidth>()
.unwrap_or(TabWidth::Value(4));
let indent_size = editor_config
.get::<IndentSize>()
.unwrap_or(IndentSize::Value(2));
let indent_style = editor_config.get::<IndentStyle>().ok();
let insert_final_newline = editor_config
.get::<FinalNewline>()
.unwrap_or(FinalNewline::Value(true));
Ok(EditorConfigProps {
eof: if matches!(insert_final_newline, FinalNewline::Value(true)) {
"\n".into()
} else {
"".into()
},
indent: match indent_style {
Some(IndentStyle::Tabs) => "\t".into(),
Some(IndentStyle::Spaces) => match indent_size {
IndentSize::UseTabWidth => match tab_width {
TabWidth::Value(value) => " ".repeat(value),
},
IndentSize::Value(value) => " ".repeat(value),
},
None => {
if path.exists() {
detect_indentation(read_file(path)?)
} else {
" ".into()
}
}
},
})
}
#[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 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 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");
File::open(path).map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})
}
#[inline]
#[instrument]
pub fn read_dir<T: AsRef<Path> + Debug>(path: T) -> Result<Vec<fs::DirEntry>, FsError> {
let path = path.as_ref();
let mut results = vec![];
if !path.exists() {
return Ok(results);
}
trace!(dir = ?path, "Reading directory");
let entries = fs::read_dir(path).map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})?;
for entry in entries {
match entry {
Ok(dir) => {
results.push(dir);
}
Err(error) => {
return Err(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<fs::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]
pub fn remove<T: AsRef<Path> + Debug>(path: T) -> Result<(), FsError> {
let path = path.as_ref();
if path.exists() {
if path.is_symlink() {
remove_link(path)?;
} else if path.is_file() {
remove_file(path)?;
} else if path.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()
{
trace!(file = ?path, "Removing symlink");
fs::remove_file(path).map_err(|error| 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() {
trace!(file = ?path, "Removing file");
fs::remove_file(path).map_err(|error| 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<u64, FsError> {
let path = path.as_ref();
if path.exists()
&& let Some((size, _)) = stale(path, true, duration, SystemTime::now())?
{
trace!(file = ?path, size, "Removing stale file");
fs::remove_file(path).map_err(|error| FsError::Remove {
path: path.to_path_buf(),
error: Box::new(error),
})?;
return Ok(size);
}
Ok(0)
}
#[inline]
#[instrument]
pub fn remove_dir_all<T: AsRef<Path> + Debug>(path: T) -> Result<(), FsError> {
let path = path.as_ref();
if path.exists() {
trace!(dir = ?path, "Removing directory");
fs::remove_dir_all(path).map_err(|error| 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();
if base_dir.exists() {
trace!(dir = ?base_dir, exceptions = ?exceptions, "Removing directory with exceptions");
fn traverse(
base_dir: &Path,
traverse_dir: &Path,
exclude: &[PathBuf],
) -> Result<(), FsError> {
for entry in read_dir(traverse_dir)? {
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 || ex.starts_with(rel_path));
if is_excluded {
if abs_path.is_dir() {
traverse(base_dir, &abs_path, exclude)?;
}
} else {
remove(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"
);
for entry in read_dir_all(dir)? {
if entry.file_type().is_ok_and(|file_type| file_type.is_file())
&& let Ok(bytes) = remove_file_if_stale(entry.path(), duration)
&& bytes > 0
{
files_deleted += 1;
bytes_saved += bytes;
}
}
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),
})
}
#[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),
})
}
#[cfg(feature = "editor-config")]
#[inline]
#[instrument(skip(data))]
pub fn write_file_with_config<D: AsRef<[u8]>>(
path: impl AsRef<Path> + Debug,
data: D,
) -> Result<(), FsError> {
let path = path.as_ref();
let editor_config = get_editor_config_props(path)?;
let mut data = unsafe { String::from_utf8_unchecked(data.as_ref().to_vec()) };
editor_config.apply_eof(&mut data);
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
trace!(file = ?path, "Writing file with .editorconfig");
fs::write(path, data).map_err(|error| FsError::Write {
path: path.to_path_buf(),
error: Box::new(error),
})
}