From 26e9c806264447d15bb71ad75da1b78a254f9aef Mon Sep 17 00:00:00 2001
From: Ibraheem Ahmed <ibrah1440@gmail.com>
Date: Sun, 18 Apr 2021 18:34:51 -0400
Subject: [PATCH] Named file service (#2135)

---
 actix-files/src/files.rs     | 12 ++++++
 actix-files/src/lib.rs       | 74 +++++++++++++++++++++++++++++++++
 actix-files/src/named.rs     | 80 +++++++++++++++++++++++++++++++++++-
 awc/tests/test_ssl_client.rs |  2 +-
 4 files changed, 165 insertions(+), 3 deletions(-)

diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs
index ff424134..9b24c4b2 100644
--- a/actix-files/src/files.rs
+++ b/actix-files/src/files.rs
@@ -199,6 +199,18 @@ impl Files {
     }
 
     /// Sets default handler which is used when no matched file could be found.
+    ///
+    /// For example, you could set a fall back static file handler:
+    /// ```rust
+    /// use actix_files::{Files, NamedFile};
+    ///
+    /// # fn run() -> Result<(), actix_web::Error> {
+    /// let files = Files::new("/", "./static")
+    ///     .index_file("index.html")
+    ///     .default_handler(NamedFile::open("./static/404.html")?);
+    /// # Ok(())
+    /// # }
+    /// ```
     pub fn default_handler<F, U>(mut self, f: F) -> Self
     where
         F: IntoServiceFactory<U, ServiceRequest>,
diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs
index e9b55e87..34b02581 100644
--- a/actix-files/src/lib.rs
+++ b/actix-files/src/lib.rs
@@ -755,6 +755,80 @@ mod tests {
         assert_eq!(res.status(), StatusCode::OK);
     }
 
+    #[actix_rt::test]
+    async fn test_serve_named_file() {
+        let srv =
+            test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap()))
+                .await;
+
+        let req = TestRequest::get().uri("/Cargo.toml").to_request();
+        let res = test::call_service(&srv, req).await;
+        assert_eq!(res.status(), StatusCode::OK);
+
+        let bytes = test::read_body(res).await;
+        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
+        assert_eq!(bytes, data);
+
+        let req = TestRequest::get().uri("/test/unknown").to_request();
+        let res = test::call_service(&srv, req).await;
+        assert_eq!(res.status(), StatusCode::NOT_FOUND);
+    }
+
+    #[actix_rt::test]
+    async fn test_serve_named_file_prefix() {
+        let srv = test::init_service(
+            App::new()
+                .service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())),
+        )
+        .await;
+
+        let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
+        let res = test::call_service(&srv, req).await;
+        assert_eq!(res.status(), StatusCode::OK);
+
+        let bytes = test::read_body(res).await;
+        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
+        assert_eq!(bytes, data);
+
+        let req = TestRequest::get().uri("/Cargo.toml").to_request();
+        let res = test::call_service(&srv, req).await;
+        assert_eq!(res.status(), StatusCode::NOT_FOUND);
+    }
+
+    #[actix_rt::test]
+    async fn test_named_file_default_service() {
+        let srv = test::init_service(
+            App::new().default_service(NamedFile::open("Cargo.toml").unwrap()),
+        )
+        .await;
+
+        for route in ["/foobar", "/baz", "/"].iter() {
+            let req = TestRequest::get().uri(route).to_request();
+            let res = test::call_service(&srv, req).await;
+            assert_eq!(res.status(), StatusCode::OK);
+
+            let bytes = test::read_body(res).await;
+            let data = Bytes::from(fs::read("Cargo.toml").unwrap());
+            assert_eq!(bytes, data);
+        }
+    }
+
+    #[actix_rt::test]
+    async fn test_default_handler_named_file() {
+        let st = Files::new("/", ".")
+            .default_handler(NamedFile::open("Cargo.toml").unwrap())
+            .new_service(())
+            .await
+            .unwrap();
+        let req = TestRequest::with_uri("/missing").to_srv_request();
+        let resp = test::call_service(&st, req).await;
+
+        assert_eq!(resp.status(), StatusCode::OK);
+        let bytes = test::read_body(resp).await;
+        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
+        assert_eq!(bytes, data);
+    }
+
     #[actix_rt::test]
     async fn test_symlinks() {
         let srv = test::init_service(App::new().service(Files::new("test", "."))).await;
diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs
index 2846646a..519234f0 100644
--- a/actix-files/src/named.rs
+++ b/actix-files/src/named.rs
@@ -1,3 +1,6 @@
+use actix_service::{Service, ServiceFactory};
+use actix_utils::future::{ok, ready, Ready};
+use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef};
 use std::fs::{File, Metadata};
 use std::io;
 use std::ops::{Deref, DerefMut};
@@ -8,14 +11,14 @@ use std::time::{SystemTime, UNIX_EPOCH};
 use std::os::unix::fs::MetadataExt;
 
 use actix_web::{
-    dev::{BodyEncoding, SizedStream},
+    dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream},
     http::{
         header::{
             self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
         },
         ContentEncoding, StatusCode,
     },
-    HttpMessage, HttpRequest, HttpResponse, Responder,
+    Error, HttpMessage, HttpRequest, HttpResponse, Responder,
 };
 use bitflags::bitflags;
 use mime_guess::from_path;
@@ -39,6 +42,29 @@ impl Default for Flags {
 }
 
 /// A file with an associated name.
+///
+/// `NamedFile` can be registered as services:
+/// ```
+/// use actix_web::App;
+/// use actix_files::NamedFile;
+///
+/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
+/// let app = App::new()
+///     .service(NamedFile::open("./static/index.html")?);
+/// # Ok(())
+/// # }
+/// ```
+///
+/// They can also be returned from handlers:
+/// ```
+/// use actix_web::{Responder, get};
+/// use actix_files::NamedFile;
+///
+/// #[get("/")]
+/// async fn index() -> impl Responder {
+///     NamedFile::open("./static/index.html")
+/// }
+/// ```
 #[derive(Debug)]
 pub struct NamedFile {
     path: PathBuf,
@@ -480,3 +506,53 @@ impl Responder for NamedFile {
         self.into_response(req)
     }
 }
+
+impl ServiceFactory<ServiceRequest> for NamedFile {
+    type Response = ServiceResponse;
+    type Error = Error;
+    type Config = ();
+    type InitError = ();
+    type Service = NamedFileService;
+    type Future = Ready<Result<Self::Service, ()>>;
+
+    fn new_service(&self, _: ()) -> Self::Future {
+        ok(NamedFileService {
+            path: self.path.clone(),
+        })
+    }
+}
+
+#[doc(hidden)]
+#[derive(Debug)]
+pub struct NamedFileService {
+    path: PathBuf,
+}
+
+impl Service<ServiceRequest> for NamedFileService {
+    type Response = ServiceResponse;
+    type Error = Error;
+    type Future = Ready<Result<Self::Response, Self::Error>>;
+
+    actix_service::always_ready!();
+
+    fn call(&self, req: ServiceRequest) -> Self::Future {
+        let (req, _) = req.into_parts();
+        ready(
+            NamedFile::open(&self.path)
+                .map_err(|e| e.into())
+                .map(|f| f.into_response(&req))
+                .map(|res| ServiceResponse::new(req, res)),
+        )
+    }
+}
+
+impl HttpServiceFactory for NamedFile {
+    fn register(self, config: &mut AppService) {
+        config.register_service(
+            ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()),
+            None,
+            self,
+            None,
+        )
+    }
+}
diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs
index 57305e49..811efd4b 100644
--- a/awc/tests/test_ssl_client.rs
+++ b/awc/tests/test_ssl_client.rs
@@ -7,7 +7,7 @@ use std::sync::Arc;
 
 use actix_http::HttpService;
 use actix_http_test::test_server;
-use actix_service::{map_config, fn_service, ServiceFactoryExt};
+use actix_service::{fn_service, map_config, ServiceFactoryExt};
 use actix_utils::future::ok;
 use actix_web::http::Version;
 use actix_web::{dev::AppConfig, web, App, HttpResponse};