1use std::{
2 cell::RefCell,
3 fmt, io,
4 path::{Path, PathBuf},
5 rc::Rc,
6};
7
8use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
9use actix_web::{
10 dev::{
11 AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, ServiceResponse,
12 },
13 error::Error,
14 guard::Guard,
15 http::header::DispositionType,
16 HttpRequest,
17};
18use futures_core::future::LocalBoxFuture;
19
20use crate::{
21 directory_listing, named,
22 service::{FilesService, FilesServiceInner},
23 Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
24};
25
26pub struct Files {
39 mount_path: String,
40 directory: PathBuf,
41 index: Option<String>,
42 show_index: bool,
43 redirect_to_slash: bool,
44 default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
45 renderer: Rc<DirectoryRenderer>,
46 mime_override: Option<Rc<MimeOverride>>,
47 path_filter: Option<Rc<PathFilter>>,
48 file_flags: named::Flags,
49 use_guards: Option<Rc<dyn Guard>>,
50 guards: Vec<Rc<dyn Guard>>,
51 hidden_files: bool,
52}
53
54impl fmt::Debug for Files {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.write_str("Files")
57 }
58}
59
60impl Clone for Files {
61 fn clone(&self) -> Self {
62 Self {
63 directory: self.directory.clone(),
64 index: self.index.clone(),
65 show_index: self.show_index,
66 redirect_to_slash: self.redirect_to_slash,
67 default: self.default.clone(),
68 renderer: self.renderer.clone(),
69 file_flags: self.file_flags,
70 mount_path: self.mount_path.clone(),
71 mime_override: self.mime_override.clone(),
72 path_filter: self.path_filter.clone(),
73 use_guards: self.use_guards.clone(),
74 guards: self.guards.clone(),
75 hidden_files: self.hidden_files,
76 }
77 }
78}
79
80impl Files {
81 pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
99 let orig_dir = serve_from.into();
100 let dir = match orig_dir.canonicalize() {
101 Ok(canon_dir) => canon_dir,
102 Err(_) => {
103 log::error!("Specified path is not a directory: {:?}", orig_dir);
104 PathBuf::new()
105 }
106 };
107
108 Files {
109 mount_path: mount_path.trim_end_matches('/').to_owned(),
110 directory: dir,
111 index: None,
112 show_index: false,
113 redirect_to_slash: false,
114 default: Rc::new(RefCell::new(None)),
115 renderer: Rc::new(directory_listing),
116 mime_override: None,
117 path_filter: None,
118 file_flags: named::Flags::default(),
119 use_guards: None,
120 guards: Vec::new(),
121 hidden_files: false,
122 }
123 }
124
125 pub fn show_files_listing(mut self) -> Self {
132 self.show_index = true;
133 self
134 }
135
136 pub fn redirect_to_slash_directory(mut self) -> Self {
140 self.redirect_to_slash = true;
141 self
142 }
143
144 pub fn files_listing_renderer<F>(mut self, f: F) -> Self
146 where
147 for<'r, 's> F:
148 Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
149 {
150 self.renderer = Rc::new(f);
151 self
152 }
153
154 pub fn mime_override<F>(mut self, f: F) -> Self
156 where
157 F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
158 {
159 self.mime_override = Some(Rc::new(f));
160 self
161 }
162
163 pub fn path_filter<F>(mut self, f: F) -> Self
188 where
189 F: Fn(&Path, &RequestHead) -> bool + 'static,
190 {
191 self.path_filter = Some(Rc::new(f));
192 self
193 }
194
195 pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
203 self.index = Some(index.into());
204 self
205 }
206
207 pub fn use_etag(mut self, value: bool) -> Self {
211 self.file_flags.set(named::Flags::ETAG, value);
212 self
213 }
214
215 pub fn use_last_modified(mut self, value: bool) -> Self {
219 self.file_flags.set(named::Flags::LAST_MD, value);
220 self
221 }
222
223 pub fn prefer_utf8(mut self, value: bool) -> Self {
227 self.file_flags.set(named::Flags::PREFER_UTF8, value);
228 self
229 }
230
231 pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
251 self.guards.push(Rc::new(guard));
252 self
253 }
254
255 pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
261 self.use_guards = Some(Rc::new(guard));
262 self
263 }
264
265 #[doc(hidden)]
267 #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
268 pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
269 self.method_guard(guard)
270 }
271
272 pub fn disable_content_disposition(mut self) -> Self {
276 self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
277 self
278 }
279
280 pub fn default_handler<F, U>(mut self, f: F) -> Self
301 where
302 F: IntoServiceFactory<U, ServiceRequest>,
303 U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
304 + 'static,
305 {
306 self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
308 f.into_factory().map_init_err(|_| ()),
309 )))));
310
311 self
312 }
313
314 pub fn use_hidden_files(mut self) -> Self {
316 self.hidden_files = true;
317 self
318 }
319}
320
321impl HttpServiceFactory for Files {
322 fn register(mut self, config: &mut AppService) {
323 let guards = if self.guards.is_empty() {
324 None
325 } else {
326 let guards = std::mem::take(&mut self.guards);
327 Some(
328 guards
329 .into_iter()
330 .map(|guard| -> Box<dyn Guard> { Box::new(guard) })
331 .collect::<Vec<_>>(),
332 )
333 };
334
335 if self.default.borrow().is_none() {
336 *self.default.borrow_mut() = Some(config.default_service());
337 }
338
339 let rdef = if config.is_root() {
340 ResourceDef::root_prefix(&self.mount_path)
341 } else {
342 ResourceDef::prefix(&self.mount_path)
343 };
344
345 config.register_service(rdef, guards, self, None)
346 }
347}
348
349impl ServiceFactory<ServiceRequest> for Files {
350 type Response = ServiceResponse;
351 type Error = Error;
352 type Config = ();
353 type Service = FilesService;
354 type InitError = ();
355 type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
356
357 fn new_service(&self, _: ()) -> Self::Future {
358 let mut inner = FilesServiceInner {
359 directory: self.directory.clone(),
360 index: self.index.clone(),
361 show_index: self.show_index,
362 redirect_to_slash: self.redirect_to_slash,
363 default: None,
364 renderer: self.renderer.clone(),
365 mime_override: self.mime_override.clone(),
366 path_filter: self.path_filter.clone(),
367 file_flags: self.file_flags,
368 guards: self.use_guards.clone(),
369 hidden_files: self.hidden_files,
370 };
371
372 if let Some(ref default) = *self.default.borrow() {
373 let fut = default.new_service(());
374 Box::pin(async {
375 match fut.await {
376 Ok(default) => {
377 inner.default = Some(default);
378 Ok(FilesService(Rc::new(inner)))
379 }
380 Err(_) => Err(()),
381 }
382 })
383 } else {
384 Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
385 }
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use actix_web::{
392 http::StatusCode,
393 test::{self, TestRequest},
394 App, HttpResponse,
395 };
396
397 use super::*;
398
399 #[actix_web::test]
400 async fn custom_files_listing_renderer() {
401 let srv = test::init_service(
402 App::new().service(
403 Files::new("/", "./tests")
404 .show_files_listing()
405 .files_listing_renderer(|dir, req| {
406 Ok(ServiceResponse::new(
407 req.clone(),
408 HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()),
409 ))
410 }),
411 ),
412 )
413 .await;
414
415 let req = TestRequest::with_uri("/").to_request();
416 let res = test::call_service(&srv, req).await;
417
418 assert_eq!(res.status(), StatusCode::OK);
419 let body = test::read_body(res).await;
420 let body_str = std::str::from_utf8(&body).unwrap();
421 let actual_path = Path::new(&body_str);
422 let expected_path = Path::new("actix-files/tests");
423 assert!(
424 actual_path.ends_with(expected_path),
425 "body {:?} does not end with {:?}",
426 actual_path,
427 expected_path
428 );
429 }
430}