use std::io::ErrorKind;
use camino::{Utf8Path, Utf8PathBuf};
#[cfg(unix)]
use cap_std::fs::{Permissions, PermissionsExt};
use cap_std::{
ambient_authority,
fs::{Dir, Metadata},
};
use color_eyre::eyre::{Context, Result};
use tracing::{error, info, info_span};
use crate::observability::LOG_TARGET;
pub fn ambient_dir_and_path(path: &Utf8Path) -> Result<(Dir, Utf8PathBuf)> {
if path.has_root() {
let (dir_path, relative) = match path.parent() {
Some(parent) => {
let relative = path
.strip_prefix(parent)
.with_context(|| {
format!("strip parent {} from {}", parent.as_str(), path.as_str())
})?
.to_path_buf();
(parent, relative)
}
None => (path, Utf8PathBuf::new()),
};
let dir = Dir::open_ambient_dir(dir_path.as_std_path(), ambient_authority())
.context("open ambient directory for absolute path")?;
Ok((dir, relative))
} else {
let dir = Dir::open_ambient_dir(".", ambient_authority())
.context("open ambient working directory")?;
Ok((dir, path.to_path_buf()))
}
}
pub(crate) fn ensure_dir_exists(path: &Utf8Path) -> Result<()> {
let span = info_span!(target: LOG_TARGET, "ensure_dir_exists", path = %path);
let _entered = span.enter();
let (dir, relative) = find_existing_ancestor(path)?;
ensure_dir_exists_inner(path, &dir, &relative)
}
fn find_existing_ancestor(path: &Utf8Path) -> Result<(Dir, Utf8PathBuf)> {
let mut last_err = None;
for ancestor in path.ancestors() {
let dir_fs_path = if ancestor.as_str().is_empty() {
std::path::Path::new(".")
} else {
ancestor.as_std_path()
};
match Dir::open_ambient_dir(dir_fs_path, ambient_authority()) {
Ok(dir) => {
let relative = path
.strip_prefix(ancestor)
.with_context(|| {
format!(
"strip ancestor {} from {}",
ancestor.as_str(),
path.as_str()
)
})?
.to_path_buf();
return Ok((dir, relative));
}
Err(err)
if err.kind() == ErrorKind::NotFound || err.kind() == ErrorKind::NotADirectory =>
{
last_err = Some(err);
}
Err(err) => {
return Err(err).context("open ambient directory for path");
}
}
}
Err(last_err.unwrap_or_else(|| {
std::io::Error::new(
ErrorKind::NotFound,
format!("no existing ancestor for {path}"),
)
}))
.context("find existing ancestor directory")
}
fn ensure_dir_exists_inner(path: &Utf8Path, dir: &Dir, relative: &Utf8PathBuf) -> Result<()> {
if relative.as_str().is_empty() {
return Ok(());
}
match dir.create_dir_all(relative.as_std_path()) {
Ok(()) => {
log_dir_created(path);
Ok(())
}
Err(err) => handle_dir_creation_error(path, err),
}
}
fn log_dir_created(path: &Utf8Path) {
info!(target: LOG_TARGET, path = %path, "ensured directory exists");
}
fn handle_dir_creation_error(path: &Utf8Path, err: std::io::Error) -> Result<()> {
if err.kind() == ErrorKind::AlreadyExists {
return ensure_existing_path_is_dir(path);
}
error!(
target: LOG_TARGET,
path = %path,
error = %err,
"failed to ensure directory exists"
);
Err(err).with_context(|| format!("create {}", path.as_str()))
}
pub(crate) fn set_permissions(path: &Utf8Path, mode: u32) -> Result<()> {
let span = info_span!(
target: LOG_TARGET,
"set_permissions",
path = %path,
mode_octal = format_args!("{mode:o}")
);
let _entered = span.enter();
let (dir, relative) = ambient_dir_and_path(path)?;
set_permissions_inner(path, mode, &dir, &relative)
}
fn set_permissions_inner(
path: &Utf8Path,
mode: u32,
dir: &Dir,
relative: &Utf8PathBuf,
) -> Result<()> {
if relative.as_str().is_empty() {
return Ok(());
}
set_permissions_for_platform(path, mode, dir, relative)
}
#[cfg(unix)]
fn set_permissions_for_platform(
path: &Utf8Path,
mode: u32,
dir: &Dir,
relative: &Utf8PathBuf,
) -> Result<()> {
match dir.set_permissions(relative.as_std_path(), Permissions::from_mode(mode)) {
Ok(()) => {
log_permissions_applied(path, mode);
Ok(())
}
Err(err) => handle_permission_error(path, mode, err),
}
}
#[cfg(not(unix))]
fn set_permissions_for_platform(
path: &Utf8Path,
mode: u32,
dir: &Dir,
relative: &Utf8PathBuf,
) -> Result<()> {
dir.metadata(relative.as_std_path())
.with_context(|| format!("stat {}", path.as_str()))?;
info!(
target: LOG_TARGET,
path = %path,
mode_octal = format_args!("{mode:o}"),
"skipped POSIX permission application on non-Unix target"
);
Ok(())
}
#[cfg(unix)]
fn log_permissions_applied(path: &Utf8Path, mode: u32) {
info!(
target: LOG_TARGET,
path = %path,
mode_octal = format_args!("{mode:o}"),
"applied permissions"
);
}
#[cfg(unix)]
fn handle_permission_error(path: &Utf8Path, mode: u32, err: std::io::Error) -> Result<()> {
error!(
target: LOG_TARGET,
path = %path,
mode_octal = format_args!("{mode:o}"),
error = %err,
"failed to apply permissions"
);
Err(err).with_context(|| format!("chmod {}", path.as_str()))
}
fn ensure_existing_path_is_dir(path: &Utf8Path) -> Result<()> {
let (dir, relative) = ambient_dir_and_path(path)?;
let metadata_result = if relative.as_str().is_empty() {
dir.dir_metadata()
} else {
dir.metadata(relative.as_std_path())
};
match metadata_result {
Ok(metadata) => handle_existing_metadata(path, &metadata),
Err(err) => Err(log_dir_metadata_error(path, err))
.with_context(|| format!("create {}", path.as_str())),
}
}
fn handle_existing_metadata(path: &Utf8Path, metadata: &Metadata) -> Result<()> {
if metadata.is_dir() {
info!(target: LOG_TARGET, path = %path, "directory already existed");
Ok(())
} else {
let err = std::io::Error::new(
ErrorKind::NotADirectory,
format!("{path} exists but is not a directory"),
);
Err(log_dir_metadata_error(path, err)).with_context(|| format!("create {}", path.as_str()))
}
}
fn log_dir_metadata_error(path: &Utf8Path, err: std::io::Error) -> std::io::Error {
error!(
target: LOG_TARGET,
path = %path,
error = %err,
"failed to ensure directory exists"
);
err
}
#[cfg(test)]
#[path = "fs_tests.rs"]
mod tests;