static-web-server 3.0.0-beta.2

A cross-platform, high-performance and asynchronous web server for static files-serving.
// SPDX-License-Identifier: MIT OR Apache-2.0
// This file is part of Static Web Server.
// See https://static-web-server.net/ for more information
// Copyright (C) 2019-present Jose Quintana <joseluisq.net>

//! Compression static module to serve compressed files directly from the file system.
//!

use headers::{HeaderMap, HeaderMapExt, HeaderValue};
use hyper::{Request, Response};
use std::ffi::OsStr;
use std::fs::Metadata;
use std::path::{Path, PathBuf};

use crate::Error;
use crate::body::Body;
use crate::exts::headers::{AcceptEncoding, ContentCoding};
use crate::exts::http::append_vary_accept_encoding;
use crate::fs::meta::try_metadata;
use crate::handler::RequestHandlerOpts;

/// It defines the pre-compressed file variant metadata of a particular file path.
pub struct CompressedFileVariant {
    /// Current file path.
    pub file_path: PathBuf,
    /// The metadata of the current file.
    pub metadata: Metadata,
    /// The content encoding based on the file extension.
    pub encoding: ContentCoding,
}

/// Initializes static compression.
pub fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
    handler_opts.compression_static = enabled;
    tracing::info!(enabled, "compression static");
}

/// Post-processing to add Vary header if necessary.
pub(crate) fn post_process<T>(
    opts: &RequestHandlerOpts,
    _req: &Request<T>,
    mut resp: Response<Body>,
) -> Result<Response<Body>, Error> {
    if !opts.compression_static {
        return Ok(resp);
    }

    // Compression content encoding varies so use a `Vary` header
    append_vary_accept_encoding(&mut resp);

    Ok(resp)
}

/// Search for the pre-compressed variant of the given file path.
pub fn precompressed_variant(
    file_path: &Path,
    headers: &HeaderMap<HeaderValue>,
) -> Option<CompressedFileVariant> {
    tracing::trace!(
        "preparing pre-compressed file variant path of {}",
        file_path.display()
    );
    let accept_encoding = headers.typed_get::<AcceptEncoding>()?;
    accept_encoding.sorted_encodings().find_map(|encoding| {
        let (file_path, metadata) = variant_metadata(file_path, encoding)?;
        tracing::trace!("pre-compressed file variant found, serving it directly");
        Some(CompressedFileVariant {
            file_path,
            metadata,
            encoding,
        })
    })
}

/// Returns the content codings of the pre-compressed variants of the given
/// file path that exist on disk.
///
/// The in-memory cache records them in each entry of the file, so that a cache
/// hit makes the same choice as [`precompressed_variant`] without touching the disk.
#[cfg(feature = "mem-cache")]
pub(crate) fn precompressed_variants(file_path: &Path) -> Box<[ContentCoding]> {
    [
        ContentCoding::ZSTD,
        ContentCoding::BROTLI,
        ContentCoding::GZIP,
    ]
    .into_iter()
    .filter(|&encoding| variant_metadata(file_path, encoding).is_some())
    .collect()
}

/// Returns the path and metadata of the pre-compressed variant of the given
/// file path in the given content coding, if that variant is a file on disk.
fn variant_metadata(file_path: &Path, encoding: ContentCoding) -> Option<(PathBuf, Metadata)> {
    // Determine preferred-encoding extension if available
    let comp_ext = match encoding {
        // `deflate` has no variant: a `.gz` file is not a valid `deflate` body.
        // https://zlib.net/zlib_faq.html#faq39
        ContentCoding::GZIP => "gz",
        // https://peazip.github.io/brotli-compressed-file-format.html
        ContentCoding::BROTLI => "br",
        // https://datatracker.ietf.org/doc/html/rfc8878
        ContentCoding::ZSTD => "zst",
        _ => {
            tracing::trace!(
                "preferred encoding based on the file extension was not determined, skipping"
            );
            return None;
        }
    };

    let Some(comp_name) = file_path.file_name().and_then(OsStr::to_str) else {
        tracing::trace!("file name was not determined for the current path, skipping");
        return None;
    };

    let file_path = file_path.with_file_name([comp_name, ".", comp_ext].concat());
    tracing::trace!(
        "trying to get the pre-compressed file variant metadata for {}",
        file_path.display()
    );

    let (metadata, is_dir) = match try_metadata(&file_path) {
        Ok(v) => v,
        Err(e) => {
            tracing::trace!("pre-compressed file variant error: {:?}", e);
            return None;
        }
    };

    if is_dir {
        tracing::trace!("pre-compressed file variant found but it's a directory, skipping");
        return None;
    }

    Some((file_path, metadata))
}