static_web_server/
compression_static.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! Compression static module to serve compressed files directly from the file system.
7//!
8
9use headers::{HeaderMap, HeaderValue};
10use hyper::{Body, Request, Response};
11use std::ffi::OsStr;
12use std::fs::Metadata;
13use std::path::{Path, PathBuf};
14
15use crate::compression;
16use crate::fs::meta::try_metadata;
17use crate::handler::RequestHandlerOpts;
18use crate::headers_ext::ContentCoding;
19use crate::Error;
20
21/// It defines the pre-compressed file variant metadata of a particular file path.
22pub struct CompressedFileVariant {
23    /// Current file path.
24    pub file_path: PathBuf,
25    /// The metadata of the current file.
26    pub metadata: Metadata,
27    /// The content encoding based on the file extension.
28    pub encoding: ContentCoding,
29}
30
31/// Initializes static compression.
32pub fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
33    handler_opts.compression_static = enabled;
34    tracing::info!("compression static: enabled={enabled}");
35}
36
37/// Post-processing to add Vary header if necessary.
38pub(crate) fn post_process<T>(
39    opts: &RequestHandlerOpts,
40    _req: &Request<T>,
41    mut resp: Response<Body>,
42) -> Result<Response<Body>, Error> {
43    if !opts.compression_static {
44        return Ok(resp);
45    }
46
47    // Compression content encoding varies so use a `Vary` header
48    let value = resp.headers().get(hyper::header::VARY).map_or(
49        HeaderValue::from_name(hyper::header::ACCEPT_ENCODING),
50        |h| {
51            let mut s = h.to_str().unwrap_or_default().to_owned();
52            s.push(',');
53            s.push_str(hyper::header::ACCEPT_ENCODING.as_str());
54            HeaderValue::from_str(s.as_str()).unwrap()
55        },
56    );
57    resp.headers_mut().insert(hyper::header::VARY, value);
58
59    Ok(resp)
60}
61
62/// Search for the pre-compressed variant of the given file path.
63pub fn precompressed_variant(
64    file_path: &Path,
65    headers: &HeaderMap<HeaderValue>,
66) -> Option<CompressedFileVariant> {
67    tracing::trace!(
68        "preparing pre-compressed file variant path of {}",
69        file_path.display()
70    );
71
72    for encoding in compression::get_encodings(headers) {
73        // Determine preferred-encoding extension if available
74        let comp_ext = match encoding {
75            // https://zlib.net/zlib_faq.html#faq39
76            #[cfg(any(
77                feature = "compression",
78                feature = "compression-gzip",
79                feature = "compression-deflate"
80            ))]
81            ContentCoding::GZIP | ContentCoding::DEFLATE => "gz",
82            // https://peazip.github.io/brotli-compressed-file-format.html
83            #[cfg(any(feature = "compression", feature = "compression-brotli"))]
84            ContentCoding::BROTLI => "br",
85            // https://datatracker.ietf.org/doc/html/rfc8878
86            #[cfg(any(feature = "compression", feature = "compression-zstd"))]
87            ContentCoding::ZSTD => "zst",
88            _ => {
89                tracing::trace!(
90                    "preferred encoding based on the file extension was not determined, skipping"
91                );
92                continue;
93            }
94        };
95
96        let comp_name = match file_path.file_name().and_then(OsStr::to_str) {
97            Some(v) => v,
98            None => {
99                tracing::trace!("file name was not determined for the current path, skipping");
100                continue;
101            }
102        };
103
104        let file_path = file_path.with_file_name([comp_name, ".", comp_ext].concat());
105        tracing::trace!(
106            "trying to get the pre-compressed file variant metadata for {}",
107            file_path.display()
108        );
109
110        let (metadata, is_dir) = match try_metadata(&file_path) {
111            Ok(v) => v,
112            Err(e) => {
113                tracing::trace!("pre-compressed file variant error: {:?}", e);
114                continue;
115            }
116        };
117
118        if is_dir {
119            tracing::trace!("pre-compressed file variant found but it's a directory, skipping");
120            continue;
121        }
122
123        tracing::trace!("pre-compressed file variant found, serving it directly");
124
125        return Some(CompressedFileVariant {
126            file_path,
127            metadata,
128            encoding,
129        });
130    }
131
132    None
133}