actix_web_lab/
spa.rs

1use std::borrow::Cow;
2
3use actix_files::{Files, NamedFile};
4use actix_service::fn_service;
5use actix_web::dev::{HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse};
6use tracing::trace;
7
8/// Single Page App (SPA) service builder.
9///
10/// # Examples
11///
12/// ```no_run
13/// # use actix_web::App;
14/// # use actix_web_lab::web::spa;
15/// App::new()
16///     // ...api routes...
17///     .service(
18///         spa()
19///             .index_file("./examples/assets/spa.html")
20///             .static_resources_mount("/static")
21///             .static_resources_location("./examples/assets")
22///             .finish(),
23///     )
24/// # ;
25/// ```
26#[derive(Debug, Clone)]
27pub struct Spa {
28    index_file: Cow<'static, str>,
29    static_resources_mount: Cow<'static, str>,
30    static_resources_location: Cow<'static, str>,
31}
32
33impl Spa {
34    /// Location of the SPA index file.
35    ///
36    /// This file will be served if:
37    /// - the Actix Web router has reached this service, indicating that none of the API routes
38    ///   matched the URL path;
39    /// - and none of the static resources handled matched.
40    ///
41    /// The default is "./index.html". I.e., the `index.html` file located in the directory that
42    /// the server is running from.
43    pub fn index_file(mut self, index_file: impl Into<Cow<'static, str>>) -> Self {
44        self.index_file = index_file.into();
45        self
46    }
47
48    /// The URL path prefix that static files should be served from.
49    ///
50    /// The default is "/". I.e., static files are served from the root URL path.
51    pub fn static_resources_mount(
52        mut self,
53        static_resources_mount: impl Into<Cow<'static, str>>,
54    ) -> Self {
55        self.static_resources_mount = static_resources_mount.into();
56        self
57    }
58
59    /// The location in the filesystem to serve static resources from.
60    ///
61    /// The default is "./". I.e., static files are located in the directory the server is
62    /// running from.
63    pub fn static_resources_location(
64        mut self,
65        static_resources_location: impl Into<Cow<'static, str>>,
66    ) -> Self {
67        self.static_resources_location = static_resources_location.into();
68        self
69    }
70
71    /// Constructs the service for use in a `.service()` call.
72    pub fn finish(self) -> impl HttpServiceFactory {
73        let index_file = self.index_file.into_owned();
74        let static_resources_location = self.static_resources_location.into_owned();
75        let static_resources_mount = self.static_resources_mount.into_owned();
76
77        let files = {
78            let index_file = index_file.clone();
79            Files::new(&static_resources_mount, static_resources_location)
80                // HACK: FilesService will try to read a directory listing unless index_file is provided
81                // FilesService will fail to load the index_file and will then call our default_handler
82                .index_file("extremely-unlikely-to-exist-!@$%^&*.txt")
83                .default_handler(move |req| serve_index(req, index_file.clone()))
84        };
85
86        SpaService { index_file, files }
87    }
88}
89
90#[derive(Debug)]
91struct SpaService {
92    index_file: String,
93    files: Files,
94}
95
96impl HttpServiceFactory for SpaService {
97    fn register(self, config: &mut actix_web::dev::AppService) {
98        // let Files register its mount path as-is
99        self.files.register(config);
100
101        // also define a root prefix handler directed towards our SPA index
102        let rdef = ResourceDef::root_prefix("");
103        config.register_service(
104            rdef,
105            None,
106            fn_service(move |req| serve_index(req, self.index_file.clone())),
107            None,
108        );
109    }
110}
111
112async fn serve_index(
113    req: ServiceRequest,
114    index_file: String,
115) -> Result<ServiceResponse, actix_web::Error> {
116    trace!("serving default SPA page");
117    let (req, _) = req.into_parts();
118    let file = NamedFile::open_async(&index_file).await?;
119    let res = file.into_response(&req);
120    Ok(ServiceResponse::new(req, res))
121}
122
123impl Default for Spa {
124    fn default() -> Self {
125        Self {
126            index_file: Cow::Borrowed("./index.html"),
127            static_resources_mount: Cow::Borrowed("/"),
128            static_resources_location: Cow::Borrowed("./"),
129        }
130    }
131}