http_fs/
config.rs

1//! Configuration module
2
3use percent_encoding::utf8_percent_encode;
4use crate::utils::PATH_ENCODE_SET;
5
6use std::fs;
7use std::path::Path;
8use core::marker;
9use core::fmt::{self, Write};
10use core::future::{self, Future};
11
12use crate::headers::cd::DispositionType;
13use crate::file::FileReadResult;
14
15///Default serve directory
16pub const DEFAULT_SERVE_DIR: &'static str = ".";
17
18///Describes task spawner for FS related tasks
19pub trait FsTaskSpawner: marker::Send + Clone + Unpin {
20    ///Error processing spawned task
21    type SpawnError: std::error::Error + fmt::Debug + fmt::Display + Send + Sync + 'static;
22    ///File read future spawned on IO runtime
23    type FileReadFut: Future<Output = Result<FileReadResult, Self::SpawnError>> + Unpin;
24
25    ///Spawns file reading task task onto runtime.
26    fn spawn_file_read<F: FnOnce() -> FileReadResult + Send + 'static>(fut: F) -> Self::FileReadFut;
27}
28
29///Describes how to serve file
30pub trait FileServeConfig: marker::Send + Unpin {
31    ///Returns maximum size for buffer to read file.
32    ///
33    ///By default 64kb
34    fn max_buffer_size() -> u64 {
35        65_536
36    }
37
38    ///Describes mapping for mime type to content disposition header
39    ///
40    ///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline.
41    ///Others are mapped to Attachment
42    fn content_disposition_map(typ: mime::Name) -> DispositionType {
43        match typ {
44            mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
45            _ => DispositionType::Attachment,
46        }
47    }
48
49    ///Specifies whether `ETag` should be used for caching
50    ///
51    ///Provides path to the file, relative to File Service directory
52    ///
53    ///By default it is true
54    fn is_use_etag(_path: &Path) -> bool {
55        true
56    }
57
58    ///Specifies whether `Last-Modified` should be used for caching
59    ///
60    ///Provides path to the file, relative to File Service directory
61    ///
62    ///By default it is true
63    fn is_use_last_modifier(_path: &Path) -> bool {
64        true
65    }
66}
67
68///Describes how to serve directory
69pub trait DirectoryListingConfig {
70    ///Describes how to create HTML text for Directory listing
71    ///
72    ///`base` is root directory for file service.
73    ///`path` is relative to `base` path to directory.
74    ///`dir` is result of `fs::read_dir`
75    ///
76    ///Default implementation provides trivial HTML page with listing.
77    fn create_body(base: &Path, path: &Path, dir: fs::ReadDir) -> bytes::Bytes {
78        let mut body = crate::utils::BytesWriter::with_capacity(256);
79
80        let _ = write!(body, "<html>\n");
81        let _ = write!(body, "    <head>\n");
82        let _ = write!(body, "        <title>Index of {}/</title>\n", path.display());
83        let _ = write!(body, "    </head>\n");
84        let _ = write!(body, "    <body>\n");
85        let _ = write!(body, "        <h1>Index of {}/</h1>\n", path.display());
86        let _ = write!(body, "        <ul>\n");
87
88        let _ = write!(body, "<li><a href=\"/{}\">.</a></li>\n", utf8_percent_encode(&path.to_string_lossy(), PATH_ENCODE_SET));
89        if let Some(parent) = path.parent() {
90            let _ = write!(body, "<li><a href=\"/{}\">..</a></li>\n", utf8_percent_encode(&parent.to_string_lossy(), PATH_ENCODE_SET));
91        }
92
93        for entry in dir.filter_map(|entry| entry.ok()) {
94            if entry.file_name().to_str().map(|entry| entry.starts_with('.')).unwrap_or(false) {
95                continue;
96            }
97
98            let is_dir = match entry.metadata().map(|meta| meta.file_type()) {
99                Ok(meta) => if !meta.is_dir() && !meta.is_file() && !meta.is_symlink() {
100                    continue;
101                } else {
102                    meta.is_dir()
103                },
104                Err(_) => continue,
105            };
106
107            let entry_path = entry.path();
108            let entry_path = match entry_path.strip_prefix(base) {
109                Ok(res) => res,
110                Err(_) => &entry_path,
111            };
112
113            let _ = write!(body, "<li><a href=\"");
114            for component in entry_path.components().map(|component| component.as_os_str()) {
115                let _ = write!(body, "/{}", utf8_percent_encode(&component.to_string_lossy(), PATH_ENCODE_SET));
116            }
117
118            let _ = write!(body, "\">{}", v_htmlescape::escape(&entry.file_name().to_string_lossy()));
119
120            if is_dir {
121                let _ = write!(body, "/");
122            }
123
124            let _ = write!(body, "</a></li>\n");
125        }
126
127        let _ = write!(body, "        </ul>\n");
128        let _ = write!(body, "    </body>\n");
129        let _ = write!(body, "</html>\n");
130
131        body.freeze()
132    }
133}
134
135///Configuration description
136pub trait StaticFileConfig {
137    ///File serve configuration
138    type FileService: FileServeConfig + 'static;
139    ///Directory serve configuration
140    type DirService: DirectoryListingConfig;
141
142    ///Returns whether specified method is allowed for use.
143    ///By default allows `HEAD` and `GET`
144    fn is_method_allowed(method: &http::Method) -> bool {
145        match method {
146            &http::Method::GET | &http::Method::HEAD => true,
147            _ => false,
148        }
149    }
150
151    ///Returns directory from where to serve files.
152    ///
153    ///By default returns `.`
154    fn serve_dir(&self) -> &Path {
155        Path::new(DEFAULT_SERVE_DIR)
156    }
157
158    ///Specifies router prefix.
159    ///
160    ///To be used by frameworks such as Actix
161    ///
162    ///Defaults to `/`
163    fn router_prefix(&self) -> &str {
164        "/"
165    }
166
167    ///Returns name of index file to show.
168    ///
169    ///`path` points to directory relative to `StaticFileConfig::serve_dir`.
170    ///
171    ///By default returns `None`
172    fn index_file(&self, _path: &Path) -> Option<&Path> {
173        None
174    }
175
176    ///Returns whether directory should be listed on access
177    ///
178    ///`path` points to directory relative to `StaticFileConfig::serve_dir`.
179    ///
180    ///By default returns `false`
181    fn handle_directory(&self, _path: &Path) -> bool {
182        false
183    }
184
185    ///Handles entry that hasn't been found.
186    ///
187    ///`path` points to entry relative to `StaticFileConfig::serve_dir`.
188    ///
189    ///By default prepares empty `NotFound` response with empty body
190    fn handle_not_found(&self, _path: &Path, _out_headers: &mut http::HeaderMap) -> (http::StatusCode, bytes::Bytes) {
191        (http::StatusCode::NOT_FOUND, bytes::Bytes::new())
192    }
193}
194
195#[derive(Clone, Copy)]
196///Default configuration
197pub struct DefaultConfig;
198impl FileServeConfig for DefaultConfig {}
199impl DirectoryListingConfig for DefaultConfig {}
200impl StaticFileConfig for DefaultConfig {
201    type FileService = Self;
202    type DirService = Self;
203}
204
205#[derive(Clone, Copy)]
206///Dummy task spawner that returns error when invoked
207pub struct DummyWorker;
208impl FsTaskSpawner for DummyWorker {
209    type SpawnError = core::convert::Infallible;
210    type FileReadFut = future::Ready<Result<FileReadResult, Self::SpawnError>>;
211
212    #[cold]
213    #[inline(never)]
214    fn spawn_file_read<F: FnOnce() -> FileReadResult + Send + 'static>(_: F) -> Self::FileReadFut {
215        unimplemented!()
216    }
217}
218
219#[cfg(feature = "tokio")]
220#[derive(Clone, Copy)]
221///Tokio worker to spawn blocking FS related tasks
222///
223///Requires `tokio` feature
224pub struct TokioWorker;
225
226#[cfg(feature = "tokio")]
227impl FsTaskSpawner for TokioWorker {
228    type SpawnError = tokio::task::JoinError;
229    type FileReadFut = tokio::task::JoinHandle<FileReadResult>;
230
231    fn spawn_file_read<F: FnOnce() -> FileReadResult + Send + 'static>(fut: F) -> Self::FileReadFut {
232        tokio::task::spawn_blocking(fut)
233    }
234}