diff --git a/Cargo.toml b/Cargo.toml index d8c78cf..1b9757e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,4 +21,6 @@ futures = "0.3.30" chrono = { version = "0.4.38", features = ["alloc"]} futures-util = "0.3.30" h2 = "0.4.6" -daemonize = "0.5.0" + +[target.'cfg(unix)'.dependencies] +daemonize = "0.5.0" \ No newline at end of file diff --git a/migrations/20240914120528_create_tables.sql b/migrations/20240914120528_create_tables.sql new file mode 100644 index 0000000..9faff80 --- /dev/null +++ b/migrations/20240914120528_create_tables.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS players +( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS votes +( + id INTEGER PRIMARY KEY NOT NULL, + submit_date TEXT NOT NULL, + player_id INTEGER NOT NULL, + nickname TEXT NOT NULL, + reason TEXT NOT NULL, + is_plus BOOLEAN NOT NULL DEFAULT 0 +) \ No newline at end of file diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..5b6203f --- /dev/null +++ b/settings.json @@ -0,0 +1,4 @@ +{ + "database_url": "sqlite:vote.db", + "bind_address": "127.0.0.1:8080" +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 92087f0..1662545 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,6 @@ -extern crate core; - -use bytes::Bytes; use bytes::Buf; +use bytes::Bytes; use chrono::{DateTime, Utc}; -use dotenvy::dotenv; use futures; use http_body_util::{BodyExt, Full}; use hyper::body::Incoming; @@ -16,16 +13,16 @@ use serde_json::{from_reader, Value}; use sqlx::sqlite::SqlitePool; use std::collections::HashMap; use std::env; -use std::env::vars; use std::fs::File; use std::io::Read; use std::net::SocketAddr; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::SystemTime; -use daemonize::Daemonize; use tokio::net::TcpListener; +#[cfg(target_os = "linux")] +use daemonize::Daemonize; #[derive(Serialize, Deserialize)] struct Player { id: i64, @@ -34,20 +31,22 @@ struct Player { #[derive(Serialize, Deserialize, Debug)] struct Vote { - vote_plus_id: i64, - vote_plus_nickname: String, - vote_plus_reason: String, - vote_moins_id: i64, - vote_moins_nickname: String, - vote_moins_reason: String, + player_id: i64, + nickname: String, + reason: String, + is_plus: bool, +} + +#[derive(Serialize, Deserialize)] +struct Settings { + database_url: String, + bind_address: String, } async fn service(req: Request, db: Arc>) -> Result>, Error> { match req.method() { - &Method::GET => { get(req,db).await } - &Method::POST => { - post(req, db).await - } - _ => { Ok(Response::new(Full::new(Bytes::from("This method is unimplemented")))) } + &Method::GET => { get(req, db).await } + &Method::POST => { post(req, db).await } + _ => { Ok(Response::builder().status(StatusCode::IM_A_TEAPOT).body(Full::new(Bytes::new())).unwrap()) } } } @@ -63,34 +62,34 @@ async fn get(req: Request, db: Arc>) -> Result Result>, Error> { - let uri_map_path = find_in_vars("ROUTES_FILE"); - let file = File::open(uri_map_path).expect("Could not find routes"); - let map: Value = from_reader(file).expect("Could not read data"); + let mut routes = env::current_dir().expect("Could not get app directory (Required to get routes)"); + routes.push("routes.json"); + let file = File::open(routes).expect("Could not open routes file."); + let map: Value = from_reader(file).expect("Could not parse routes, please verify syntax."); match &map[path] { - Value::String(s) => get_file(s.as_str()).await, + Value::String(s) => get_file(&s).await, _ => not_found().await } } -async fn get_file(path: &str) -> Result>, Error> { - let current_dir = env::current_dir().unwrap().clone(); - let mut path = path; +async fn get_file(mut path: &str) -> Result>, Error> { + let mut file_path = env::current_dir().expect("Could not get app directory."); if path.starts_with(r"/") { path = path.strip_prefix(r"/").unwrap(); } - let file_path = current_dir.join(path); + file_path.push(path); match File::open(&file_path) { Ok(mut file) => { let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); - let header = match file_path.extension().unwrap().to_str().unwrap() { + let content_type = match file_path.extension().unwrap().to_str().unwrap() { "js" => "text/javascript", "html" => "text/html", "css" => "text/css", _ => "" }; - Ok(Response::builder().header("content-type", header).body(Full::new(Bytes::from(buf))).unwrap()) + Ok(Response::builder().header("content-type", content_type).body(Full::new(Bytes::from(buf))).unwrap()) } Err(_) => not_found().await } @@ -100,7 +99,7 @@ async fn get_data(path: &str, req: &Request, db: Arc let pool = db.clone().lock().unwrap().clone(); match path { "/data/players" => { - let items = sqlx::query!(r#"SELECT id, name FROM players"#).fetch_all(&pool).await.unwrap(); + let items = sqlx::query!(r#"SELECT * FROM players"#).fetch_all(&pool).await.unwrap(); let players: Vec = items.iter().map(|x| Player { id: x.id, name: x.name.clone() }).collect(); Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&players).unwrap())))) } @@ -110,24 +109,36 @@ async fn get_data(path: &str, req: &Request, db: Arc } "/data/results" => { let votes = get_votes(req, db).await; - let ids: Vec<[i64; 2]> = votes.iter().map(|x| [x.vote_plus_id, x.vote_moins_id]).collect(); - let mut results: Vec> = Vec::new(); - for i in 0..=1 { - let mut counts = HashMap::new(); - ids.iter().for_each(|x| { - if counts.get(&x[i]).is_none() { - counts.insert(x[i], 0); - } - *counts.get_mut(&x[i]).unwrap() += 1; - }); - results.push(counts); - } + let ids: Vec<(i64, bool)> = votes.iter().map(|x| (x.player_id, x.is_plus)).collect(); - let mut sorted: Vec> = results.into_iter().map(|hashmap| hashmap.into_iter().collect::>()).collect(); + let mut plus_results: HashMap = HashMap::new(); + let mut minus_results: HashMap = HashMap::new(); - sorted.iter_mut().for_each(|x| x.sort_by(|a, b| b.1.cmp(&a.1))); + let _ = ids.iter().filter(|x| x.1).for_each(|x| { + let id = x.0; + if plus_results.contains_key(&id) { + plus_results.insert(id, 0); + } + *plus_results.get_mut(&id).unwrap() += 1; + }); - Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&sorted).unwrap())))) + let _ = ids.iter().filter(|x| !x.1).for_each(|x| { + let id = x.0; + if minus_results.contains_key(&id) { + minus_results.insert(id, 0); + } + *minus_results.get_mut(&id).unwrap() += 1; + }); + + let mut plus_results: Vec<(i64, i64)> = plus_results.into_iter().collect(); + let mut minus_results: Vec<(i64, i64)> = minus_results.into_iter().collect(); + + plus_results.sort_by(|a, b| { b.1.cmp(&a.1) }); + minus_results.sort_by(|a, b| { b.1.cmp(&a.1) }); + + let sorted_results = vec![plus_results, minus_results]; + + Ok(Response::new(Full::new(Bytes::from(serde_json::to_string(&sorted_results).unwrap())))) } _ => not_found().await } @@ -153,15 +164,13 @@ async fn get_votes(req: &Request, db: Arc>) -> Vec, db: Arc>) -> Result = serde_json::from_reader(body.aggregate().reader()); + let body = req.into_body().collect().await?; + let data: Result = from_reader(body.aggregate().reader()); if data.is_err() { - return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()) + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Full::new(Bytes::from("Bad Request"))).unwrap()); } let vote = data.unwrap(); let timestamp: DateTime = DateTime::from(SystemTime::now()); let formatted = timestamp.format("%d/%m/%Y").to_string(); let pool = db.clone().lock().unwrap().clone(); let mut conn = pool.acquire().await.unwrap(); - let result = sqlx::query!(r#"INSERT INTO votes ( plus_id, plus_nickname, plus_reason, moins_id, moins_nickname, moins_reason, timestamp ) - VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7 )"#, - vote.vote_plus_id, - vote.vote_plus_nickname, - vote.vote_plus_reason, - vote.vote_moins_id, - vote.vote_moins_nickname, - vote.vote_moins_reason, + let result = sqlx::query!(r#"INSERT INTO votes ( PLAYER_ID, NICKNAME, REASON, IS_PLUS, SUBMIT_DATE ) + VALUES ( ?1, ?2, ?3, ?4, ?5 )"#, + vote.player_id, + vote.nickname, + vote.reason, + vote.is_plus, formatted).execute(&mut *conn).await; if result.is_err() { - return Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Internet Error"))).unwrap()) + return Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Full::new(Bytes::from("Internet Error"))).unwrap()); } - Ok(Response::builder().body(Full::new(Bytes::from("OK"))).unwrap()) + Ok(Response::builder().body(Full::new(Bytes::new())).unwrap()) } async fn not_found() -> Result>, Error> { - let current_dir = env::current_dir().unwrap().clone(); - let file_path = current_dir.join("static/html/404.html"); + let mut file_path = env::current_dir().expect("Could not get app directory."); + file_path.push("static/html/404.html"); let mut file = File::open(file_path).unwrap(); let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); Ok(Response::builder().status(StatusCode::NOT_FOUND).body(Full::new(Bytes::from(buf))).unwrap()) } -fn find_in_vars(pattern: &str) -> String { - let mut vars = vars(); - let result = vars.find(|x| x.0.contains(pattern)).expect(&format!("Could not find '{}' in environment variables", pattern)); - result.1 +fn get_settings() -> Settings { + let mut settings_path = env::current_dir().expect("Could not get app directory. (Required to read settings)"); + settings_path.push("settings.json"); + let settings_file = File::open(settings_path).expect("Could not open settings file, does it exists?"); + let settings: Settings = from_reader(settings_file).expect("Could not parse settings, please check syntax."); + settings } +#[cfg(target_os = "linux")] #[tokio::main] async fn main() { - let stdout = File::create("/tmp/daemon.out").unwrap(); - let stderr = File::create("/tmp/daemon.err").unwrap(); + let current_directory = env::current_dir().expect("Could not get app directory."); + let stdout = File::create("/var/vote/log/daemon.out").unwrap(); + let stderr = File::create("/var/vote/log/daemon.err").unwrap(); let daemonize = Daemonize::new() - .pid_file("/tmp/test.pid") // Every method except `new` and `start` - .chown_pid_file(true) // is optional, see `Daemonize` documentation - .working_directory("/home/aindustries/vote-rllhc/") // for default behaviour. - .stdout(stdout) // Redirect stdout to `/tmp/daemon.out`. - .stderr(stderr); // Redirect stderr to `/tmp/daemon.err`. + .pid_file("/var/vote/server.pid") + .chown_pid_file(true) + .working_directory(current_directory.to_str()) + .stdout(stdout) + .stderr(stderr); match daemonize.start() { - Ok(_) => { - let _ = dotenv(); - let db_adrr = find_in_vars("DATABASE_ADRR"); - let db_pool = Arc::new(Mutex::new(SqlitePool::connect(&db_adrr).await.unwrap())); - let bind_adrr: SocketAddr = SocketAddr::from_str(find_in_vars("BIND_ADRR").as_str()).expect("Could not parse bind address"); - let listener = TcpListener::bind(bind_adrr).await.expect("Could not bind to address."); - loop { - let (tcp, _) = listener.accept().await.expect("Could not accept stream"); - let io = TokioIo::new(tcp); - let db = db_pool.clone(); - let service = service_fn(move |req| { - service(req, db.clone()) - }); - tokio::task::spawn(async move { - if let Err(err) = http1::Builder::new() - .timer(TokioTimer::new()) - .serve_connection(io, service).await - { - println!("Failed to serve connection: {:?}", err); - } - }); - } - }, + Ok(_) => run().await, Err(e) => eprintln!("Error, {}", e), } - +} + +#[cfg(target_os = "windows")] +#[tokio::main] +async fn main() { + run().await; +} + +async fn run() { + let settings = get_settings(); + let db_pool = Arc::new(Mutex::new(SqlitePool::connect(&settings.database_url).await.expect("Could not connect to database. Make sure the url is correct."))); + let bind_address: SocketAddr = SocketAddr::from_str(&settings.bind_address).expect("Could not parse bind address."); + let listener = TcpListener::bind(bind_address).await.expect("Could not bind to address."); + loop { + let (stream, _) = listener.accept().await.expect("Could not accept incoming stream."); + let io = TokioIo::new(stream); + let db = db_pool.clone(); + let service = service_fn( + move |req| { + service(req, db.clone()) + } + ); + tokio::task::spawn( + async move { + if let Err(err) = http1::Builder::new() + .timer(TokioTimer::new()) + .serve_connection(io, service).await { + println!("Failed to serve connection: {:?}", err); + } + } + ); + } }