Skip to main content

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