use std::{
ffi::OsStr,
fmt::{self, Display},
fs,
ops::Deref,
path::{Path, PathBuf, StripPrefixError},
};
use miette::{ensure, Context, IntoDiagnostic};
#[derive(Debug)]
pub(crate) struct FileRoot {
nickname: &'static str,
path: PathBuf,
}
impl FileRoot {
pub(crate) fn new
(nickname: &'static str, path: P) -> miette::Result
where
P: AsRef,
{
let path = path.as_ref();
Ok(Self {
nickname,
path: dunce::canonicalize(path)
.map_err(miette::Report::msg)
.wrap_err_with(|| format!("failed to canonicalize {path:?}"))?,
})
}
pub(crate) fn nickname(&self) -> &str {
self.nickname
}
pub(crate) fn try_child(&self, path: P) -> Result, StripPrefixError>
where
P: AsRef,
{
let path = path.as_ref();
if path.is_absolute() {
path.strip_prefix(&self.path)?;
}
Ok(Child {
root: self,
path: self.path.join(path),
})
}
#[track_caller]
pub(crate) fn child(&self, path: P) -> Child<'_>
where
P: AsRef,
{
self.try_child(path)
.into_diagnostic()
.wrap_err("invariant violation: `path` is absolute and not a child of this file root")
.unwrap()
}
fn removed_dir(&self, path: P) -> miette::Result>
where
P: AsRef,
{
let path = path.as_ref();
let child = self.child(path);
if child.exists() {
log::info!("removing old contents of {child}…",);
log::trace!("removing directory {:?}", &*child);
fs::remove_dir_all(&*child)
.map_err(miette::Report::msg)
.wrap_err_with(|| format!("failed to remove old contents of {child}"))?;
}
Ok(child)
}
fn removed_file(&self, path: P) -> miette::Result>
where
P: AsRef,
{
let path = path.as_ref();
let child = self.child(path);
if child.exists() {
log::info!("removing old copy of {child}…",);
fs::remove_file(&*child)
.map_err(miette::Report::msg)
.wrap_err_with(|| format!("failed to remove old copy of {child}"))?;
}
Ok(child)
}
pub(crate) fn regen_dir(
&self,
path: P,
gen: impl FnOnce(&Child<'_>) -> miette::Result<()>,
) -> miette::Result>
where
P: AsRef,
{
let child = self.removed_dir(path)?;
gen(&child)?;
ensure!(
child.is_dir(),
"{} was not regenerated for an unknown reason",
child,
);
Ok(child)
}
pub(crate) fn regen_file(
&self,
path: P,
gen: impl FnOnce(&Child<'_>) -> miette::Result<()>,
) -> miette::Result>
where
P: AsRef,
{
let child = self.removed_file(path)?;
gen(&child)?;
ensure!(
child.is_file(),
"{} was not regenerated for an unknown reason",
child,
);
Ok(child)
}
}
impl Deref for FileRoot {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.path
}
}
impl AsRef for FileRoot {
fn as_ref(&self) -> &Path {
&self.path
}
}
impl AsRef for FileRoot {
fn as_ref(&self) -> &OsStr {
self.path.as_os_str()
}
}
impl Display for FileRoot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { nickname, path } = self;
write!(f, "`{}` (AKA `<{nickname}>`)", path.display())
}
}
pub(crate) struct Child<'a> {
root: &'a FileRoot,
/// NOTE: This is always an absolute path that is a child of the `root`.
path: PathBuf,
}
impl Child<'_> {
pub(crate) fn relative_path(&self) -> &Path {
let Self { root, path } = self;
path.strip_prefix(root).unwrap()
}
pub(crate) fn try_child(&self, path: P) -> Result
where
P: AsRef,
{
let child_path = path.as_ref();
let Self { root, path } = self;
if child_path.is_absolute() {
child_path.strip_prefix(path)?;
}
Ok(Child {
root,
path: path.join(child_path),
})
}
#[track_caller]
pub(crate) fn child(&self, path: P) -> Child<'_>
where
P: AsRef,
{
self.try_child(path)
.into_diagnostic()
.wrap_err("invariant violation: `path` is absolute and not a child of this child")
.unwrap()
}
}
impl Deref for Child<'_> {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.path
}
}
impl AsRef for Child<'_> {
fn as_ref(&self) -> &Path {
&self.path
}
}
impl AsRef for Child<'_> {
fn as_ref(&self) -> &OsStr {
self.path.as_os_str()
}
}
impl Display for Child<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"`<{}>{}{}`",
self.root.nickname(),
std::path::MAIN_SEPARATOR,
self.relative_path().display()
)
}
}
pub(crate) fn existing_file(path: P) -> P
where
P: AsRef,
{
let p = path.as_ref();
assert!(p.is_file(), "{p:?} does not exist as a file");
path
}
pub(crate) fn copy_dir(source: P, dest: Q) -> miette::Result<()>
where
P: Display + AsRef,
Q: Display + AsRef,
{
log::debug!(
"copy-merging directories from {} into {}",
source.as_ref().display(),
dest.as_ref().display(),
);
::dircpy::copy_dir(&source, &dest)
.into_diagnostic()
.wrap_err_with(|| format!("failed to copy files from {source} to {dest}"))
}
pub(crate) fn read_to_string(path: P) -> miette::Result
where
P: AsRef,
{
fs::read_to_string(&path)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"failed to read UTF-8 string from path {}",
path.as_ref().display()
)
})
}
pub(crate) fn copy(from: P1, to: P2) -> miette::Result
where
P1: AsRef,
P2: AsRef,
{
fs::copy(&from, &to).into_diagnostic().wrap_err_with(|| {
format!(
"failed to copy {} to {}",
from.as_ref().display(),
to.as_ref().display()
)
})
}
pub(crate) fn create_dir_all(path: P) -> miette::Result<()>
where
P: AsRef,
{
fs::create_dir_all(&path)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"failed to create directories leading up to {}",
path.as_ref().display()
)
})
}
pub(crate) fn remove_file(path: P) -> miette::Result<()>
where
P: AsRef,
{
fs::remove_file(&path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to remove file at path {}", path.as_ref().display()))
}
pub(crate) fn write(path: P, contents: C) -> miette::Result<()>
where
P: AsRef,
C: AsRef<[u8]>,
{
fs::write(&path, &contents)
.into_diagnostic()
.wrap_err_with(|| format!("failed to write to path {}", path.as_ref().display()))
}