1use std::{
2 fmt, io,
3 ops::Deref,
4 path::{Path, PathBuf},
5 rc::Rc,
6};
7
8use actix_web::{
9 body::BoxBody,
10 dev::{self, Service, ServiceRequest, ServiceResponse},
11 error::Error,
12 guard::Guard,
13 http::{header, Method},
14 HttpResponse,
15};
16use futures_core::future::LocalBoxFuture;
17
18use crate::{
19 named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
20 PathBufWrap, PathFilter,
21};
22
23#[derive(Clone)]
25pub struct FilesService(pub(crate) Rc<FilesServiceInner>);
26
27impl Deref for FilesService {
28 type Target = FilesServiceInner;
29
30 fn deref(&self) -> &Self::Target {
31 &self.0
32 }
33}
34
35pub struct FilesServiceInner {
36 pub(crate) directories: Vec<PathBuf>,
37 pub(crate) index: Option<String>,
38 pub(crate) show_index: bool,
39 pub(crate) redirect_to_slash: bool,
40 pub(crate) default: Option<HttpService>,
41 pub(crate) renderer: Rc<DirectoryRenderer>,
42 pub(crate) mime_override: Option<Rc<MimeOverride>>,
43 pub(crate) path_filter: Option<Rc<PathFilter>>,
44 pub(crate) file_flags: named::Flags,
45 pub(crate) guards: Option<Rc<dyn Guard>>,
46 pub(crate) hidden_files: bool,
47 pub(crate) try_compressed: bool,
48 pub(crate) size_threshold: u64,
49 pub(crate) with_permanent_redirect: bool,
50}
51
52impl fmt::Debug for FilesServiceInner {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str("FilesServiceInner")
55 }
56}
57
58impl FilesService {
59 async fn handle_err(
60 &self,
61 err: io::Error,
62 req: ServiceRequest,
63 ) -> Result<ServiceResponse, Error> {
64 log::debug!("error handling {}: {}", req.path(), err);
65
66 if let Some(ref default) = self.default {
67 default.call(req).await
68 } else {
69 Ok(req.error_response(err))
70 }
71 }
72
73 fn serve_named_file_with_encoding(
74 &self,
75 req: ServiceRequest,
76 mut named_file: NamedFile,
77 encoding: header::ContentEncoding,
78 ) -> ServiceResponse {
79 if let Some(ref mime_override) = self.mime_override {
80 let new_disposition = mime_override(&named_file.content_type.type_());
81 named_file.content_disposition.disposition = new_disposition;
82 }
83 named_file.flags = self.file_flags;
84
85 let (req, _) = req.into_parts();
86 let mut res = named_file
87 .read_mode_threshold(self.size_threshold)
88 .into_response(&req);
89
90 let header_value = match encoding {
91 header::ContentEncoding::Brotli => Some("br"),
92 header::ContentEncoding::Gzip => Some("gzip"),
93 header::ContentEncoding::Zstd => Some("zstd"),
94 header::ContentEncoding::Identity => None,
95 _ => unreachable!(),
97 };
98 if let Some(header_value) = header_value {
99 res.headers_mut().insert(
100 header::CONTENT_ENCODING,
101 header::HeaderValue::from_static(header_value),
102 );
103 res.headers_mut().append(
105 header::VARY,
106 header::HeaderValue::from_static("accept-encoding"),
107 );
108 }
109 ServiceResponse::new(req, res)
110 }
111
112 fn serve_named_file(&self, req: ServiceRequest, named_file: NamedFile) -> ServiceResponse {
113 self.serve_named_file_with_encoding(req, named_file, header::ContentEncoding::Identity)
114 }
115
116 fn show_index(&self, req: ServiceRequest, base: PathBuf, path: PathBuf) -> ServiceResponse {
117 let dir = Directory::new(base, path);
118
119 let (req, _) = req.into_parts();
120
121 (self.renderer)(&dir, &req).unwrap_or_else(|err| ServiceResponse::from_err(err, req))
122 }
123}
124
125impl fmt::Debug for FilesService {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 f.write_str("FilesService")
128 }
129}
130
131impl Service<ServiceRequest> for FilesService {
132 type Response = ServiceResponse<BoxBody>;
133 type Error = Error;
134 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
135
136 dev::always_ready!();
137
138 fn call(&self, req: ServiceRequest) -> Self::Future {
139 let is_method_valid = if let Some(guard) = &self.guards {
140 (**guard).check(&req.guard_ctx())
142 } else {
143 matches!(*req.method(), Method::HEAD | Method::GET)
145 };
146
147 let this = self.clone();
148
149 Box::pin(async move {
150 if !is_method_valid {
151 return Ok(req.into_response(
152 HttpResponse::MethodNotAllowed()
153 .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
154 .body("Request did not meet this resource's requirements."),
155 ));
156 }
157
158 let path_on_disk =
159 match PathBufWrap::parse_path(req.match_info().unprocessed(), this.hidden_files) {
160 Ok(item) => item,
161 Err(err) => return Ok(req.error_response(err)),
162 };
163
164 if let Some(filter) = &this.path_filter {
165 if !filter(path_on_disk.as_ref(), req.head()) {
166 if let Some(ref default) = this.default {
167 return default.call(req).await;
168 } else {
169 return Ok(req.into_response(HttpResponse::NotFound().finish()));
170 }
171 }
172 }
173
174 let mut last_miss = None;
175 let mut first_index_listing = None;
176 let mut found_unrenderable_dir = false;
177
178 for directory in &this.directories {
179 let path = directory.join(&path_on_disk);
181
182 if this.try_compressed && !path.is_dir() {
185 if let Some((named_file, encoding)) = find_compressed(&req, &path).await {
186 return Ok(this.serve_named_file_with_encoding(req, named_file, encoding));
187 }
188 }
189
190 if let Err(err) = path.canonicalize() {
191 if matches!(
192 err.kind(),
193 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
194 ) {
195 last_miss = Some(err);
196 continue;
197 }
198
199 return this.handle_err(err, req).await;
200 }
201
202 if path.is_dir() {
203 if this.redirect_to_slash
204 && !req.path().ends_with('/')
205 && (this.index.is_some() || this.show_index)
206 {
207 let redirect_to = format!("{}/", req.path());
208
209 let response = if this.with_permanent_redirect {
210 HttpResponse::PermanentRedirect()
211 } else {
212 HttpResponse::TemporaryRedirect()
213 }
214 .insert_header((header::LOCATION, redirect_to))
215 .finish();
216
217 return Ok(req.into_response(response));
218 }
219
220 match &this.index {
221 Some(index) => {
222 let named_path = path.join(index);
223 if this.try_compressed {
224 if let Some((named_file, encoding)) =
225 find_compressed(&req, &named_path).await
226 {
227 return Ok(this.serve_named_file_with_encoding(
228 req, named_file, encoding,
229 ));
230 }
231 }
232 match NamedFile::open(named_path) {
234 Ok(named_file) => return Ok(this.serve_named_file(req, named_file)),
235 Err(err)
236 if matches!(
237 err.kind(),
238 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
239 ) =>
240 {
241 if this.show_index && first_index_listing.is_none() {
242 first_index_listing =
243 Some((directory.to_path_buf(), path.clone()));
244 }
245 last_miss = Some(err);
246 }
247 Err(_) if this.show_index => {
248 if first_index_listing.is_none() {
249 first_index_listing =
250 Some((directory.to_path_buf(), path.clone()));
251 }
252 break;
253 }
254 Err(err) => return this.handle_err(err, req).await,
255 }
256 }
257 None if this.show_index => {
258 return Ok(this.show_index(req, directory.to_path_buf(), path));
259 }
260 None => found_unrenderable_dir = true,
261 }
262 } else {
263 match NamedFile::open(&path) {
264 Ok(named_file) => return Ok(this.serve_named_file(req, named_file)),
265 Err(err)
266 if matches!(
267 err.kind(),
268 io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
269 ) =>
270 {
271 last_miss = Some(err);
272 }
273 Err(err) => return this.handle_err(err, req).await,
274 }
275 }
276 }
277
278 if let Some((base, path)) = first_index_listing {
279 return Ok(this.show_index(req, base, path));
280 }
281
282 if found_unrenderable_dir {
283 return Ok(ServiceResponse::from_err(
284 FilesError::IsDirectory,
285 req.into_parts().0,
286 ));
287 }
288
289 let err = last_miss
290 .unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No such file"));
291 this.handle_err(err, req).await
292 })
293 }
294}
295
296const SUPPORTED_PRECOMPRESSION_ENCODINGS: &[header::ContentEncoding] = &[
298 header::ContentEncoding::Brotli,
299 header::ContentEncoding::Gzip,
300 header::ContentEncoding::Zstd,
301 header::ContentEncoding::Identity,
302];
303
304async fn find_compressed(
308 req: &ServiceRequest,
309 original_path: &Path,
310) -> Option<(NamedFile, header::ContentEncoding)> {
311 use actix_web::HttpMessage;
312 use header::{AcceptEncoding, ContentEncoding, Encoding};
313
314 let (content_type, content_disposition) =
317 match crate::named::get_content_type_and_disposition(original_path) {
318 Ok(values) => values,
319 Err(_) => return None,
320 };
321
322 let accept_encoding = req.get_header::<AcceptEncoding>()?;
323
324 let mut supported = SUPPORTED_PRECOMPRESSION_ENCODINGS
325 .iter()
326 .copied()
327 .map(Encoding::Known)
328 .collect::<Vec<_>>();
329
330 let mut content_type = Some(content_type);
332 let mut content_disposition = Some(content_disposition);
333
334 loop {
335 let chosen = accept_encoding.negotiate(supported.iter())?;
337
338 let encoding = match chosen {
339 Encoding::Known(enc) => enc,
340 Encoding::Unknown(_) => return None,
342 };
343
344 if encoding == ContentEncoding::Identity {
346 return None;
347 }
348
349 let extension = match encoding {
350 ContentEncoding::Brotli => ".br",
351 ContentEncoding::Gzip => ".gz",
352 ContentEncoding::Zstd => ".zst",
353 ContentEncoding::Identity => unreachable!(),
354 _ => unreachable!(),
356 };
357
358 let mut compressed_path = original_path.to_owned();
359 let mut filename = compressed_path.file_name()?.to_owned();
360 filename.push(extension);
361 compressed_path.set_file_name(filename);
362
363 match NamedFile::open(&compressed_path) {
364 Ok(mut named_file) => {
365 named_file.content_type = content_type.take().unwrap();
366 named_file.content_disposition = content_disposition.take().unwrap();
367 return Some((named_file, encoding));
368 }
369 Err(_) => {
371 supported.retain(|enc| enc != &chosen);
372 }
373 }
374 }
375}