Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: load static assets manually so all other requests rely on wasm_handler #226

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/server/src/handlers/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NamedFile, Error> {
pub async fn handle_assets(req: HttpRequest) -> Result<NamedFile, Error> {
let root_path = &req
.app_data::<Data<AppData>>()
.expect("error fetching app data")
Expand All @@ -25,7 +25,7 @@ pub async fn handle_assets(req: &HttpRequest) -> Result<NamedFile, Error> {
// 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 != "/" {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this new condition to avoid returning the public folder information.

NamedFile::open_async(file_path).await
} else if uri_path.ends_with('/') && index_folder_path.exists() {
NamedFile::open_async(index_folder_path).await
Expand Down
14 changes: 3 additions & 11 deletions crates/server/src/handlers/worker.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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);
}
}

Comment on lines -45 to -53
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're explicitly mounting the static files in certain routes, this is not required anymore.

if let Some(route) = selected_route {
let workers = WORKERS
.read()
.expect("error locking worker lock for reading");
Expand Down
92 changes: 38 additions & 54 deletions crates/server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -75,11 +74,38 @@ impl From<ServeOptions> for AppData {
/// assets and workers.
pub async fn serve(serve_options: ServeOptions) -> Result<Server> {
// 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");

Comment on lines +82 to +106
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted all the initialization befure the HttpServer::new call. Actix runs this block on every thread, so we avoid calculating static assets multiple times.

// Build the actix server with all the configuration
let server = HttpServer::new(move || {
// Initializes the app data for handlers
let app_data: Data<AppData> = Data::new(
Expand All @@ -101,55 +127,13 @@ pub async fn serve(serve_options: ServeOptions) -> Result<Server> {
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
})
Expand Down
99 changes: 99 additions & 0 deletions crates/server/src/static_assets.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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(())
}
}
Loading