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