actix_files/
files.rs

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
26/// Static files handling service.
27///
28/// `Files` service must be registered with `App::service()` method.
29///
30/// # Examples
31/// ```
32/// use actix_web::App;
33/// use actix_files::Files;
34///
35/// let app = App::new()
36///     .service(Files::new("/static", "."));
37/// ```
38pub 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    read_mode_threshold: u64,
53}
54
55impl fmt::Debug for Files {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        f.write_str("Files")
58    }
59}
60
61impl Clone for Files {
62    fn clone(&self) -> Self {
63        Self {
64            directory: self.directory.clone(),
65            index: self.index.clone(),
66            show_index: self.show_index,
67            redirect_to_slash: self.redirect_to_slash,
68            default: self.default.clone(),
69            renderer: self.renderer.clone(),
70            file_flags: self.file_flags,
71            mount_path: self.mount_path.clone(),
72            mime_override: self.mime_override.clone(),
73            path_filter: self.path_filter.clone(),
74            use_guards: self.use_guards.clone(),
75            guards: self.guards.clone(),
76            hidden_files: self.hidden_files,
77            read_mode_threshold: self.read_mode_threshold,
78        }
79    }
80}
81
82impl Files {
83    /// Create new `Files` instance for a specified base directory.
84    ///
85    /// # Argument Order
86    /// The first argument (`mount_path`) is the root URL at which the static files are served.
87    /// For example, `/assets` will serve files at `example.com/assets/...`.
88    ///
89    /// The second argument (`serve_from`) is the location on disk at which files are loaded.
90    /// This can be a relative path. For example, `./` would serve files from the current
91    /// working directory.
92    ///
93    /// # Implementation Notes
94    /// If the mount path is set as the root path `/`, services registered after this one will
95    /// be inaccessible. Register more specific handlers and services first.
96    ///
97    /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
98    /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
99    /// the number of server [workers](actix_web::HttpServer::workers), by default.
100    pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
101        let orig_dir = serve_from.into();
102        let dir = match orig_dir.canonicalize() {
103            Ok(canon_dir) => canon_dir,
104            Err(_) => {
105                log::error!("Specified path is not a directory: {:?}", orig_dir);
106                PathBuf::new()
107            }
108        };
109
110        Files {
111            mount_path: mount_path.trim_end_matches('/').to_owned(),
112            directory: dir,
113            index: None,
114            show_index: false,
115            redirect_to_slash: false,
116            default: Rc::new(RefCell::new(None)),
117            renderer: Rc::new(directory_listing),
118            mime_override: None,
119            path_filter: None,
120            file_flags: named::Flags::default(),
121            use_guards: None,
122            guards: Vec::new(),
123            hidden_files: false,
124            read_mode_threshold: 0,
125        }
126    }
127
128    /// Show files listing for directories.
129    ///
130    /// By default show files listing is disabled.
131    ///
132    /// When used with [`Files::index_file()`], files listing is shown as a fallback
133    /// when the index file is not found.
134    pub fn show_files_listing(mut self) -> Self {
135        self.show_index = true;
136        self
137    }
138
139    /// Redirects to a slash-ended path when browsing a directory.
140    ///
141    /// By default never redirect.
142    pub fn redirect_to_slash_directory(mut self) -> Self {
143        self.redirect_to_slash = true;
144        self
145    }
146
147    /// Set custom directory renderer.
148    pub fn files_listing_renderer<F>(mut self, f: F) -> Self
149    where
150        for<'r, 's> F:
151            Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
152    {
153        self.renderer = Rc::new(f);
154        self
155    }
156
157    /// Specifies MIME override callback.
158    pub fn mime_override<F>(mut self, f: F) -> Self
159    where
160        F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
161    {
162        self.mime_override = Some(Rc::new(f));
163        self
164    }
165
166    /// Sets path filtering closure.
167    ///
168    /// The path provided to the closure is relative to `serve_from` path.
169    /// You can safely join this path with the `serve_from` path to get the real path.
170    /// However, the real path may not exist since the filter is called before checking path existence.
171    ///
172    /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise,
173    /// `404 Not Found` is returned.
174    ///
175    /// # Examples
176    /// ```
177    /// use std::path::Path;
178    /// use actix_files::Files;
179    ///
180    /// // prevent searching subdirectories and following symlinks
181    /// let files_service = Files::new("/", "./static").path_filter(|path, _| {
182    ///     path.components().count() == 1
183    ///         && Path::new("./static")
184    ///             .join(path)
185    ///             .symlink_metadata()
186    ///             .map(|m| !m.file_type().is_symlink())
187    ///             .unwrap_or(false)
188    /// });
189    /// ```
190    pub fn path_filter<F>(mut self, f: F) -> Self
191    where
192        F: Fn(&Path, &RequestHead) -> bool + 'static,
193    {
194        self.path_filter = Some(Rc::new(f));
195        self
196    }
197
198    /// Set index file
199    ///
200    /// Shows specific index file for directories instead of
201    /// showing files listing.
202    ///
203    /// If the index file is not found, files listing is shown as a fallback if
204    /// [`Files::show_files_listing()`] is set.
205    pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
206        self.index = Some(index.into());
207        self
208    }
209
210    /// Sets the size threshold that determines file read mode (sync/async).
211    ///
212    /// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
213    /// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
214    /// files.
215    ///
216    /// Tweaking this value according to your expected usage may lead to signifiant performance
217    /// gains (or losses in other handlers, if `size` is too high).
218    ///
219    /// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
220    ///
221    /// Default is 0, meaning all files are read asynchronously.
222    pub fn read_mode_threshold(mut self, size: u64) -> Self {
223        self.read_mode_threshold = size;
224        self
225    }
226
227    /// Specifies whether to use ETag or not.
228    ///
229    /// Default is true.
230    pub fn use_etag(mut self, value: bool) -> Self {
231        self.file_flags.set(named::Flags::ETAG, value);
232        self
233    }
234
235    /// Specifies whether to use Last-Modified or not.
236    ///
237    /// Default is true.
238    pub fn use_last_modified(mut self, value: bool) -> Self {
239        self.file_flags.set(named::Flags::LAST_MD, value);
240        self
241    }
242
243    /// Specifies whether text responses should signal a UTF-8 encoding.
244    ///
245    /// Default is false (but will default to true in a future version).
246    pub fn prefer_utf8(mut self, value: bool) -> Self {
247        self.file_flags.set(named::Flags::PREFER_UTF8, value);
248        self
249    }
250
251    /// Adds a routing guard.
252    ///
253    /// Use this to allow multiple chained file services that respond to strictly different
254    /// properties of a request. Due to the way routing works, if a guard check returns true and the
255    /// request starts being handled by the file service, it will not be able to back-out and try
256    /// the next service, you will simply get a 404 (or 405) error response.
257    ///
258    /// To allow `POST` requests to retrieve files, see [`Files::method_guard()`].
259    ///
260    /// # Examples
261    /// ```
262    /// use actix_web::{guard::Header, App};
263    /// use actix_files::Files;
264    ///
265    /// App::new().service(
266    ///     Files::new("/","/my/site/files")
267    ///         .guard(Header("Host", "example.com"))
268    /// );
269    /// ```
270    pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
271        self.guards.push(Rc::new(guard));
272        self
273    }
274
275    /// Specifies guard to check before fetching directory listings or files.
276    ///
277    /// Note that this guard has no effect on routing; it's main use is to guard on the request's
278    /// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
279    /// See [`Files::guard`] for routing guards.
280    pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
281        self.use_guards = Some(Rc::new(guard));
282        self
283    }
284
285    /// See [`Files::method_guard`].
286    #[doc(hidden)]
287    #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
288    pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
289        self.method_guard(guard)
290    }
291
292    /// Disable `Content-Disposition` header.
293    ///
294    /// By default Content-Disposition` header is enabled.
295    pub fn disable_content_disposition(mut self) -> Self {
296        self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
297        self
298    }
299
300    /// Sets default handler which is used when no matched file could be found.
301    ///
302    /// # Examples
303    /// Setting a fallback static file handler:
304    /// ```
305    /// use actix_files::{Files, NamedFile};
306    /// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
307    ///
308    /// # fn run() -> Result<(), actix_web::Error> {
309    /// let files = Files::new("/", "./static")
310    ///     .index_file("index.html")
311    ///     .default_handler(fn_service(|req: ServiceRequest| async {
312    ///         let (req, _) = req.into_parts();
313    ///         let file = NamedFile::open_async("./static/404.html").await?;
314    ///         let res = file.into_response(&req);
315    ///         Ok(ServiceResponse::new(req, res))
316    ///     }));
317    /// # Ok(())
318    /// # }
319    /// ```
320    pub fn default_handler<F, U>(mut self, f: F) -> Self
321    where
322        F: IntoServiceFactory<U, ServiceRequest>,
323        U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
324            + 'static,
325    {
326        // create and configure default resource
327        self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
328            f.into_factory().map_init_err(|_| ()),
329        )))));
330
331        self
332    }
333
334    /// Enables serving hidden files and directories, allowing a leading dots in url fragments.
335    pub fn use_hidden_files(mut self) -> Self {
336        self.hidden_files = true;
337        self
338    }
339}
340
341impl HttpServiceFactory for Files {
342    fn register(mut self, config: &mut AppService) {
343        let guards = if self.guards.is_empty() {
344            None
345        } else {
346            let guards = std::mem::take(&mut self.guards);
347            Some(
348                guards
349                    .into_iter()
350                    .map(|guard| -> Box<dyn Guard> { Box::new(guard) })
351                    .collect::<Vec<_>>(),
352            )
353        };
354
355        if self.default.borrow().is_none() {
356            *self.default.borrow_mut() = Some(config.default_service());
357        }
358
359        let rdef = if config.is_root() {
360            ResourceDef::root_prefix(&self.mount_path)
361        } else {
362            ResourceDef::prefix(&self.mount_path)
363        };
364
365        config.register_service(rdef, guards, self, None)
366    }
367}
368
369impl ServiceFactory<ServiceRequest> for Files {
370    type Response = ServiceResponse;
371    type Error = Error;
372    type Config = ();
373    type Service = FilesService;
374    type InitError = ();
375    type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
376
377    fn new_service(&self, _: ()) -> Self::Future {
378        let mut inner = FilesServiceInner {
379            directory: self.directory.clone(),
380            index: self.index.clone(),
381            show_index: self.show_index,
382            redirect_to_slash: self.redirect_to_slash,
383            default: None,
384            renderer: self.renderer.clone(),
385            mime_override: self.mime_override.clone(),
386            path_filter: self.path_filter.clone(),
387            file_flags: self.file_flags,
388            guards: self.use_guards.clone(),
389            hidden_files: self.hidden_files,
390            size_threshold: self.read_mode_threshold,
391        };
392
393        if let Some(ref default) = *self.default.borrow() {
394            let fut = default.new_service(());
395            Box::pin(async {
396                match fut.await {
397                    Ok(default) => {
398                        inner.default = Some(default);
399                        Ok(FilesService(Rc::new(inner)))
400                    }
401                    Err(_) => Err(()),
402                }
403            })
404        } else {
405            Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use actix_web::{
413        http::StatusCode,
414        test::{self, TestRequest},
415        App, HttpResponse,
416    };
417
418    use super::*;
419
420    #[actix_web::test]
421    async fn custom_files_listing_renderer() {
422        let srv = test::init_service(
423            App::new().service(
424                Files::new("/", "./tests")
425                    .show_files_listing()
426                    .files_listing_renderer(|dir, req| {
427                        Ok(ServiceResponse::new(
428                            req.clone(),
429                            HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()),
430                        ))
431                    }),
432            ),
433        )
434        .await;
435
436        let req = TestRequest::with_uri("/").to_request();
437        let res = test::call_service(&srv, req).await;
438
439        assert_eq!(res.status(), StatusCode::OK);
440        let body = test::read_body(res).await;
441        let body_str = std::str::from_utf8(&body).unwrap();
442        let actual_path = Path::new(&body_str);
443        let expected_path = Path::new("actix-files/tests");
444        assert!(
445            actual_path.ends_with(expected_path),
446            "body {:?} does not end with {:?}",
447            actual_path,
448            expected_path
449        );
450    }
451}