Skip to main content

actix_files/
files.rs

1use std::{
2    borrow::Cow,
3    cell::RefCell,
4    ffi::{OsStr, OsString},
5    fmt, io,
6    path::{Path, PathBuf},
7    rc::Rc,
8};
9
10use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
11use actix_web::{
12    dev::{
13        AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, ServiceResponse,
14    },
15    error::Error,
16    guard::Guard,
17    http::header::DispositionType,
18    HttpRequest,
19};
20use futures_core::future::LocalBoxFuture;
21
22use crate::{
23    directory_listing, named,
24    service::{FilesService, FilesServiceInner},
25    Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
26};
27
28/// Static files handling service.
29///
30/// `Files` service must be registered with `App::service()` method.
31///
32/// # Examples
33/// ```
34/// use actix_web::App;
35/// use actix_files::Files;
36///
37/// let app = App::new()
38///     .service(Files::new("/static", "."));
39/// ```
40pub struct Files {
41    mount_path: String,
42    directories: Vec<PathBuf>,
43    index: Option<String>,
44    show_index: bool,
45    redirect_to_slash: bool,
46    with_permanent_redirect: bool,
47    default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
48    renderer: Rc<DirectoryRenderer>,
49    mime_override: Option<Rc<MimeOverride>>,
50    path_filter: Option<Rc<PathFilter>>,
51    file_flags: named::Flags,
52    use_guards: Option<Rc<dyn Guard>>,
53    guards: Vec<Rc<dyn Guard>>,
54    hidden_files: bool,
55    try_compressed: bool,
56    read_mode_threshold: u64,
57}
58
59impl fmt::Debug for Files {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str("Files")
62    }
63}
64
65impl Clone for Files {
66    fn clone(&self) -> Self {
67        Self {
68            directories: self.directories.clone(),
69            index: self.index.clone(),
70            show_index: self.show_index,
71            redirect_to_slash: self.redirect_to_slash,
72            with_permanent_redirect: self.with_permanent_redirect,
73            default: self.default.clone(),
74            renderer: self.renderer.clone(),
75            file_flags: self.file_flags,
76            mount_path: self.mount_path.clone(),
77            mime_override: self.mime_override.clone(),
78            path_filter: self.path_filter.clone(),
79            use_guards: self.use_guards.clone(),
80            guards: self.guards.clone(),
81            hidden_files: self.hidden_files,
82            try_compressed: self.try_compressed,
83            read_mode_threshold: self.read_mode_threshold,
84        }
85    }
86}
87
88/// File serving root directories for [`Files`].
89///
90/// This type is used by [`Files::new`] to accept either one root directory or an ordered
91/// collection of root directories.
92#[derive(Debug)]
93pub struct FilesDirs(Vec<PathBuf>);
94
95impl FilesDirs {
96    fn canonicalize(self) -> Vec<PathBuf> {
97        self.0
98            .into_iter()
99            .map(|orig_dir| match orig_dir.canonicalize() {
100                Ok(canon_dir) => canon_dir,
101                Err(_) => {
102                    log::error!("Specified path is not a directory: {:?}", orig_dir);
103                    // Preserve original path so requests don't fall back to CWD.
104                    orig_dir
105                }
106            })
107            .collect()
108    }
109}
110
111impl From<&Path> for FilesDirs {
112    fn from(dir: &Path) -> Self {
113        Self(vec![dir.into()])
114    }
115}
116
117impl From<&PathBuf> for FilesDirs {
118    fn from(dir: &PathBuf) -> Self {
119        Self(vec![dir.into()])
120    }
121}
122
123impl From<PathBuf> for FilesDirs {
124    fn from(dir: PathBuf) -> Self {
125        Self(vec![dir])
126    }
127}
128
129impl From<&str> for FilesDirs {
130    fn from(dir: &str) -> Self {
131        Self(vec![dir.into()])
132    }
133}
134
135impl From<&String> for FilesDirs {
136    fn from(dir: &String) -> Self {
137        Self(vec![dir.into()])
138    }
139}
140
141impl From<String> for FilesDirs {
142    fn from(dir: String) -> Self {
143        Self(vec![dir.into()])
144    }
145}
146
147impl From<&OsStr> for FilesDirs {
148    fn from(dir: &OsStr) -> Self {
149        Self(vec![dir.into()])
150    }
151}
152
153impl From<OsString> for FilesDirs {
154    fn from(dir: OsString) -> Self {
155        Self(vec![dir.into()])
156    }
157}
158
159impl From<&OsString> for FilesDirs {
160    fn from(dir: &OsString) -> Self {
161        Self(vec![dir.into()])
162    }
163}
164
165impl From<Box<Path>> for FilesDirs {
166    fn from(dir: Box<Path>) -> Self {
167        Self(vec![dir.into()])
168    }
169}
170
171impl From<Cow<'_, Path>> for FilesDirs {
172    fn from(dir: Cow<'_, Path>) -> Self {
173        Self(vec![dir.into()])
174    }
175}
176
177impl<P, const N: usize> From<[P; N]> for FilesDirs
178where
179    P: Into<PathBuf>,
180{
181    fn from(dirs: [P; N]) -> Self {
182        Self(dirs.into_iter().map(Into::into).collect())
183    }
184}
185
186impl<P, const N: usize> From<&[P; N]> for FilesDirs
187where
188    P: Clone + Into<PathBuf>,
189{
190    fn from(dirs: &[P; N]) -> Self {
191        Self(dirs.iter().cloned().map(Into::into).collect())
192    }
193}
194
195impl<P> From<&[P]> for FilesDirs
196where
197    P: Clone + Into<PathBuf>,
198{
199    fn from(dirs: &[P]) -> Self {
200        Self(dirs.iter().cloned().map(Into::into).collect())
201    }
202}
203
204impl<P> From<Vec<P>> for FilesDirs
205where
206    P: Into<PathBuf>,
207{
208    fn from(dirs: Vec<P>) -> Self {
209        Self(dirs.into_iter().map(Into::into).collect())
210    }
211}
212
213impl Files {
214    /// Create new `Files` instance for a specified base directory.
215    ///
216    /// # Argument Order
217    /// The first argument (`mount_path`) is the root URL at which the static files are served.
218    /// For example, `/assets` will serve files at `example.com/assets/...`.
219    ///
220    /// The second argument (`serve_from`) is the location on disk that files are served from. This
221    /// can be a single path or an ordered collection of paths. Relative paths are resolved from the
222    /// current working directory.
223    ///
224    /// When multiple directories are provided, they are checked in order. The first directory that
225    /// can serve the requested path is used.
226    ///
227    /// Directory listings are generated from the first matching directory and are not merged across
228    /// roots. When [`Files::index_file()`] is configured, later roots are searched if an earlier
229    /// matching directory does not contain the index file.
230    ///
231    /// Empty root collections never match files; requests fall through to the default handler, or
232    /// return `404 Not Found` if none is configured.
233    ///
234    /// # Implementation Notes
235    /// If the mount path is set as the root path `/`, services registered after this one will
236    /// be inaccessible. Register more specific handlers and services first.
237    ///
238    /// If a `serve_from` path cannot be canonicalized at startup, an error is logged and the
239    /// original path is preserved. Requests will return `404 Not Found` until the path exists.
240    ///
241    /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
242    /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
243    /// the number of server [workers](actix_web::HttpServer::workers), by default.
244    pub fn new<T: Into<FilesDirs>>(mount_path: &str, serve_from: T) -> Files {
245        Files {
246            mount_path: mount_path.trim_end_matches('/').to_owned(),
247            directories: serve_from.into().canonicalize(),
248            index: None,
249            show_index: false,
250            redirect_to_slash: false,
251            with_permanent_redirect: false,
252            default: Rc::new(RefCell::new(None)),
253            renderer: Rc::new(directory_listing),
254            mime_override: None,
255            path_filter: None,
256            file_flags: named::Flags::default(),
257            use_guards: None,
258            guards: Vec::new(),
259            hidden_files: false,
260            try_compressed: false,
261            read_mode_threshold: 0,
262        }
263    }
264
265    /// Show files listing for directories.
266    ///
267    /// By default show files listing is disabled.
268    ///
269    /// When used with [`Files::index_file()`], files listing is shown as a fallback
270    /// when the index file is not found.
271    pub fn show_files_listing(mut self) -> Self {
272        self.show_index = true;
273        self
274    }
275
276    /// Redirects to a slash-ended path when browsing a directory.
277    ///
278    /// By default never redirect.
279    ///
280    /// When multiple root directories are configured, a matching directory in an earlier root can
281    /// trigger a redirect before later roots are checked for a file at the same path.
282    pub fn redirect_to_slash_directory(mut self) -> Self {
283        self.redirect_to_slash = true;
284        self
285    }
286
287    /// Redirect with permanent redirect status code (308).
288    ///
289    /// By default redirect with temporary redirect status code (307).
290    pub fn with_permanent_redirect(mut self) -> Self {
291        self.with_permanent_redirect = true;
292        self
293    }
294
295    /// Set custom directory renderer.
296    pub fn files_listing_renderer<F>(mut self, f: F) -> Self
297    where
298        for<'r, 's> F:
299            Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
300    {
301        self.renderer = Rc::new(f);
302        self
303    }
304
305    /// Specifies MIME override callback.
306    pub fn mime_override<F>(mut self, f: F) -> Self
307    where
308        F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
309    {
310        self.mime_override = Some(Rc::new(f));
311        self
312    }
313
314    /// Sets path filtering closure.
315    ///
316    /// The path provided to the closure is relative to `serve_from` path.
317    /// You can safely join this path with the `serve_from` path to get the real path.
318    /// However, the real path may not exist since the filter is called before checking path existence.
319    ///
320    /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise,
321    /// `404 Not Found` is returned.
322    ///
323    /// # Examples
324    /// ```
325    /// use std::path::Path;
326    /// use actix_files::Files;
327    ///
328    /// // prevent searching subdirectories and following symlinks
329    /// let files_service = Files::new("/", "./static").path_filter(|path, _| {
330    ///     path.components().count() == 1
331    ///         && Path::new("./static")
332    ///             .join(path)
333    ///             .symlink_metadata()
334    ///             .map(|m| !m.file_type().is_symlink())
335    ///             .unwrap_or(false)
336    /// });
337    /// ```
338    pub fn path_filter<F>(mut self, f: F) -> Self
339    where
340        F: Fn(&Path, &RequestHead) -> bool + 'static,
341    {
342        self.path_filter = Some(Rc::new(f));
343        self
344    }
345
346    /// Sets index file for directory requests.
347    ///
348    /// When a directory is requested, this value is appended to the directory's path on disk.
349    /// Therefore, the index file path is relative to the served directory (the `serve_from` path
350    /// passed to [`Files::new`]) and should not include the `serve_from` prefix.
351    ///
352    /// For example, to serve `./static/index.html` when mounting `Files::new("/", "./static")`,
353    /// configure it as `.index_file("index.html")` (not `.index_file("./static/index.html")`).
354    ///
355    /// If the index file is not found, files listing is shown as a fallback if
356    /// [`Files::show_files_listing()`] is set.
357    pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
358        self.index = Some(index.into());
359        self
360    }
361
362    /// Sets the size threshold that determines file read mode (sync/async).
363    ///
364    /// When a file is smaller than the threshold (bytes), the reader will use synchronous
365    /// (blocking) file reads. For larger files, it switches to async reads to avoid blocking the
366    /// main thread.
367    ///
368    /// Tweaking this value according to your expected usage may lead to significant performance
369    /// gains (or losses in other handlers, if `size` is too high).
370    ///
371    /// Default is 0, meaning all files are read asynchronously.
372    pub fn read_mode_threshold(mut self, size: u64) -> Self {
373        self.read_mode_threshold = size;
374        self
375    }
376
377    /// Specifies whether to use ETag or not.
378    ///
379    /// Default is true.
380    pub fn use_etag(mut self, value: bool) -> Self {
381        self.file_flags.set(named::Flags::ETAG, value);
382        self
383    }
384
385    /// Specifies whether to use Last-Modified or not.
386    ///
387    /// Default is true.
388    pub fn use_last_modified(mut self, value: bool) -> Self {
389        self.file_flags.set(named::Flags::LAST_MD, value);
390        self
391    }
392
393    /// Specifies whether text responses should signal a UTF-8 encoding.
394    ///
395    /// Default is false (but will default to true in a future version).
396    pub fn prefer_utf8(mut self, value: bool) -> Self {
397        self.file_flags.set(named::Flags::PREFER_UTF8, value);
398        self
399    }
400
401    /// Adds a routing guard.
402    ///
403    /// Use this to allow multiple chained file services that respond to strictly different
404    /// properties of a request. Due to the way routing works, if a guard check returns true and the
405    /// request starts being handled by the file service, it will not be able to back-out and try
406    /// the next service, you will simply get a 404 (or 405) error response.
407    ///
408    /// To allow `POST` requests to retrieve files, see [`Files::method_guard()`].
409    ///
410    /// # Examples
411    /// ```
412    /// use actix_web::{guard::Header, App};
413    /// use actix_files::Files;
414    ///
415    /// App::new().service(
416    ///     Files::new("/","/my/site/files")
417    ///         .guard(Header("Host", "example.com"))
418    /// );
419    /// ```
420    pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
421        self.guards.push(Rc::new(guard));
422        self
423    }
424
425    /// Specifies guard to check before fetching directory listings or files.
426    ///
427    /// Note that this guard has no effect on routing; it's main use is to guard on the request's
428    /// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
429    /// See [`Files::guard`] for routing guards.
430    pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
431        self.use_guards = Some(Rc::new(guard));
432        self
433    }
434
435    /// See [`Files::method_guard`].
436    #[doc(hidden)]
437    #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
438    pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
439        self.method_guard(guard)
440    }
441
442    /// Disable `Content-Disposition` header.
443    ///
444    /// By default Content-Disposition` header is enabled.
445    pub fn disable_content_disposition(mut self) -> Self {
446        self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
447        self
448    }
449
450    /// Sets default handler which is used when no matched file could be found.
451    ///
452    /// # Examples
453    /// Setting a fallback static file handler:
454    /// ```
455    /// use actix_files::{Files, NamedFile};
456    /// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
457    ///
458    /// # fn run() -> Result<(), actix_web::Error> {
459    /// let files = Files::new("/", "./static")
460    ///     .index_file("index.html")
461    ///     .default_handler(fn_service(|req: ServiceRequest| async {
462    ///         let (req, _) = req.into_parts();
463    ///         let file = NamedFile::open("./static/404.html")?;
464    ///         let res = file.into_response(&req);
465    ///         Ok(ServiceResponse::new(req, res))
466    ///     }));
467    /// # Ok(())
468    /// # }
469    /// ```
470    pub fn default_handler<F, U>(mut self, f: F) -> Self
471    where
472        F: IntoServiceFactory<U, ServiceRequest>,
473        U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
474            + 'static,
475    {
476        // create and configure default resource
477        self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
478            f.into_factory().map_init_err(|_| ()),
479        )))));
480
481        self
482    }
483
484    /// Enables serving hidden files and directories, allowing a leading dots in url fragments.
485    pub fn use_hidden_files(mut self) -> Self {
486        self.hidden_files = true;
487        self
488    }
489
490    /// Attempts to search for a suitable pre-compressed version of a file on disk before falling
491    /// back to the uncompressed version.
492    ///
493    /// Currently, `.gz`, `.br`, and `.zst` files are supported.
494    pub fn try_compressed(mut self) -> Self {
495        self.try_compressed = true;
496        self
497    }
498}
499
500impl HttpServiceFactory for Files {
501    fn register(mut self, config: &mut AppService) {
502        let guards = if self.guards.is_empty() {
503            None
504        } else {
505            let guards = std::mem::take(&mut self.guards);
506            Some(
507                guards
508                    .into_iter()
509                    .map(|guard| -> Box<dyn Guard> { Box::new(guard) })
510                    .collect::<Vec<_>>(),
511            )
512        };
513
514        if self.default.borrow().is_none() {
515            *self.default.borrow_mut() = Some(config.default_service());
516        }
517
518        let rdef = if config.is_root() {
519            ResourceDef::root_prefix(&self.mount_path)
520        } else {
521            ResourceDef::prefix(&self.mount_path)
522        };
523
524        config.register_service(rdef, guards, self, None)
525    }
526}
527
528impl ServiceFactory<ServiceRequest> for Files {
529    type Response = ServiceResponse;
530    type Error = Error;
531    type Config = ();
532    type Service = FilesService;
533    type InitError = ();
534    type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
535
536    fn new_service(&self, _: ()) -> Self::Future {
537        let mut inner = FilesServiceInner {
538            directories: self.directories.clone(),
539            index: self.index.clone(),
540            show_index: self.show_index,
541            redirect_to_slash: self.redirect_to_slash,
542            default: None,
543            renderer: self.renderer.clone(),
544            mime_override: self.mime_override.clone(),
545            path_filter: self.path_filter.clone(),
546            file_flags: self.file_flags,
547            guards: self.use_guards.clone(),
548            hidden_files: self.hidden_files,
549            try_compressed: self.try_compressed,
550            size_threshold: self.read_mode_threshold,
551            with_permanent_redirect: self.with_permanent_redirect,
552        };
553
554        if let Some(ref default) = *self.default.borrow() {
555            let fut = default.new_service(());
556            Box::pin(async {
557                match fut.await {
558                    Ok(default) => {
559                        inner.default = Some(default);
560                        Ok(FilesService(Rc::new(inner)))
561                    }
562                    Err(_) => Err(()),
563                }
564            })
565        } else {
566            Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
567        }
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use actix_web::{
574        http::StatusCode,
575        test::{self, TestRequest},
576        App, HttpResponse,
577    };
578
579    use super::*;
580
581    #[actix_web::test]
582    async fn custom_files_listing_renderer() {
583        let srv = test::init_service(
584            App::new().service(
585                Files::new("/", "./tests")
586                    .show_files_listing()
587                    .files_listing_renderer(|dir, req| {
588                        Ok(ServiceResponse::new(
589                            req.clone(),
590                            HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()),
591                        ))
592                    }),
593            ),
594        )
595        .await;
596
597        let req = TestRequest::with_uri("/").to_request();
598        let res = test::call_service(&srv, req).await;
599
600        assert_eq!(res.status(), StatusCode::OK);
601        let body = test::read_body(res).await;
602        let body_str = std::str::from_utf8(&body).unwrap();
603        let actual_path = Path::new(&body_str);
604        let expected_path = Path::new("actix-files/tests");
605        assert!(
606            actual_path.ends_with(expected_path),
607            "body {:?} does not end with {:?}",
608            actual_path,
609            expected_path
610        );
611    }
612}