diff --git a/crates/server/src/handlers/assets.rs b/crates/server/src/handlers/assets.rs index d7cf18ff..6a6a1537 100644 --- a/crates/server/src/handlers/assets.rs +++ b/crates/server/src/handlers/assets.rs @@ -11,7 +11,7 @@ use std::io::{Error, ErrorKind}; /// like "public/{uri}/index.html" and "public/{uri}.html". /// /// If no file is present, it will try to get a default "public/404.html" -pub async fn handle_assets(req: &HttpRequest) -> Result { +pub async fn handle_assets(req: HttpRequest) -> Result { let root_path = &req .app_data::>() .expect("error fetching app data") @@ -25,7 +25,7 @@ pub async fn handle_assets(req: &HttpRequest) -> Result { // Same as before, but the file is located at ./about.html let html_ext_path = root_path.join(format!("public{uri_path}.html")); - if file_path.exists() { + if file_path.exists() && !uri_path.is_empty() && uri_path != "/" { NamedFile::open_async(file_path).await } else if uri_path.ends_with('/') && index_folder_path.exists() { NamedFile::open_async(index_folder_path).await diff --git a/crates/server/src/handlers/worker.rs b/crates/server/src/handlers/worker.rs index daadd1de..eca01928 100644 --- a/crates/server/src/handlers/worker.rs +++ b/crates/server/src/handlers/worker.rs @@ -1,7 +1,7 @@ -// Copyright 2022 VMware, Inc. +// Copyright 2022-2023 VMware, Inc. // SPDX-License-Identifier: Apache-2.0 -use super::{assets::handle_assets, not_found::handle_not_found}; +use super::not_found::handle_not_found; use crate::{AppData, DataConnectors}; use actix_web::{ http::StatusCode, @@ -42,16 +42,8 @@ pub async fn handle_worker(req: HttpRequest, body: Bytes) -> HttpResponse { // First, we need to identify the best suited route let selected_route = app_data.routes.retrieve_best_route(req.path()); - if let Some(route) = selected_route { - // First, check if there's an existing static file. Static assets have more priority - // than dynamic routes. However, I cannot set the static assets as the first service - // as it's captures everything. - if route.is_dynamic() { - if let Ok(existing_file) = handle_assets(&req).await { - return existing_file.into_response(&req); - } - } + if let Some(route) = selected_route { let workers = WORKERS .read() .expect("error locking worker lock for reading"); diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index a09fd16f..08fc5f2a 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,21 +1,20 @@ -// Copyright 2022 VMware, Inc. +// Copyright 2022-2023 VMware, Inc. // SPDX-License-Identifier: Apache-2.0 mod errors; -use errors::{Result, ServeError}; - mod handlers; +mod static_assets; -use actix_files::Files; -use actix_web::dev::{fn_service, Server, ServiceRequest, ServiceResponse}; +use actix_web::dev::Server; use actix_web::{ middleware, web::{self, Data}, App, HttpServer, }; +use errors::{Result, ServeError}; use handlers::assets::handle_assets; -use handlers::not_found::handle_not_found; use handlers::worker::handle_worker; +use static_assets::StaticAssets; use std::{path::PathBuf, sync::RwLock}; use wws_api_manage::config_manage_api_handlers; use wws_data_kv::KV; @@ -75,11 +74,38 @@ impl From for AppData { /// assets and workers. pub async fn serve(serve_options: ServeOptions) -> Result { // Initializes the data connectors. For now, just KV - let data_connectors = Data::new(RwLock::new(DataConnectors::default())); + let mut data = DataConnectors::default(); let (hostname, port) = (serve_options.hostname.clone(), serve_options.port); let serve_options = serve_options.clone(); + let workers = WORKERS + .read() + .expect("error locking worker lock for reading"); + + // Configure the KV store when required + for route in serve_options.base_routes.routes.iter() { + let worker = workers + .get(&route.worker) + .expect("unexpected missing worker"); + + // Configure KV + if let Some(namespace) = worker.config.data_kv_namespace() { + data.kv.create_store(&namespace); + } + } + + // Pre-create the KV namespaces + let data_connectors = Data::new(RwLock::new(data)); + + // Static assets + let mut static_assets = + StaticAssets::new(&serve_options.root_path, &serve_options.base_routes.prefix); + static_assets + .load() + .expect("Error loading the static assets"); + + // Build the actix server with all the configuration let server = HttpServer::new(move || { // Initializes the app data for handlers let app_data: Data = Data::new( @@ -101,55 +127,13 @@ pub async fn serve(serve_options: ServeOptions) -> Result { app = app.configure(config_manage_api_handlers); } - let workers = WORKERS - .read() - .expect("error locking worker lock for reading"); - - // Append routes to the current service - for route in app_data.routes.iter() { - app = app.service(web::resource(route.actix_path()).to(handle_worker)); - - let worker = workers - .get(&route.worker) - .expect("unexpected missing worker"); - - // Configure KV - if let Some(namespace) = worker.config.data_kv_namespace() { - data_connectors - .write() - .expect("cannot retrieve shared data") - .kv - .create_store(&namespace); - } + // Mount static assets + for actix_path in static_assets.paths.iter() { + app = app.route(actix_path, web::get().to(handle_assets)); } - // Serve static files from the static folder - let mut static_prefix = app_data.routes.prefix.clone(); - if static_prefix.is_empty() { - static_prefix = String::from("/"); - } - - app = app.service( - Files::new(&static_prefix, app_data.root_path.join("public")) - .index_file("index.html") - // This handler check if there's an HTML file in the public folder that - // can reply to the given request. For example, if someone request /about, - // this handler will look for a /public/about.html file. - .default_handler(fn_service(|req: ServiceRequest| async { - let (req, _) = req.into_parts(); - - match handle_assets(&req).await { - Ok(existing_file) => { - let res = existing_file.into_response(&req); - Ok(ServiceResponse::new(req, res)) - } - Err(_) => { - let res = handle_not_found(&req).await; - Ok(ServiceResponse::new(req, res)) - } - } - })), - ); + // Default all other routes to the Wasm handler + app = app.default_service(web::route().to(handle_worker)); app }) diff --git a/crates/server/src/static_assets.rs b/crates/server/src/static_assets.rs new file mode 100644 index 00000000..8875b500 --- /dev/null +++ b/crates/server/src/static_assets.rs @@ -0,0 +1,99 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + ffi::OsStr, + io::Error as IoError, + path::{Path, PathBuf}, +}; + +/// Folder that contains the static assets in a wws project +pub const STATIC_ASSETS_FOLDER: &str = "public"; + +/// Load and stores the information of static assets +/// in Wasm Workers Server. It enables to manually set +/// the list of routes in actix. +#[derive(Default)] +pub struct StaticAssets { + /// Static assets folder + folder: PathBuf, + /// The initial prefix to mount the static assets + prefix: String, + /// List of local paths to set in actix + pub paths: Vec, +} + +impl StaticAssets { + /// Creates a new instance by looking at the public + /// folder if exists. + pub fn new(root_path: &Path, prefix: &str) -> Self { + Self { + folder: root_path.join(STATIC_ASSETS_FOLDER), + prefix: prefix.to_string(), + paths: Vec::new(), + } + } + + /// Load the assets in the public folder. + pub fn load(&mut self) -> Result<(), IoError> { + if self.folder.exists() { + // Set the provided prefix + let prefix = self.prefix.clone(); + + self.load_folder(&self.folder.clone(), &prefix)?; + } + + Ok(()) + } + + /// Load the assets from a specific folder + fn load_folder(&mut self, folder: &Path, prefix: &str) -> Result<(), IoError> { + let paths = folder.read_dir()?; + + for path in paths { + let path = path?.path(); + + if path.is_dir() { + let folder_path = path + .file_stem() + .expect("Error reading the file stem from a static file") + .to_string_lossy(); + + let new_prefix = format!("{}/{}", prefix, folder_path); + // Recursive + self.load_folder(&path, &new_prefix)?; + } else { + // Save the static file + match path.extension() { + Some(ext) if ext == OsStr::new("html") => { + let stem = path + .file_stem() + .expect("Error reading the file name from a static file") + .to_string_lossy(); + + // Add the full file path + self.paths.push(format!("{prefix}/{stem}.html")); + + if stem == "index" { + // For index files, mount it on the prefix (folder) + self.paths.push(format!("{prefix}/")); + } else { + // Mount it without the .html (pretty routes) + self.paths.push(format!("{prefix}/{stem}")); + } + } + _ => { + let name = path + .file_name() + .expect("Error reading the file name from a static file") + .to_string_lossy(); + + self.paths.push(format!("{prefix}/{name}")); + } + } + } + } + + Ok(()) + } +}