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    server_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    resp.headers_mut().insert(
49        hyper::header::VARY,
50        HeaderValue::from_name(hyper::header::ACCEPT_ENCODING),
51    );
52
53    Ok(resp)
54}
55
56/// Search for the pre-compressed variant of the given file path.
57pub fn precompressed_variant(
58    file_path: &Path,
59    headers: &HeaderMap<HeaderValue>,
60) -> Option<CompressedFileVariant> {
61    tracing::trace!(
62        "preparing pre-compressed file variant path of {}",
63        file_path.display()
64    );
65
66    for encoding in compression::get_encodings(headers) {
67        // Determine preferred-encoding extension if available
68        let comp_ext = match encoding {
69            // https://zlib.net/zlib_faq.html#faq39
70            #[cfg(any(
71                feature = "compression",
72                feature = "compression-gzip",
73                feature = "compression-deflate"
74            ))]
75            ContentCoding::GZIP | ContentCoding::DEFLATE => "gz",
76            // https://peazip.github.io/brotli-compressed-file-format.html
77            #[cfg(any(feature = "compression", feature = "compression-brotli"))]
78            ContentCoding::BROTLI => "br",
79            // https://datatracker.ietf.org/doc/html/rfc8878
80            #[cfg(any(feature = "compression", feature = "compression-zstd"))]
81            ContentCoding::ZSTD => "zst",
82            _ => {
83                tracing::trace!(
84                    "preferred encoding based on the file extension was not determined, skipping"
85                );
86                continue;
87            }
88        };
89
90        let comp_name = match file_path.file_name().and_then(OsStr::to_str) {
91            Some(v) => v,
92            None => {
93                tracing::trace!("file name was not determined for the current path, skipping");
94                continue;
95            }
96        };
97
98        let file_path = file_path.with_file_name([comp_name, ".", comp_ext].concat());
99        tracing::trace!(
100            "trying to get the pre-compressed file variant metadata for {}",
101            file_path.display()
102        );
103
104        let (metadata, is_dir) = match try_metadata(&file_path) {
105            Ok(v) => v,
106            Err(e) => {
107                tracing::trace!("pre-compressed file variant error: {:?}", e);
108                continue;
109            }
110        };
111
112        if is_dir {
113            tracing::trace!("pre-compressed file variant found but it's a directory, skipping");
114            continue;
115        }
116
117        tracing::trace!("pre-compressed file variant found, serving it directly");
118
119        return Some(CompressedFileVariant {
120            file_path,
121            metadata,
122            encoding,
123        });
124    }
125
126    None
127}