#[macro_use] extern crate actix_web; #[macro_use] extern crate log; #[macro_use] extern crate thiserror; #[macro_use] extern crate serde_json; use std::collections::HashMap; use std::ops::Deref; use std::path::PathBuf; use actix_session::CookieSession; use actix_web::{web, App, HttpResponse, HttpServer}; use actix_web_static_files; use actix_web_static_files::ResourceFiles as StaticFiles; use clap::Arg; use log::LevelFilter; use once_cell::sync::Lazy; use rand::Rng; use tera::Tera; use yopa::Storage; use crate::tera_ext::TeraExt; mod routes; mod session_ext; mod tera_ext; mod utils; // Embed static files include!(concat!(env!("OUT_DIR"), "/static_files.rs")); // Embed templates static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/templates"); pub(crate) static TERA: Lazy = Lazy::new(|| { let mut tera = Tera::default(); tera.add_include_dir_templates(&TEMPLATES).unwrap(); // Special filter for the TypedValue map use serde_json::Value; tera.register_filter( "print_typed_value", |v: &Value, _: &HashMap| -> tera::Result { if v.is_null() { return Ok(v.clone()); } if let Value::Object(map) = v { if let Some((_, v)) = map.iter().next() { return Ok(v.clone()); } } Err(tera::Error::msg("Expected nonenmpty object")) }, ); // opt(checked=foo.is_checked) tera.register_function( "opt", |args: &HashMap| -> tera::Result { if args.len() != 1 { return Err("Expected 1 argument".into()); } match args.iter().nth(0) { Some((name, &Value::Bool(true))) => Ok(Value::String(name.clone())), Some((_, &Value::Bool(false))) => Ok(Value::Null), _ => Err("Expected bool argument".into()), } }, ); // selected(val=foo.color,opt="Red") tera.register_function( "selected", |args: &HashMap| -> tera::Result { match (args.get("val"), args.get("opt")) { (Some(v), Some(w)) => { if v == w { Ok(Value::String("selected".into())) } else { Ok(Value::Null) } } _ => Err("Expected val and opt args".into()), } }, ); // TODO need to inject HttpRequest::url_for() into tera context, but it then can't be accessed by the functions. // tera.register_function("url_for", |args: HashMap| -> tera::Result { // match args.get("name") { // Some(Value::String(s)) => { // let r = // }, // _ => Err("Expected string argument".into()), // } // }); tera }); type YopaStoreWrapper = web::Data>; const DEF_ADDRESS: &str = "127.0.0.1:8080"; #[actix_web::main] async fn main() -> std::io::Result<()> { let def_addr_help = format!("Set custom server address, default {}", DEF_ADDRESS); let app = clap::App::new("yopa") .arg( Arg::with_name("version") .long("version") .short("V") .help("Show version and exit"), ) .arg( Arg::with_name("verbose") .short("v") .multiple(true) .help("Increase verbosity of logging"), ) .arg( Arg::with_name("address") .short("a") .takes_value(true) .help(&def_addr_help), ) .arg( Arg::with_name("json") .long("json") .short("j") .help("Use JSON storage format instead of binary"), ) .arg(Arg::with_name("file").help("Database file to use")); let matches = app.get_matches(); if matches.is_present("version") { println!( "yopa-web {}, using yopa {}", env!("CARGO_PKG_VERSION"), yopa::VERSION ); return Ok(()); } simple_logging::log_to_stderr(match matches.occurrences_of("verbose") { 0 => LevelFilter::Warn, 1 => LevelFilter::Info, 2 => LevelFilter::Debug, _ => LevelFilter::Trace, }); debug!("Load Tera templates"); // Ensure the lazy ref is initialized early (to catch template bugs ASAP) let _ = TERA.deref(); let json = matches.is_present("json"); let file = matches.value_of("file").unwrap_or(if json { "yopa-store.json" } else { "yopa-store.dat" }); let file_path = if file.starts_with('/') { std::env::current_dir()?.join(file) } else { PathBuf::from(file) }; debug!("Using database file: {}", file_path.display()); let store = if json { Storage::new_json(file_path) } else { Storage::new_bincode(file_path) } .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; let yopa_store: YopaStoreWrapper = web::Data::new(tokio::sync::RwLock::new(store)); let mut session_key = [0u8; 32]; rand::thread_rng().fill(&mut session_key); trace!("Cookie session key: {:?}", session_key); let server_address = matches.value_of("address").unwrap_or(DEF_ADDRESS); println!("Starting web interface at http://{}", server_address); HttpServer::new(move || { let static_files = StaticFiles::new("/static", included_static_files()).do_not_resolve_defaults(); debug!("Starting server thread"); App::new() /* Middlewares */ .wrap(CookieSession::signed(&session_key).secure(false)) /* Bind shared objects */ .app_data(yopa_store.clone()) /* Routes */ .service(routes::index) .service(routes::takeout) // .service(routes::models::list) // .service(routes::models::object::create_form) .service(routes::models::object::create) .service(routes::models::object::update_form) .service(routes::models::object::update) .service(routes::models::object::delete) // .service(routes::models::relation::create_form) .service(routes::models::relation::create) .service(routes::models::relation::update_form) .service(routes::models::relation::update) .service(routes::models::relation::delete) // .service(routes::models::property::create_form) .service(routes::models::property::create) .service(routes::models::property::update_form) .service(routes::models::property::update) .service(routes::models::property::delete) // .service(routes::objects::list) .service(routes::objects::create_form) .service(routes::objects::create) .service(routes::objects::detail) .service(routes::objects::update_form) .service(routes::objects::update) .service(routes::objects::delete) // .service(static_files) .default_service(web::to(|| { HttpResponse::NotFound().body("File or endpoint not found") })) }) .bind(server_address)? .run() .await }