You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
242 lines
7.4 KiB
242 lines
7.4 KiB
#[macro_use]
|
|
extern crate actix_web;
|
|
#[macro_use]
|
|
extern crate log;
|
|
#[macro_use]
|
|
extern crate thiserror;
|
|
|
|
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<Tera> = 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<String, Value>| -> tera::Result<Value> {
|
|
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<String, Value>| -> tera::Result<Value> {
|
|
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<String, Value>| -> tera::Result<Value> {
|
|
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<String, Value>| -> tera::Result<Value> {
|
|
// match args.get("name") {
|
|
// Some(Value::String(s)) => {
|
|
// let r =
|
|
// },
|
|
// _ => Err("Expected string argument".into()),
|
|
// }
|
|
// });
|
|
|
|
tera
|
|
});
|
|
|
|
type YopaStoreWrapper = web::Data<tokio::sync::RwLock<yopa::Storage>>;
|
|
|
|
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
|
|
}
|
|
|