use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
use camino::{Utf8Path, Utf8PathBuf};
use indicatif::ProgressStyle;
use rayon::prelude::*;
use tracing::{Level, info, span};
use tracing_indicatif::span_ext::IndicatifSpanExt;
use crate::core::Hash32;
use crate::error::StepCopyStatic;
pub struct ProgressStyles {
pub build: ProgressStyle,
pub task: ProgressStyle,
pub task_items: ProgressStyle,
pub copy: ProgressStyle,
}
#[allow(clippy::expect_used)] impl Default for ProgressStyles {
fn default() -> Self {
Self {
build: ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}",
)
.expect("hardcoded template")
.progress_chars("=>-"),
task: ProgressStyle::default_spinner()
.template("{spinner:.blue} {msg}")
.expect("hardcoded template"),
task_items: ProgressStyle::default_spinner()
.template("{spinner:.blue} {msg} {pos}/{len} ")
.expect("hardcoded template"),
copy: ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed}] [{bar:40.cyan/blue}] {pos} {msg}")
.expect("hardcoded template")
.progress_chars("#>-"),
}
}
}
#[rustfmt::skip]
fn is_unchanged(src: &Path, dst: &Path) -> bool {
let Ok(src_meta) = fs::metadata(src) else { return false };
let Ok(dst_meta) = fs::metadata(dst) else { return false };
if src_meta.len() != dst_meta.len() {
return false;
}
let Ok(src_mod) = src_meta.modified() else { return false };
let Ok(dst_mod) = dst_meta.modified() else { return false };
if src_mod == dst_mod {
return true;
}
let Ok(src) = Hash32::hash_file(src) else { return false };
let Ok(dst) = Hash32::hash_file(dst) else { return false };
src == dst
}
#[derive(Clone, Debug)]
pub(crate) struct StaticFileEntry {
pub(crate) src: PathBuf,
pub(crate) dst: PathBuf,
pub(crate) source_utf8: Utf8PathBuf,
pub(crate) dist_rel: Utf8PathBuf,
}
pub(crate) fn collect_static(
copied: &[(String, String)],
out_dir: &Utf8Path,
) -> Result<Vec<StaticFileEntry>, StepCopyStatic> {
if copied.is_empty() {
return Ok(vec![]);
}
let mut files: Vec<StaticFileEntry> = Vec::new();
for (into, from) in copied {
let path = std::path::Path::new(into);
let mut depth = 0;
let mut safe = true;
for component in path.components() {
match component {
std::path::Component::ParentDir => {
depth -= 1;
if depth < 0 {
safe = false;
break;
}
}
std::path::Component::Normal(_) => {
depth += 1;
}
std::path::Component::RootDir | std::path::Component::Prefix(_) => {
safe = false;
break;
}
std::path::Component::CurDir => {}
}
}
if !safe {
return Err(StepCopyStatic::UnsafeTarget(into.clone()));
}
let target = out_dir.as_std_path().join(into);
let dist_rel = Utf8Path::new(into);
let metadata = fs::metadata(from).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StepCopyStatic::MissingSource(from.clone())
} else {
StepCopyStatic::Io(e)
}
})?;
if metadata.is_dir() {
collect_files(from, &target, dist_rel, &mut files)?;
} else {
let source_utf8 = Utf8PathBuf::try_from(PathBuf::from(from))
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
files.push(StaticFileEntry {
src: PathBuf::from(from),
dst: target,
source_utf8,
dist_rel: dist_rel.to_path_buf(),
});
}
}
Ok(files)
}
pub(crate) fn copy_static_entries(
files: &[StaticFileEntry],
style: &ProgressStyle,
) -> Result<Vec<(Utf8PathBuf, Utf8PathBuf)>, StepCopyStatic> {
if files.is_empty() {
return Ok(vec![]);
}
let span = span!(Level::INFO, "copy_static", indicatif.pb_show = true);
span.pb_set_message("Copying static files...");
span.pb_set_style(style);
let _enter = span.enter();
let s = Instant::now();
span.pb_set_length(files.len() as u64);
for dir in files
.iter()
.filter_map(|f| f.dst.parent())
.collect::<HashSet<&Path>>()
{
fs::create_dir_all(dir)?;
}
let entries: Vec<(Utf8PathBuf, Utf8PathBuf)> = files
.par_iter()
.map(|f| -> std::io::Result<(Utf8PathBuf, Utf8PathBuf)> {
if !is_unchanged(&f.src, &f.dst) {
fs::copy(&f.src, &f.dst)?;
}
span.pb_inc(1);
Ok((f.source_utf8.clone(), f.dist_rel.clone()))
})
.collect::<std::io::Result<_>>()?;
info!(duration_ms = s.elapsed().as_millis() as u64, "Finished copying static files");
Ok(entries)
}
fn collect_files(
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
dist_rel: &Utf8Path,
files: &mut Vec<StaticFileEntry>,
) -> std::io::Result<()> {
for entry in fs::read_dir(src)? {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_str().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "non-UTF-8 filename")
})?;
if entry.file_type()?.is_dir() {
collect_files(
entry.path(),
dst.as_ref().join(&name),
&dist_rel.join(name_str),
files,
)?;
} else {
let src_path = entry.path();
let source_utf8 = Utf8PathBuf::try_from(src_path.clone())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
files.push(StaticFileEntry {
dst: dst.as_ref().join(&name),
src: src_path,
source_utf8,
dist_rel: dist_rel.join(name_str),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clone_static_errors_for_missing_source() {
let result = collect_static(
&[("assets".to_string(), "missing-static-source".to_string())],
Utf8Path::new("dist"),
);
assert!(matches!(
result,
Err(StepCopyStatic::MissingSource(source)) if source == "missing-static-source"
));
}
}