pub mod html_rewrite;
use anyhow::{anyhow, bail, Context, Result};
use base64::{engine::general_purpose, Engine};
use console::Emoji;
use once_cell::sync::Lazy;
use rand::TryRngCore;
use std::{
collections::HashSet,
ffi::OsStr,
fmt::Debug,
fs::Metadata,
io::ErrorKind,
path::{Component, Path, PathBuf},
process::Stdio,
};
use tokio::{fs, process::Command};
pub static BUILDING: Emoji = Emoji("📦 ", "");
pub static SUCCESS: Emoji = Emoji("✅ ", "");
pub static ERROR: Emoji = Emoji("❌ ", "");
pub static SERVER: Emoji = Emoji("📡 ", "");
pub static LOCAL: Emoji = Emoji("🏠 ", "");
pub static NETWORK: Emoji = Emoji("💻 ", "");
pub static STARTING: Emoji = Emoji("🚀 ", "");
#[cfg(feature = "update_check")]
pub static UPDATE: Emoji = Emoji("⏫ ", "");
#[allow(clippy::expect_used)]
static CWD: Lazy<PathBuf> =
Lazy::new(|| std::env::current_dir().expect("error getting current dir"));
pub async fn copy_dir_recursive<F, T>(from_dir: F, to_dir: T) -> Result<HashSet<PathBuf>>
where
F: AsRef<Path> + Debug + Send + 'static,
T: AsRef<Path> + Send + 'static,
{
let from = from_dir.as_ref();
let to: &Path = to_dir.as_ref();
let from_metadata = tokio::fs::metadata(from).await.with_context(|| {
format!("Unable to retrieve metadata of '{from:?}'. Path does probably not exist.")
})?;
if !from_metadata.is_dir() {
return Err(anyhow!(
"Path '{from:?}' can not be copied as it is not a directory!"
));
}
if tokio::fs::metadata(to).await.is_err() {
tokio::fs::create_dir_all(to)
.await
.with_context(|| format!("Unable to create target directory '{to:?}'."))?;
}
let mut collector = HashSet::new();
let mut read_dir = tokio::fs::read_dir(from)
.await
.context(anyhow!("Unable to read dir"))?;
while let Some(entry) = read_dir
.next_entry()
.await
.context(anyhow!("Unable to read next dir entry"))?
{
if entry.file_type().await?.is_dir() {
let files = Box::pin(async move {
copy_dir_recursive(entry.path(), to.join(entry.file_name())).await
})
.await?;
collector.extend(files);
} else {
let to = to.join(entry.file_name());
tokio::fs::copy(entry.path(), &to).await?;
collector.insert(to);
}
}
Ok(collector)
}
pub async fn remove_dir_all(from_dir: PathBuf) -> Result<()> {
if !path_exists(&from_dir).await? {
return Ok(());
}
tokio::task::spawn_blocking(move || {
::remove_dir_all::remove_dir_all(from_dir).context("error removing directory")?;
Ok(())
})
.await
.context("error awaiting spawned remove dir call")?
}
pub async fn path_exists(path: impl AsRef<Path>) -> Result<bool> {
path_exists_and(path, |_| true).await
}
pub async fn path_exists_and(
path: impl AsRef<Path>,
and: impl FnOnce(Metadata) -> bool,
) -> Result<bool> {
tokio::fs::metadata(path.as_ref())
.await
.map(and)
.or_else(|error| {
if error.kind() == ErrorKind::NotFound {
Ok(false)
} else {
Err(error)
}
})
.with_context(|| {
format!(
"error checking for existence of path at {:?}",
path.as_ref()
)
})
}
pub async fn is_executable(path: impl AsRef<Path>) -> Result<bool> {
#[cfg(unix)]
let has_executable_flag = |meta: Metadata| {
use std::os::unix::fs::PermissionsExt;
meta.permissions().mode() & 0o100 != 0
};
#[cfg(not(unix))]
let has_executable_flag = |_meta: Metadata| true;
fs::metadata(path.as_ref())
.await
.map(|meta| meta.is_file() && has_executable_flag(meta))
.or_else(|error| {
if error.kind() == ErrorKind::NotFound {
Ok(false)
} else {
Err(error)
}
})
.with_context(|| format!("error checking file mode for file {:?}", path.as_ref()))
}
pub fn strip_prefix(target: &Path) -> &Path {
target.strip_prefix(CWD.as_path()).unwrap_or(target)
}
#[tracing::instrument(level = "trace", skip(name, args))]
pub async fn run_command(
name: &str,
path: impl AsRef<Path> + Debug,
args: &[impl AsRef<OsStr> + Debug],
working_dir: impl AsRef<Path> + Debug,
) -> Result<()> {
tracing::debug!(?args, "{name} args");
let path = path.as_ref();
let status = Command::new(path)
.current_dir(working_dir.as_ref())
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| {
format!(
"error running {name} using executable '{}' with args: '{args:?}'",
path.display(),
)
})?
.wait()
.await
.with_context(|| format!("error during {name} call"))?;
if !status.success() {
bail!(
"{name} call to executable '{}' with args: '{args:?}' returned a bad status: {status}",
path.display()
);
}
Ok(())
}
pub fn check_target_not_found_err(err: anyhow::Error, target: &str) -> anyhow::Error {
let io_err: &std::io::Error = match err.downcast_ref() {
Some(io_err) => io_err,
None => return err,
};
match io_err.kind() {
std::io::ErrorKind::NotFound => err.context(format!("'{}' not found", target)),
_ => err,
}
}
pub async fn target_path(
base: &Path,
target_path: Option<&Path>,
default: Option<&OsStr>,
) -> Result<PathBuf> {
if let Some(path) = target_path {
if path.is_absolute() || path.components().any(|c| matches!(c, Component::ParentDir)) {
bail!(
"Invalid data-target-path '{}'. Must be a relative path without '..'.",
path.display()
);
}
let dir_out = base.join(path);
tokio::fs::create_dir_all(&dir_out).await?;
Ok(dir_out)
} else if let Some(default) = default {
Ok(base.join(default))
} else {
Ok(base.to_owned())
}
}
pub fn dist_relative(dist: &Path, target_file: &Path) -> Result<String> {
let target_file = target_file.strip_prefix(dist).with_context(|| {
format!(
"unable to create a relative path of '{}' in '{}'",
target_file.display(),
dist.display()
)
})?;
Ok(path_to_href(target_file))
}
pub fn apply_data_target_path(path: impl Into<String>, target_path: &Option<PathBuf>) -> String {
match target_path {
Some(target_path) => path_to_href(target_path.join(path.into())),
None => path.into(),
}
}
pub fn path_to_href(path: impl AsRef<Path>) -> String {
let path = path
.as_ref()
.iter()
.map(|c| c.to_string_lossy())
.collect::<Vec<_>>();
path.join("/")
}
pub fn nonce() -> anyhow::Result<String> {
let mut buffer = [0u8; 16];
rand::rngs::OsRng.try_fill_bytes(&mut buffer)?;
Ok(general_purpose::STANDARD.encode(buffer))
}
pub fn nonce_attr(attr: &Option<String>) -> String {
match attr {
Some(v) => format!(r#" nonce="{v}""#),
None => "".to_string(),
}
}