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}