mini-static 0.15.0

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use crate::bundle;
use crate::minify;

/// Bundle all CSS files from a single source directory into a single output file.
///
/// Convenience wrapper around [`bundle_css_sources`] for callers with one source directory.
///
/// # Errors
///
/// See [`bundle_css_sources`].
pub async fn bundle_directory_css(
    src_dir: &Path,
    output_path: &Path,
) -> Result<(), CssBundlerError> {
    let src_dir_canon = src_dir
        .canonicalize()
        .map_err(|e| CssBundlerError::ReadSource {
            path: src_dir.to_path_buf(),
            reason: e.to_string(),
        })?;
    bundle_css_sources(
        std::slice::from_ref(&src_dir_canon),
        std::slice::from_ref(&src_dir_canon),
        output_path,
    )
    .await
}

/// Bundle all CSS files across `source_dirs` into a single output file.
///
/// Discovers every `.css` file under `source_dirs` (recursively), resolves `@import`
/// statements within `allowed_roots`, concatenates the per-file bundles in sorted path
/// order, minifies the result, and writes it to `output_path`. `allowed_roots` should
/// include `source_dirs` plus any files-only import roots (see [`Server::with_bundle_root`]).
///
/// # Errors
///
/// Returns `Err` if:
/// - any of `source_dirs` cannot be read
/// - no `.css` files exist under `source_dirs`
/// - `output_path` cannot be written to
/// - CSS parsing or bundling fails
pub async fn bundle_css_sources(
    allowed_roots: &[PathBuf],
    source_dirs: &[PathBuf],
    output_path: &Path,
) -> Result<(), CssBundlerError> {
    let mut css_files = Vec::new();

    for src_dir in source_dirs {
        let files = find_css_files(src_dir).map_err(|e| CssBundlerError::ReadSource {
            path: src_dir.to_path_buf(),
            reason: e.to_string(),
        })?;
        css_files.extend(files);
    }

    if css_files.is_empty() {
        let first = source_dirs
            .first()
            .cloned()
            .unwrap_or_else(|| PathBuf::from("."));
        return Err(CssBundlerError::NoFilesFound(first));
    }

    let mut bundled_content = String::new();

    for css_file in &css_files {
        let (bytes, _deps) = bundle::bundle_and_minify_css(allowed_roots, css_file)
            .await
            .map_err(|e| CssBundlerError::Bundle(format!("{:?}", e)))?;

        bundled_content.push_str(&String::from_utf8_lossy(&bytes));
    }

    let bundled_bytes_final =
        minify::minify(bundled_content.as_bytes(), crate::reload::ChangeType::Css)
            .map_err(|e| CssBundlerError::Bundle(format!("Minification failed: {:?}", e)))?;

    if let Some(parent) = output_path.parent() {
        fs::create_dir_all(parent).map_err(|e| CssBundlerError::WriteOutput {
            path: output_path.to_path_buf(),
            reason: e.to_string(),
        })?;
    }

    fs::write(output_path, &bundled_bytes_final).map_err(|e| CssBundlerError::WriteOutput {
        path: output_path.to_path_buf(),
        reason: e.to_string(),
    })?;

    Ok(())
}

/// Find all `.css` files in a directory tree.
fn find_css_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
    let mut css_files = Vec::new();
    let mut dirs = vec![dir.to_path_buf()];

    while let Some(current_dir) = dirs.pop() {
        let entries = fs::read_dir(&current_dir)?;
        for entry in entries {
            let entry = entry?;
            let path = entry.path();
            let file_type = entry.file_type()?;

            if file_type.is_dir() {
                dirs.push(path);
            } else if file_type.is_file() && path.extension().and_then(|s| s.to_str()) == Some("css")
            {
                css_files.push(path);
            }
        }
    }

    css_files.sort();
    Ok(css_files)
}

/// Errors that can occur during CSS bundling.
#[derive(Debug)]
pub enum CssBundlerError {
    /// Failed to read the source directory.
    ReadSource { path: PathBuf, reason: String },
    /// Failed to write the output file.
    WriteOutput { path: PathBuf, reason: String },
    /// No CSS files found in the source directory.
    NoFilesFound(PathBuf),
    /// CSS bundling/minification failed.
    Bundle(String),
}

impl std::fmt::Display for CssBundlerError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CssBundlerError::ReadSource { path, reason } => {
                write!(
                    f,
                    "failed to read source dir {}: {}",
                    path.display(),
                    reason
                )
            }
            CssBundlerError::WriteOutput { path, reason } => {
                write!(
                    f,
                    "failed to write output file {}: {}",
                    path.display(),
                    reason
                )
            }
            CssBundlerError::NoFilesFound(path) => {
                write!(f, "no CSS files found in {}", path.display())
            }
            CssBundlerError::Bundle(msg) => {
                write!(f, "CSS bundling failed: {}", msg)
            }
        }
    }
}

impl std::error::Error for CssBundlerError {}