#[macro_use] extern crate rocket; use clap::Parser; use inkopslista_lib::{ButtonItem, Item, ItemData}; use rocket::{ State, fs::{FileServer, NamedFile}, serde::json::Json, shield::Shield, }; use sqlite::ConnectionThreadSafe; use std::path::{Path, PathBuf}; /// The directory where the web files for the gui is stored. fn www_path() -> &'static Path { Path::new("./www") } /// Serve `index.html` from [`www_path`]. #[get("/")] async fn index() -> NamedFile { NamedFile::open(www_path().join("index.html")) .await .expect("index.html") } #[get("/api/list")] fn get_list(db: &State) -> Json> { // TODO errorhandling let query = "SELECT * FROM list;"; let list = db .prepare(query) .unwrap() .into_iter() .map(|row| { let row = row.unwrap(); Item { name: row.read::<&str, _>("itemname").to_string(), data: ItemData { checked: row.read::("checked") > 0, amount: row.read::("amount"), }, } }) .collect(); Json(list) } #[get("/api/favorites")] fn get_favorites(db: &State) -> Json> { let opt = Opt::parse(); let query = format!("WITH favorites as (SELECT itemname, COUNT(date_added) day_count FROM adds WHERE (date(julianday('now')) - date(date_added, 'unixepoch')) < 184 GROUP BY itemname HAVING day_count > {num_days}) SELECT favorites.itemname, category FROM favorites LEFT OUTER JOIN categories ON favorites.itemname = categories.itemname", num_days = opt.num_days); let list: Vec = db .prepare(query) .unwrap() .into_iter() .map(|row| { let row = row.unwrap(); ButtonItem { name: row.read::<&str, _>("itemname").to_string(), category: row .try_read::<&str, _>("category") .ok() .map(ToString::to_string), } }) .collect(); Json(list) } #[put("/api/favourites//category", data = "")] fn put_category(db: &State, itemname: &str, category: &str) { let query = "INSERT INTO categories VALUES (?, ?) ON CONFLICT DO UPDATE SET category = ?"; let mut statement = db.prepare(query).unwrap(); statement.bind((1, itemname)).unwrap(); statement.bind((2, category)).unwrap(); statement.bind((3, category)).unwrap(); while let Ok(sqlite::State::Row) = statement.next() {} } #[put("/api/list/", data = "")] fn put_list(item: &str, data: Json, db: &State) { // TODO: errorhandling let query = "INSERT INTO list VALUES(?, ?, ?) ON CONFLICT DO UPDATE SET checked = ?, amount = ?;"; let mut statement = db.prepare(query).unwrap(); let data = data.into_inner(); statement.bind((1, item)).unwrap(); statement.bind((2, data.checked as i64)).unwrap(); statement.bind((3, data.amount)).unwrap(); statement.bind((4, data.checked as i64)).unwrap(); statement.bind((5, data.amount)).unwrap(); while let Ok(sqlite::State::Row) = statement.next() {} let query = "INSERT INTO adds VALUES(?, strftime('%s','now')) ON CONFLICT DO NOTHING"; let mut statement = db.prepare(query).unwrap(); statement.bind((1, item)).unwrap(); while let Ok(sqlite::State::Row) = statement.next() {} } #[delete("/api/list/")] fn delete_item(item: &str, db: &State) { // TODO: Eror handling let query = "DELETE FROM list WHERE itemname = ?"; let mut statement = db.prepare(query).unwrap(); statement.bind((1, item)).unwrap(); while let Ok(sqlite::State::Row) = statement.next() {} } #[derive(Parser)] struct Opt { #[clap(long, env = "DB_PATH")] db_path: PathBuf, #[clap(long, env = "NUM_DAYS", default_value = "5")] num_days: u32, } #[launch] fn rocket() -> _ { let opt = Opt::parse(); let db = sqlite::Connection::open_thread_safe(opt.db_path).unwrap(); db.execute(include_str!("db/init.sql")) .expect("unable to initialize database"); rocket::build() .attach( Shield::default() // Allow content to be served through iframes .disable::(), ) .manage(db) .mount( "/", routes![ index, get_list, put_list, delete_item, get_favorites, put_category ], ) .mount("/", FileServer::from(www_path())) }