Reimplement basic static (+handlebars) file hosting with actix-web
This commit is contained in:
		
							parent
							
								
									26ec66d03e
								
							
						
					
					
						commit
						0f736978f3
					
				
					 6 changed files with 601 additions and 2393 deletions
				
			
		
							
								
								
									
										2741
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2741
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										13
									
								
								Cargo.toml
									
										
									
									
									
								
							
							
						
						
									
										13
									
								
								Cargo.toml
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,13 +1,10 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "tlaternet-webserver"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
authors = ["Tristan Daniël Maat <tm@tlater.net>"]
 | 
			
		||||
edition = "2018"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
check-if-email-exists = "0.8.5"
 | 
			
		||||
lettre = "0.9.3"
 | 
			
		||||
lettre_email = "0.9.3"
 | 
			
		||||
rocket = "0.4.4"
 | 
			
		||||
rocket_contrib = { version = "0.4.4", features = [ "handlebars_templates", "serve" ] }
 | 
			
		||||
serde = { version = "1.0.111", features = [ "derive" ] }
 | 
			
		||||
actix-files = "0.6.2"
 | 
			
		||||
actix-web = "4.1.0"
 | 
			
		||||
clap = { version = "3.2.17", features = ["derive"] }
 | 
			
		||||
handlebars = { version = "4.3.3", features = ["dir_source"] }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,38 +0,0 @@
 | 
			
		|||
use rocket::request::FlashMessage;
 | 
			
		||||
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
#[derive(Default, Serialize)]
 | 
			
		||||
pub(crate) struct Context {
 | 
			
		||||
    pub flash: Option<Flash>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)] // Want to keep all possible flash types
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
#[serde(rename_all = "lowercase", tag = "type", content = "message")]
 | 
			
		||||
pub(crate) enum Flash {
 | 
			
		||||
    Primary(String),
 | 
			
		||||
    Secondary(String),
 | 
			
		||||
    Success(String),
 | 
			
		||||
    Danger(String),
 | 
			
		||||
    Warning(String),
 | 
			
		||||
    Info(String),
 | 
			
		||||
    Light(String),
 | 
			
		||||
    Dark(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<FlashMessage<'_, '_>> for Flash {
 | 
			
		||||
    fn from(message: FlashMessage) -> Self {
 | 
			
		||||
        match message.name() {
 | 
			
		||||
            "success" => Self::Success(message.msg().to_string()),
 | 
			
		||||
            "warning" => Self::Warning(message.msg().to_string()),
 | 
			
		||||
            "error" => Self::Danger(message.msg().to_string()),
 | 
			
		||||
            "primary" => Self::Primary(message.msg().to_string()),
 | 
			
		||||
            "secondary" => Self::Secondary(message.msg().to_string()),
 | 
			
		||||
            "info" => Self::Info(message.msg().to_string()),
 | 
			
		||||
            "light" => Self::Light(message.msg().to_string()),
 | 
			
		||||
            "dark" => Self::Dark(message.msg().to_string()),
 | 
			
		||||
            name => Self::Info(format!("{}: {}", name, message.msg())),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								src/mail.rs
									
										
									
									
									
								
							
							
						
						
									
										50
									
								
								src/mail.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,50 +0,0 @@
 | 
			
		|||
use std::fs::create_dir_all;
 | 
			
		||||
 | 
			
		||||
use lettre::{FileTransport, Transport};
 | 
			
		||||
use lettre_email::EmailBuilder;
 | 
			
		||||
use rocket::request::{Form, FromForm};
 | 
			
		||||
use rocket::response::{Flash, Redirect};
 | 
			
		||||
use rocket::{post, routes, Route};
 | 
			
		||||
 | 
			
		||||
#[derive(FromForm)]
 | 
			
		||||
struct Email {
 | 
			
		||||
    mail: String,
 | 
			
		||||
    subject: String,
 | 
			
		||||
    message: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn send_mail(email: &Email) -> Result<(), String> {
 | 
			
		||||
    let email = EmailBuilder::new()
 | 
			
		||||
        .to("tm@tlater.net")
 | 
			
		||||
        .from(email.mail.clone())
 | 
			
		||||
        .subject(email.subject.clone())
 | 
			
		||||
        .text(email.message.clone())
 | 
			
		||||
        .build()
 | 
			
		||||
        .map_err(|err| format!("Invalid email contents: {}", err))?;
 | 
			
		||||
 | 
			
		||||
    let mut mailer = FileTransport::new("/srv/mail");
 | 
			
		||||
    if let Err(error) = create_dir_all("/srv/mail") {
 | 
			
		||||
        println!("Could not create mail directory: {}", error);
 | 
			
		||||
    };
 | 
			
		||||
    mailer.send(email.into()).map_err(|err| {
 | 
			
		||||
        println!("Could not save mail: {}", err);
 | 
			
		||||
        "Failed to send email due to internal issues; please try again later".to_string()
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[post("/mail.html", data = "<email>")]
 | 
			
		||||
fn mail_post(email: Form<Email>) -> Result<Flash<Redirect>, Flash<Redirect>> {
 | 
			
		||||
    match send_mail(&email) {
 | 
			
		||||
        Ok(_) => Ok(Flash::success(
 | 
			
		||||
            Redirect::to("/mail.html"),
 | 
			
		||||
            "Email sent successfully",
 | 
			
		||||
        )),
 | 
			
		||||
        Err(err) => Err(Flash::error(Redirect::to("/mail.html"), err)),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn routes() -> Vec<Route> {
 | 
			
		||||
    routes![mail_post]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										74
									
								
								src/main.rs
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,34 +1,46 @@
 | 
			
		|||
#![feature(proc_macro_hygiene, decl_macro)]
 | 
			
		||||
use std::io;
 | 
			
		||||
 | 
			
		||||
use rocket::fairing::AdHoc;
 | 
			
		||||
use actix_files::Files;
 | 
			
		||||
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder};
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use handlebars::Handlebars;
 | 
			
		||||
 | 
			
		||||
use rocket_contrib::serve::StaticFiles;
 | 
			
		||||
use rocket_contrib::templates::Template;
 | 
			
		||||
 | 
			
		||||
mod context;
 | 
			
		||||
mod mail;
 | 
			
		||||
mod static_templates;
 | 
			
		||||
 | 
			
		||||
use mail::routes as mail_routes;
 | 
			
		||||
use static_templates::routes as static_templates;
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    rocket::ignite()
 | 
			
		||||
        .attach(Template::fairing())
 | 
			
		||||
        .mount("/", mail_routes())
 | 
			
		||||
        .mount("/", static_templates())
 | 
			
		||||
        .attach(AdHoc::on_attach("Static files config", |rocket| {
 | 
			
		||||
            let static_path = match rocket.config().get_string("template_dir") {
 | 
			
		||||
                Ok(dir) => dir,
 | 
			
		||||
                Err(rocket::config::ConfigError::Missing { .. }) => "templates".to_string(),
 | 
			
		||||
                Err(err) => {
 | 
			
		||||
                    eprintln!("Error reading configuration: {}", err);
 | 
			
		||||
                    eprintln!("Using default templates path.");
 | 
			
		||||
                    "templates".to_string()
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            Ok(rocket.mount("/", StaticFiles::from(static_path)))
 | 
			
		||||
        }))
 | 
			
		||||
        .launch();
 | 
			
		||||
#[derive(Parser, Debug)]
 | 
			
		||||
struct Config {
 | 
			
		||||
    template_directory: String,
 | 
			
		||||
    address: String,
 | 
			
		||||
    port: u16,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn template(hb: web::Data<Handlebars<'_>>, req: HttpRequest) -> impl Responder {
 | 
			
		||||
    let path: String = req
 | 
			
		||||
        .match_info()
 | 
			
		||||
        .query("filename")
 | 
			
		||||
        .parse()
 | 
			
		||||
        .expect("need a file name on the request");
 | 
			
		||||
 | 
			
		||||
    let body = hb.render(path.strip_suffix(".html").unwrap(), &()).unwrap();
 | 
			
		||||
 | 
			
		||||
    HttpResponse::Ok().body(body)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[actix_web::main]
 | 
			
		||||
async fn main() -> io::Result<()> {
 | 
			
		||||
    let config = Config::parse();
 | 
			
		||||
 | 
			
		||||
    let mut handlebars = Handlebars::new();
 | 
			
		||||
    handlebars
 | 
			
		||||
        .register_templates_directory(".html", config.template_directory.clone())
 | 
			
		||||
        .expect("could not read template directory");
 | 
			
		||||
    let handlebars_ref = web::Data::new(handlebars);
 | 
			
		||||
 | 
			
		||||
    HttpServer::new(move || {
 | 
			
		||||
        App::new()
 | 
			
		||||
            .app_data(handlebars_ref.clone())
 | 
			
		||||
            .route("/{filename:.*.html}", web::get().to(template))
 | 
			
		||||
            .service(Files::new("/", config.template_directory.clone()))
 | 
			
		||||
    })
 | 
			
		||||
    .bind((config.address, config.port))?
 | 
			
		||||
    .run()
 | 
			
		||||
    .await
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,78 +0,0 @@
 | 
			
		|||
/// Serve templates "statically".
 | 
			
		||||
///
 | 
			
		||||
/// In the generic case, we don't want to do any processing on
 | 
			
		||||
/// template pages. They should be served "statically", but we want to
 | 
			
		||||
/// transform the bits of default handlebars templating - currently
 | 
			
		||||
/// setting the flash to nothing.
 | 
			
		||||
///
 | 
			
		||||
/// This module implements a catchall route for this purpose.
 | 
			
		||||
use rocket::request::FlashMessage;
 | 
			
		||||
use std::ffi::OsString;
 | 
			
		||||
 | 
			
		||||
use rocket::http::uri::Segments;
 | 
			
		||||
use rocket::request::FromSegments;
 | 
			
		||||
use rocket::response::status;
 | 
			
		||||
use rocket::{get, routes, Route};
 | 
			
		||||
use rocket_contrib::templates::Template;
 | 
			
		||||
 | 
			
		||||
use crate::context::Context;
 | 
			
		||||
 | 
			
		||||
pub struct HTMLPage {
 | 
			
		||||
    pub page: OsString,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> FromSegments<'a> for HTMLPage {
 | 
			
		||||
    type Error = &'static str;
 | 
			
		||||
 | 
			
		||||
    fn from_segments(segments: Segments<'a>) -> Result<Self, Self::Error> {
 | 
			
		||||
        let page = segments
 | 
			
		||||
            .into_path_buf(false)
 | 
			
		||||
            .map_err(|_| "Invalid segments")?;
 | 
			
		||||
 | 
			
		||||
        page.extension()
 | 
			
		||||
            .map(|extension| {
 | 
			
		||||
                if extension == "html" {
 | 
			
		||||
                    Some(HTMLPage {
 | 
			
		||||
                        page: page
 | 
			
		||||
                            .file_stem()
 | 
			
		||||
                            .expect("Should exist if the extension does")
 | 
			
		||||
                            .to_os_string(),
 | 
			
		||||
                    })
 | 
			
		||||
                } else {
 | 
			
		||||
                    None
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .flatten()
 | 
			
		||||
            .ok_or("Invalid page name")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/")]
 | 
			
		||||
pub fn static_index() -> Template {
 | 
			
		||||
    Template::render("index", Context::default())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/<path..>")]
 | 
			
		||||
pub fn static_templates(
 | 
			
		||||
    path: HTMLPage,
 | 
			
		||||
    flash: Option<FlashMessage>,
 | 
			
		||||
) -> Result<Template, status::BadRequest<String>> {
 | 
			
		||||
    let path = path.page.into_string().map_err(|path| {
 | 
			
		||||
        status::BadRequest(Some(format!("Invalid path: {}", path.to_string_lossy())))
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    if let Some(flash) = flash {
 | 
			
		||||
        Ok(Template::render(
 | 
			
		||||
            path,
 | 
			
		||||
            Context {
 | 
			
		||||
                flash: Some(flash.into()),
 | 
			
		||||
            },
 | 
			
		||||
        ))
 | 
			
		||||
    } else {
 | 
			
		||||
        Ok(Template::render(path, Context::default()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn routes() -> Vec<Route> {
 | 
			
		||||
    routes![static_index, static_templates]
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue