Skip to main content

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