1#![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)]
68pub enum ServeEntry {
70 NotFound,
72 IoError(io::Error),
76 File(fs::File, fs::Metadata, PathBuf),
78 Directory(PathBuf, fs::ReadDir),
80}
81
82pub 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 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 pub fn new(worker: W, config: C) -> Self {
110 Self {
111 worker,
112 config,
113 }
114 }
115
116 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 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 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 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 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 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}