haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::Site;
use crate::door::http::{Request, Response};
use crate::door::packed::{IMMUTABLE, hashed, takes_gzip};
use serde_json::json;
use std::path::{Component, Path};

/// One of Studio's files, or its index for an address with no extension.
/// A text file is gzipped for a browser that takes gzip, and says it varies
/// by Accept-Encoding; a content-hashed asset is kept by the browser for
/// good, and every other answer is `no-store`.
pub(super) fn file(site: &Site, request: &Request) -> Response {
    let (files, path) = (&site.files, request.path.as_str());
    let relative = Path::new(path.trim_start_matches('/'));
    let plain = relative
        .components()
        .all(|component| matches!(component, Component::Normal(_)))
        && !path.contains(['%', '\\']);
    let missing = || {
        Response::json(
            404,
            &json!({"code": "file_unknown", "message": "Studio has no such file"}),
        )
    };
    if !plain {
        return missing();
    }
    let wanted = files.join(relative);
    let (target, kind) = wanted
        .extension()
        .and_then(|extension| extension.to_str())
        .map_or_else(
            || (files.join("index.html"), "html".to_owned()),
            |extension| (wanted.clone(), extension.to_owned()),
        );
    let content_type = match kind.as_str() {
        "html" => "text/html; charset=utf-8",
        "js" | "mjs" => "text/javascript; charset=utf-8",
        "css" => "text/css; charset=utf-8",
        "json" | "map" => "application/json",
        "svg" => "image/svg+xml",
        "png" => "image/png",
        "woff2" => "font/woff2",
        "wasm" => "application/wasm",
        _ => "application/octet-stream",
    };
    // Images and fonts are compressed already; text and wasm are not.
    let compressible = matches!(
        kind.as_str(),
        "html" | "js" | "mjs" | "css" | "json" | "map" | "svg" | "wasm"
    );
    let gzip = compressible && takes_gzip(request.header("accept-encoding"));
    let immutable = target == wanted && hashed(path);
    let packed = &site.packed;
    let body = if gzip {
        packed
            .gzipped(&target, immutable)
            .map(|bytes| bytes.to_vec())
    } else {
        std::fs::read(&target)
    };
    let Ok(body) = body else {
        return missing();
    };
    let mut response = Response::new(200, content_type, body);
    if compressible {
        response = response.with("Vary", "Accept-Encoding");
    }
    if gzip {
        response = response.with("Content-Encoding", "gzip");
    }
    if immutable {
        response = response.cached(IMMUTABLE);
    }
    response
}