Files
inkopslista/srv/src/main.rs
T
ide 22cf928852 Add automatic generation of favourite buttons
favourite buttons are based on how often a good i added to list
finished implementing setting category
made number of days until button an environment variable

rebase on main and fix

ran cargo fmt
2026-08-19 23:48:19 +02:00

150 lines
4.7 KiB
Rust

#[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<ConnectionThreadSafe>) -> Json<Vec<Item>> {
// 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::<i64, _>("checked") > 0,
amount: row.read::<i64, _>("amount"),
},
}
})
.collect();
Json(list)
}
#[get("/api/favorites")]
fn get_favorites(db: &State<ConnectionThreadSafe>) -> Json<Vec<ButtonItem>> {
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<ButtonItem> = 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/<itemname>/category", data = "<category>")]
fn put_category(db: &State<ConnectionThreadSafe>, 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/<item>", data = "<data>")]
fn put_list(item: &str, data: Json<ItemData>, db: &State<ConnectionThreadSafe>) {
// 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/<item>")]
fn delete_item(item: &str, db: &State<ConnectionThreadSafe>) {
// 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::<rocket::shield::Frame>(),
)
.manage(db)
.mount(
"/",
routes![
index,
get_list,
put_list,
delete_item,
get_favorites,
put_category
],
)
.mount("/", FileServer::from(www_path()))
}