bckt 0.7.1

bckt is an opinionated but flexible static site generator for blogs
use std::fs;
use std::path::Path;
use std::time::{Duration, UNIX_EPOCH};

use anyhow::{Context, Result};
use blake3::Hasher;
use walkdir::WalkDir;

use super::utils::normalize_path;

pub(super) fn compute_static_digest(root: &Path) -> Result<String> {
    let skel_dir = root.join("skel");
    if !skel_dir.exists() {
        return Ok(Hasher::new().finalize().to_hex().to_string());
    }

    let mut files = Vec::new();
    for entry in WalkDir::new(&skel_dir) {
        let entry = entry?;
        if entry.file_type().is_file() {
            files.push(entry.into_path());
        }
    }
    files.sort();

    let mut hasher = Hasher::new();
    for path in files {
        let relative = path.strip_prefix(&skel_dir).with_context(|| {
            format!(
                "path {} is not under {}",
                path.display(),
                skel_dir.display()
            )
        })?;
        let normalized = normalize_path(relative);
        hasher.update(normalized.as_bytes());
        let data = fs::read(&path)
            .with_context(|| format!("failed to read static asset {}", path.display()))?;
        hasher.update(&data);
        let metadata = fs::metadata(&path)
            .with_context(|| format!("failed to inspect static asset {}", path.display()))?;
        hasher.update(&metadata.len().to_le_bytes());
        let modified = metadata.modified().with_context(|| {
            format!(
                "failed to read modification time for static asset {}",
                path.display()
            )
        })?;
        let duration = modified
            .duration_since(UNIX_EPOCH)
            .unwrap_or_else(|_| Duration::new(0, 0));
        hasher.update(&duration.as_secs().to_le_bytes());
        hasher.update(&duration.subsec_nanos().to_le_bytes());
    }

    Ok(hasher.finalize().to_hex().to_string())
}

pub(super) fn copy_static_assets(root: &Path, html_root: &Path) -> Result<usize> {
    let skel_dir = root.join("skel");
    if !skel_dir.exists() {
        return Ok(0);
    }

    let mut copied = 0usize;
    for entry in WalkDir::new(&skel_dir) {
        let entry = entry?;
        if entry.file_type().is_dir() {
            continue;
        }
        let relative = entry.path().strip_prefix(&skel_dir).with_context(|| {
            format!(
                "path {} is not under {}",
                entry.path().display(),
                skel_dir.display()
            )
        })?;
        let destination = html_root.join(relative);
        if let Some(parent) = destination.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("failed to create {}", parent.display()))?;
        }
        fs::copy(entry.path(), &destination).with_context(|| {
            format!(
                "failed to copy static asset from {} to {}",
                entry.path().display(),
                destination.display()
            )
        })?;
        copied += 1;
    }

    Ok(copied)
}