http_fs/
lib.rs

1//!# http-fs
2//!
3//!## Features
4//!
5//!- `tokio` - Enables `tokio` runtime integration. Enables `rt`.
6//!- `hyper` - Enables `hyper` [integration](adaptors/hyper/index.html). Enables `http1` and `server` features.
7//!
8//!## Usage
9//!
10//!```rust
11//!use http_fs::config::{self, StaticFileConfig, DummyWorker};
12//!use http_fs::{StaticFiles};
13//!
14//!use std::path::Path;
15//!
16//!pub struct DirectoryConfig;
17//!impl StaticFileConfig for DirectoryConfig {
18//!    type FileService = config::DefaultConfig;
19//!    type DirService = config::DefaultConfig;
20//!
21//!    fn handle_directory(&self, _path: &Path) -> bool {
22//!        true
23//!    }
24//!}
25//!
26//!fn main() {
27//!    let static_files = StaticFiles::new(DummyWorker, DirectoryConfig);
28//!}
29//!```
30
31#![warn(missing_docs)]
32#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]
33
34#[cfg(feature = "hyper")]
35pub extern crate hyper;
36pub extern crate http;
37pub extern crate etag;
38pub extern crate httpdate;
39
40pub mod config;
41pub mod headers;
42pub mod file;
43mod body;
44pub mod utils;
45pub mod adaptors;
46
47use http::header::{self, HeaderValue, HeaderMap};
48use http::{Method, StatusCode, Uri};
49use percent_encoding::percent_decode;
50
51pub use config::{FileServeConfig, DirectoryListingConfig, StaticFileConfig, FsTaskSpawner};
52pub use body::Body;
53
54use core::fmt;
55use std::{fs, io};
56use std::path::{PathBuf, Path};
57use std::borrow::Cow;
58
59#[cold]
60#[inline(never)]
61fn unexpected_error<T: fmt::Display, W: FsTaskSpawner, C: FileServeConfig>(error: T, response: &mut http::Response<Body<W, C>>) {
62    *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
63    *response.body_mut() = Body::Full(error.to_string().into())
64}
65
66#[allow(clippy::large_enum_variant)]
67#[derive(Debug)]
68///Entry's in `fs`
69pub enum ServeEntry {
70    ///Entry is not found
71    NotFound,
72    ///Error when looking up entry
73    ///
74    ///Likely because file either doesn't exist or you lacks permissions
75    IoError(io::Error),
76    ///File entry is found
77    File(fs::File, fs::Metadata, PathBuf),
78    ///Directory entry is found
79    Directory(PathBuf, fs::ReadDir),
80}
81
82/// Static files service
83pub struct StaticFiles<W, C> {
84    worker: W,
85    config: C,
86}
87
88impl<W: Clone, C: Clone> Clone for StaticFiles<W, C> {
89    fn clone(&self) -> StaticFiles<W, C> {
90        Self {
91            worker: self.worker.clone(),
92            config: self.config.clone(),
93        }
94    }
95}
96
97impl<W: FsTaskSpawner> StaticFiles<W, config::DefaultConfig> {
98    ///Creates new instance with default config.
99    pub fn default_with(worker: W) -> Self {
100        Self {
101            worker,
102            config: config::DefaultConfig,
103        }
104    }
105}
106
107impl<W: FsTaskSpawner, C: StaticFileConfig> StaticFiles<W, C> {
108    ///Creates new instance with provided config
109    pub fn new(worker: W, config: C) -> Self {
110        Self {
111            worker,
112            config,
113        }
114    }
115
116    ///Serves file
117    pub fn serve(&self, path: &Path) -> ServeEntry {
118        let mut full_path = self.config.serve_dir().join(path);
119
120        let mut meta = match full_path.metadata() {
121            Ok(meta) => meta,
122            Err(_) => return ServeEntry::NotFound,
123        };
124
125        if meta.is_dir() {
126            if let Some(name) = self.config.index_file(path) {
127                full_path = full_path.join(name);
128                meta = match full_path.metadata() {
129                    Ok(meta) => meta,
130                    Err(_) => return ServeEntry::NotFound,
131                };
132            } else if self.config.handle_directory(path) {
133                return match full_path.read_dir() {
134                    Ok(dir) => ServeEntry::Directory(path.to_path_buf(), dir),
135                    Err(error) => ServeEntry::IoError(error),
136                }
137            } else {
138                return ServeEntry::NotFound
139            }
140        }
141
142        match fs::File::open(&full_path) {
143            Ok(file) => ServeEntry::File(file, meta, full_path),
144            Err(error) => ServeEntry::IoError(error),
145        }
146    }
147
148    ///Handles not found directory
149    pub fn handle_not_found(&self, path: &Path, out_headers: &mut http::HeaderMap) -> (StatusCode, bytes::Bytes) {
150        self.config.handle_not_found(path, out_headers)
151    }
152
153    ///Get HTML page with listing
154    pub fn list_dir(&self, path: &Path, dir: fs::ReadDir) -> bytes::Bytes {
155        C::DirService::create_body(self.config.serve_dir(), path, dir)
156    }
157
158    ///Serve directory routine
159    pub fn handle_dir(&self, path: &Path, dir: fs::ReadDir, out_headers: &mut http::HeaderMap) -> bytes::Bytes {
160        const HTML: HeaderValue = HeaderValue::from_static("text/html; charset=utf-8");
161
162        let body = C::DirService::create_body(self.config.serve_dir(), path, dir);
163        out_headers.insert(header::CONTENT_TYPE, HTML);
164        out_headers.insert(header::CONTENT_LENGTH, body.len().into());
165        body
166    }
167
168    ///Serves file routine
169    pub fn serve_file(&self, path: &Path, file: fs::File, meta: fs::Metadata, method: http::Method, headers: &http::HeaderMap, out_headers: &mut http::HeaderMap) -> (StatusCode, Body<W, C::FileService>) {
170        let file_name = match path.file_name().and_then(|file_name| file_name.to_str()) {
171            Some(file_name) => file_name,
172            None => return (StatusCode::NOT_FOUND, Body::empty())
173        };
174
175        file::ServeFile::<W, C::FileService>::from_parts_with_cfg(file_name, file, meta).prepare(path, method, headers, out_headers)
176    }
177
178    ///Serves `http` request
179    pub fn serve_http(&self, method: &Method, uri: &Uri, headers: &HeaderMap) -> http::Response<Body<W, C::FileService>> {
180        const ALLOWED: HeaderValue = HeaderValue::from_static("GET, HEAD");
181
182        let mut response = http::Response::new(Body::empty());
183        if !C::is_method_allowed(method) {
184            *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
185            response.headers_mut().insert(header::ALLOW, ALLOWED);
186        } else {
187            let path = uri.path().trim_start_matches('/');
188            let path = match percent_decode(path.as_bytes()).decode_utf8() {
189                Ok(path) => path,
190                Err(unexpected) => {
191                    unexpected_error(unexpected, &mut response);
192                    return response;
193                }
194            };
195            let path = &path;
196            let path = match path {
197                Cow::Borrowed(path) => Path::new(path),
198                Cow::Owned(ref path) => Path::new(path),
199            };
200
201            match self.serve(&path) {
202                ServeEntry::NotFound | ServeEntry::IoError(_) => {
203                    let (code, body) = self.handle_not_found(&path, response.headers_mut());
204                    *response.status_mut() = code;
205                    *response.body_mut() = body.into();
206                },
207                ServeEntry::Directory(path, dir) => {
208                    *response.status_mut() = StatusCode::OK;
209                    let body = self.handle_dir(&path, dir, response.headers_mut());
210                    *response.body_mut() = body.into();
211                },
212                ServeEntry::File(file, meta, path) => {
213                    let (code, body) = self.serve_file(&path, file, meta, method.clone(), headers, response.headers_mut());
214                    *response.status_mut() = code;
215                    *response.body_mut() = body;
216                }
217            }
218        }
219        response
220    }
221}
222
223#[cfg(feature = "tokio")]
224impl Default for StaticFiles<config::TokioWorker, config::DefaultConfig> {
225    #[inline]
226    fn default() -> Self {
227        Self::default_with(config::TokioWorker)
228    }
229}