1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use std::borrow::Cow;

use actix_files::{Files, NamedFile};
use actix_service::fn_service;
use actix_web::dev::{HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse};

/// Single Page App (SPA) service builder.
///
/// # Examples
/// ```
/// # use actix_web::App;
/// # use actix_web_lab::web::spa;
/// let app = App::new()
///     // ...api routes...
///     .service(
///         spa()
///             .index_file("./examples/assets/spa.html")
///             .static_resources_mount("/static")
///             .static_resources_location("./examples/assets")
///             .finish()
///     );
/// ```
#[derive(Debug, Clone)]
pub struct Spa {
    index_file: Cow<'static, str>,
    static_resources_mount: Cow<'static, str>,
    static_resources_location: Cow<'static, str>,
}

impl Spa {
    /// Location of the SPA index file.
    ///
    /// This file will be served if:
    /// - the Actix Web router has reached this service, indicating that none of the API routes
    ///   matched the URL path;
    /// - and none of the static resources handled matched.
    ///
    /// The default is "./index.html". I.e., the `index.html` file located in the directory that
    /// the server is running from.
    pub fn index_file(mut self, index_file: impl Into<Cow<'static, str>>) -> Self {
        self.index_file = index_file.into();
        self
    }

    /// The URL path prefix that static files should be served from.
    ///
    /// The default is "/". I.e., static files are served from the root URL path.
    pub fn static_resources_mount(
        mut self,
        static_resources_mount: impl Into<Cow<'static, str>>,
    ) -> Self {
        self.static_resources_mount = static_resources_mount.into();
        self
    }

    /// The location in the filesystem to serve static resources from.
    ///
    /// The default is "./". I.e., static files are located in the directory the server is
    /// running from.
    pub fn static_resources_location(
        mut self,
        static_resources_location: impl Into<Cow<'static, str>>,
    ) -> Self {
        self.static_resources_location = static_resources_location.into();
        self
    }

    /// Constructs the service for use in a `.service()` call.
    pub fn finish(self) -> impl HttpServiceFactory {
        let index_file = self.index_file.into_owned();
        let static_resources_location = self.static_resources_location.into_owned();
        let static_resources_mount = self.static_resources_mount.into_owned();

        let files = {
            let index_file = index_file.clone();
            Files::new(&static_resources_mount, static_resources_location)
                // HACK: FilesService will try to read a directory listing unless index_file is provided
                // FilesService will fail to load the index_file and will then call our default_handler
                .index_file("extremely-unlikely-to-exist-!@$%^&*.txt")
                .default_handler(move |req| serve_index(req, index_file.clone()))
        };

        SpaService { index_file, files }
    }
}

#[derive(Debug)]
struct SpaService {
    index_file: String,
    files: Files,
}

impl HttpServiceFactory for SpaService {
    fn register(self, config: &mut actix_web::dev::AppService) {
        // let Files register its mount path as-is
        self.files.register(config);

        // also define a root prefix handler directed towards our SPA index
        let rdef = ResourceDef::root_prefix("");
        config.register_service(
            rdef,
            None,
            fn_service(move |req| serve_index(req, self.index_file.clone())),
            None,
        );
    }
}

async fn serve_index(
    req: ServiceRequest,
    index_file: String,
) -> Result<ServiceResponse, actix_web::Error> {
    log::trace!("serving default SPA page");
    let (req, _) = req.into_parts();
    let file = NamedFile::open_async(&index_file).await?;
    let res = file.into_response(&req);
    Ok(ServiceResponse::new(req, res))
}

impl Default for Spa {
    fn default() -> Self {
        Self {
            index_file: Cow::Borrowed("./index.html"),
            static_resources_mount: Cow::Borrowed("/"),
            static_resources_location: Cow::Borrowed("./"),
        }
    }
}