use core::iter;
use std::path::{Path, PathBuf};
use std::{fs, io};
use thiserror::Error;
use crate::model::{AbsolutePath, AbsolutePathBuf};
#[derive(Error, Debug)]
pub enum FsError {
#[error("'{}' is not a directory", .0.display())]
NotDirectory(PathBuf),
#[error("'{}' is a broken symlink", .0.display())]
BrokenSymlink(PathBuf),
#[error("unsupported file type: '{}'", .0.display())]
UnsupportedFileType(PathBuf),
#[error("'{}': {}", .0.display(), .1)]
Io(PathBuf, #[source] io::Error),
}
fn fs_io_error(path: &Path, error: io::Error) -> FsError {
FsError::Io(path.to_owned(), error)
}
pub fn symlink_metadata_if_exists(path: &Path) -> Result<Option<fs::Metadata>, FsError> {
match fs::symlink_metadata(path) {
Ok(metadata) => Ok(Some(metadata)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(fs_io_error(path, error)),
}
}
pub fn create_parent_dir(path: &AbsolutePath) -> Result<Option<&AbsolutePath>, FsError> {
if let Some(parent) = path.parent()
&& !parent.is_dir()
{
fs::create_dir_all(parent).map_err(|error| fs_io_error(parent, error))?;
return Ok(Some(parent));
}
Ok(None)
}
pub fn resolve_partially_existing_path(path: &AbsolutePath) -> Result<AbsolutePathBuf, FsError> {
let (existing_path, metadata) = find_existing_ancestor(path)?;
let metadata = follow_symlink(existing_path, metadata)?;
if !metadata.is_dir() {
return Err(FsError::NotDirectory(existing_path.to_path_buf()));
}
let resolved = AbsolutePathBuf::canonicalize(existing_path)
.map_err(|error| fs_io_error(existing_path, error))?;
Ok(resolved.join(
path.strip_prefix(existing_path)
.expect("existing is an ancestor"),
))
}
pub fn create_symlink(
source: &Path,
destination: &Path,
file_type: fs::FileType,
) -> Result<(), FsError> {
#[cfg(unix)]
let result = {
use std::os::unix::fs::symlink;
let _ = file_type;
symlink(source, destination)
};
#[cfg(windows)]
let result = {
use std::os::windows::fs::{FileTypeExt, symlink_dir, symlink_file};
if file_type.is_dir() || file_type.is_symlink_dir() {
symlink_dir(source, destination)
} else {
symlink_file(source, destination)
}
};
result.map_err(|error| fs_io_error(destination, error))
}
pub fn remove_path(path: &Path, file_type: fs::FileType) -> Result<(), FsError> {
#[cfg(windows)]
{
use std::os::windows::fs::FileTypeExt;
if file_type.is_symlink_dir() {
return fs::remove_dir(path).map_err(|error| fs_io_error(path, error));
}
}
let result = if file_type.is_dir() {
fs::remove_dir_all(path)
} else {
fs::remove_file(path)
};
result.map_err(|error| fs_io_error(path, error))
}
pub fn iter_tree(
root: &AbsolutePath,
) -> impl Iterator<Item = Result<(PathBuf, fs::Metadata), FsError>> + use<> {
let root = root.to_owned();
let mut pending = vec![PathBuf::new()];
iter::from_fn(move || {
pending.pop().map(|relative_path| {
let path = root.join(&relative_path);
let metadata = path
.symlink_metadata()
.map_err(|error| fs_io_error(&path, error))?;
if metadata.is_dir() {
for entry in fs::read_dir(&path).map_err(|error| fs_io_error(&path, error))? {
let entry = entry.map_err(|error| fs_io_error(&path, error))?;
pending.push(relative_path.join(entry.file_name()));
}
}
Ok((relative_path, metadata))
})
})
}
pub fn copy_overlay(source: &AbsolutePath, destination: &AbsolutePath) -> Result<(), FsError> {
let mut directories = Vec::new();
for entry in iter_tree(source) {
let (relative_entry_path, entry_metadata) = entry?;
let entry_path = source.join(&relative_entry_path);
let entry_destination = destination.join(&relative_entry_path);
let entry_type = entry_metadata.file_type();
if !entry_type.is_symlink() && !entry_type.is_dir() && !entry_type.is_file() {
return Err(FsError::UnsupportedFileType(entry_path.into()));
}
let destination_metadata = symlink_metadata_if_exists(&entry_destination)?;
let reuse_destination = destination_metadata
.as_ref()
.is_some_and(|meta| !entry_type.is_symlink() && meta.file_type() == entry_type);
if !reuse_destination && let Some(destination_metadata) = destination_metadata {
remove_path(&entry_destination, destination_metadata.file_type())?;
}
if entry_type.is_symlink() {
let entry_link_target =
fs::read_link(&entry_path).map_err(|error| fs_io_error(&entry_path, error))?;
create_symlink(&entry_link_target, &entry_destination, entry_type)?;
} else if entry_type.is_dir() {
if !reuse_destination {
fs::create_dir(&entry_destination)
.map_err(|error| fs_io_error(&entry_destination, error))?;
}
directories.push((entry_destination, entry_metadata.permissions()));
} else {
fs::copy(&entry_path, &entry_destination)
.map_err(|error| fs_io_error(&entry_destination, error))?;
}
}
for (path, permissions) in directories.into_iter().rev() {
fs::set_permissions(&path, permissions).map_err(|error| fs_io_error(&path, error))?;
}
Ok(())
}
fn find_existing_ancestor(path: &AbsolutePath) -> Result<(&AbsolutePath, fs::Metadata), FsError> {
let mut existing = path;
loop {
match existing.symlink_metadata() {
Ok(metadata) => return Ok((existing, metadata)),
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
existing = existing
.parent()
.ok_or_else(|| fs_io_error(existing, error))?;
}
Err(error) => return Err(fs_io_error(existing, error)),
}
}
}
fn follow_symlink(path: &Path, metadata: fs::Metadata) -> Result<fs::Metadata, FsError> {
if !metadata.is_symlink() {
return Ok(metadata);
}
path.metadata().map_err(|error| match error.kind() {
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory => {
FsError::BrokenSymlink(path.to_owned())
}
_ => fs_io_error(path, error),
})
}