From 4e91dbb571eb3c133ef11de99ae245b9066d9066 Mon Sep 17 00:00:00 2001 From: CEbbinghaus Date: Tue, 31 Oct 2023 02:51:14 +1100 Subject: [PATCH] Fixdd bug, Serialized card hashes and added event types --- backend/Cargo.lock | 8 +- backend/Cargo.toml | 1 + backend/rustfmt.toml | 1 + backend/src/api.rs | 311 +++++++------ backend/src/ds.rs | 735 ++++++++++++++++-------------- backend/src/dto.rs | 48 +- backend/src/env.rs | 52 ++- backend/src/err.rs | 72 +-- backend/src/log.rs | 112 +++-- backend/src/main.rs | 159 +++---- backend/src/sdcard.rs | 24 +- backend/src/steam.rs | 36 +- backend/src/watch.rs | 295 +++++------- lib/src/backend.ts | 4 +- lib/src/state/MicoSDeckManager.ts | 2 +- 15 files changed, 948 insertions(+), 912 deletions(-) create mode 100644 backend/rustfmt.toml diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 8142d57..a138357 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -323,6 +323,7 @@ dependencies = [ "keyvalues-serde", "log", "once_cell", + "semver", "serde", "serde_json", "simplelog", @@ -1394,9 +1395,12 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +dependencies = [ + "serde", +] [[package]] name = "serde" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 45df166..870e277 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -22,6 +22,7 @@ async-trait = "0.1.68" serde = { version = "1.0.145", features = ["derive", "rc"] } slotmap = { version = "1.0.6", features = ["serde"] } glob = "0.3.1" +semver = { version = "1.0.20", features = ["serde"] } [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } diff --git a/backend/rustfmt.toml b/backend/rustfmt.toml new file mode 100644 index 0000000..7558deb --- /dev/null +++ b/backend/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs=true \ No newline at end of file diff --git a/backend/src/api.rs b/backend/src/api.rs index fd096da..b20ed9f 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -1,9 +1,9 @@ use crate::{ - ds::Store, - dto::{Game, MicroSDCard}, - env::PACKAGE_VERSION, - err::Error, - sdcard::{get_card_cid, is_card_inserted}, + ds::Store, + dto::{CardEvent, Game, MicroSDCard}, + env::PACKAGE_VERSION, + err::Error, + sdcard::{get_card_cid, is_card_inserted}, }; use actix_web::{delete, get, post, web, HttpResponse, Responder, Result}; use serde::Deserialize; @@ -11,299 +11,306 @@ use std::{ops::Deref, sync::Arc}; use tokio::sync::broadcast::Sender; pub(crate) fn config(cfg: &mut web::ServiceConfig) { - cfg.service(health) - .service(version) - .service(listen) - .service(save) - .service(get_current_card) - .service(get_current_card_id) - .service(get_current_card_and_games) - .service(get_games_on_current_card) - .service(create_card) - .service(delete_card) - .service(list_cards) - .service(get_card) - .service(create_game) - .service(create_games) - .service(delete_game) - .service(list_games) - .service(get_game) - .service(list_games_for_card) - .service(list_cards_for_game) - .service(list_cards_with_games) - .service(create_link) - .service(create_links) - .service(delete_link) - .service(delete_links) - ; + cfg.service(health) + .service(version) + .service(listen) + .service(save) + .service(get_current_card) + .service(get_current_card_id) + .service(get_current_card_and_games) + .service(get_games_on_current_card) + .service(create_card) + .service(delete_card) + .service(list_cards) + .service(get_card) + .service(create_game) + .service(create_games) + .service(delete_game) + .service(list_games) + .service(get_game) + .service(list_games_for_card) + .service(list_cards_for_game) + .service(list_cards_with_games) + .service(create_link) + .service(create_links) + .service(delete_link) + .service(delete_links); } #[get("/version")] pub(crate) async fn version() -> impl Responder { - HttpResponse::Ok().body(PACKAGE_VERSION) + HttpResponse::Ok().body(PACKAGE_VERSION) } #[get("/health")] pub(crate) async fn health() -> impl Responder { - HttpResponse::Ok() + HttpResponse::Ok() } #[get("/listen")] -pub(crate) async fn listen(sender: web::Data>) -> Result { - sender - .subscribe() - .recv() - .await - .map_err(|_| Error::from_str("Unable to retrieve update"))?; - Ok(HttpResponse::Ok()) +pub(crate) async fn listen(sender: web::Data>) -> Result { + Ok(web::Json(sender.subscribe().recv().await.map_err( + |_| Error::from_str("Unable to retrieve update"), + )?)) } #[get("/list")] pub(crate) async fn list_cards_with_games(datastore: web::Data>) -> impl Responder { - web::Json(datastore.list_cards_with_games()) + web::Json(datastore.list_cards_with_games()) } #[get("/list/games/{card_id}")] pub(crate) async fn list_games_for_card( - card_id: web::Path, - datastore: web::Data>, + card_id: web::Path, + datastore: web::Data>, ) -> Result { - match datastore.get_games_on_card(&card_id) { - Ok(value) => Ok(web::Json(value)), - Err(err) => Err(actix_web::Error::from(err)), - } + match datastore.get_games_on_card(&card_id) { + Ok(value) => Ok(web::Json(value)), + Err(err) => Err(actix_web::Error::from(err)), + } } #[get("/list/cards/{game_id}")] pub(crate) async fn list_cards_for_game( - game_id: web::Path, - datastore: web::Data>, + game_id: web::Path, + datastore: web::Data>, ) -> Result { - match datastore.get_cards_for_game(&game_id) { - Ok(value) => Ok(web::Json(value)), - Err(err) => Err(actix_web::Error::from(err)), - } + match datastore.get_cards_for_game(&game_id) { + Ok(value) => Ok(web::Json(value)), + Err(err) => Err(actix_web::Error::from(err)), + } } #[get("/current")] pub(crate) async fn get_current_card_and_games( - datastore: web::Data>, + datastore: web::Data>, ) -> Result { - if !is_card_inserted() { - return Err(Error::from_str("No card is inserted").into()); - } + if !is_card_inserted() { + return Err(Error::from_str("No card is inserted").into()); + } - let uid = get_card_cid().ok_or(Error::Error("Unable to evaluate Card Id".into()))?; + let uid = get_card_cid().ok_or(Error::Error("Unable to evaluate Card Id".into()))?; - Ok(web::Json(datastore.get_card_and_games(&uid)?)) + Ok(web::Json(datastore.get_card_and_games(&uid)?)) } #[get("/current/card")] pub(crate) async fn get_current_card(datastore: web::Data>) -> Result { - if !is_card_inserted() { - return Err(Error::from_str("No card is inserted").into()); - } + if !is_card_inserted() { + return Err(Error::from_str("No card is inserted").into()); + } - let uid = get_card_cid().ok_or(Error::Error("Unable to evaluate Card Id".into()))?; + let uid = get_card_cid().ok_or(Error::Error("Unable to evaluate Card Id".into()))?; - Ok(web::Json(datastore.get_card(&uid)?)) + Ok(web::Json(datastore.get_card(&uid)?)) } #[get("/current/id")] pub(crate) async fn get_current_card_id() -> Result { - if !is_card_inserted() { - return Err(Error::from_str("No card is inserted").into()); - } + if !is_card_inserted() { + return Err(Error::from_str("No card is inserted").into()); + } - Ok(get_card_cid().ok_or(Error::from_str("Unable to evaluate Card Id"))?) + Ok(get_card_cid().ok_or(Error::from_str("Unable to evaluate Card Id"))?) } #[get("/current/games")] pub(crate) async fn get_games_on_current_card( - datastore: web::Data>, + datastore: web::Data>, ) -> Result { - if !is_card_inserted() { - return Err(Error::from_str("No card is inserted").into()); - } + if !is_card_inserted() { + return Err(Error::from_str("No card is inserted").into()); + } - let uid = get_card_cid().ok_or(Error::from_str("Unable to evaluate Card Id"))?; + let uid = get_card_cid().ok_or(Error::from_str("Unable to evaluate Card Id"))?; - match datastore.get_games_on_card(&uid) { - Ok(value) => Ok(web::Json(value)), - Err(err) => Err(actix_web::Error::from(err)), - } + match datastore.get_games_on_card(&uid) { + Ok(value) => Ok(web::Json(value)), + Err(err) => Err(actix_web::Error::from(err)), + } } #[post("/card/{id}")] pub(crate) async fn create_card( - id: web::Path, - body: web::Json, - datastore: web::Data>, + id: web::Path, + body: web::Json, + datastore: web::Data>, + sender: web::Data>, ) -> Result { - if *id != body.uid { - return Err(Error::from_str("uid did not match id provided").into()); - } - - match datastore.contains_element(&id) { - // Merge the records allowing us to update all properties - true => datastore.update_card(&id, move |existing_card| { - existing_card.merge(body.deref().to_owned())?; - Ok(()) - })?, - // Insert a new card if it doesn't exist - false => datastore.add_card(id.into_inner(), body.into_inner()), - } - Ok(HttpResponse::Ok()) + if *id != body.uid { + return Err(Error::from_str("uid did not match id provided").into()); + } + + match datastore.contains_element(&id) { + // Merge the records allowing us to update all properties + true => datastore.update_card(&id, move |existing_card| { + existing_card.merge(body.deref().to_owned())?; + Ok(()) + })?, + // Insert a new card if it doesn't exist + false => datastore.add_card(id.into_inner(), body.into_inner()), + } + + _ = sender.send(CardEvent::Updated); + Ok(HttpResponse::Ok()) } #[delete("/card/{id}")] pub(crate) async fn delete_card( - id: web::Path, - datastore: web::Data>, + id: web::Path, + datastore: web::Data>, + sender: web::Data>, ) -> Result { - datastore.remove_element(&id)?; + datastore.remove_element(&id)?; - Ok(HttpResponse::Ok()) + _ = sender.send(CardEvent::Updated); + Ok(HttpResponse::Ok()) } #[get("/card/{id}")] pub(crate) async fn get_card( - id: web::Path, - datastore: web::Data>, + id: web::Path, + datastore: web::Data>, ) -> Result { - Ok(web::Json(datastore.get_card(&id)?)) + Ok(web::Json(datastore.get_card(&id)?)) } -#[post("/cards")] +#[get("/cards")] pub(crate) async fn list_cards(datastore: web::Data>) -> impl Responder { - web::Json(datastore.list_cards()) + web::Json(datastore.list_cards()) } #[post("/game/{id}")] pub(crate) async fn create_game( - id: web::Path, - body: web::Json, - datastore: web::Data>, + id: web::Path, + body: web::Json, + datastore: web::Data>, ) -> Result { - if *id != body.uid { - return Err(Error::from_str("uid did not match id provided").into()); - } + if *id != body.uid { + return Err(Error::from_str("uid did not match id provided").into()); + } - let mut game = body.to_owned(); + let mut game = body.to_owned(); - if !cfg!(debug_assertions) { - game.is_steam = false; - } + if !cfg!(debug_assertions) { + game.is_steam = false; + } - datastore.add_game(id.into_inner(), game); + datastore.add_game(id.into_inner(), game); - Ok(HttpResponse::Ok()) + Ok(HttpResponse::Ok()) } #[delete("/game/{id}")] pub(crate) async fn delete_game( - id: web::Path, - datastore: web::Data>, + id: web::Path, + datastore: web::Data>, ) -> Result { - datastore.remove_element(&id)?; + datastore.remove_element(&id)?; - Ok(HttpResponse::Ok()) + Ok(HttpResponse::Ok()) } #[get("/game/{id}")] pub(crate) async fn get_game( - id: web::Path, - datastore: web::Data>, + id: web::Path, + datastore: web::Data>, ) -> Result { - Ok(web::Json(datastore.get_game(&id)?)) + Ok(web::Json(datastore.get_game(&id)?)) } #[get("/games")] pub(crate) async fn list_games(datastore: web::Data>) -> impl Responder { - web::Json(datastore.list_games()) + web::Json(datastore.list_games()) } #[post("/games")] pub(crate) async fn create_games( - body: web::Json>, - datastore: web::Data>, + body: web::Json>, + datastore: web::Data>, ) -> impl Responder { for game in body.iter() { let mut game = game.to_owned(); - + if !cfg!(debug_assertions) { game.is_steam = false; } - + datastore.add_game(game.uid.clone(), game); } - - HttpResponse::Ok() + + HttpResponse::Ok() } #[derive(Deserialize)] pub struct LinkBody { - card_id: String, - game_id: String, + card_id: String, + game_id: String, } #[derive(Deserialize)] pub struct ManyLinkBody { - card_id: String, - game_ids: Vec, + card_id: String, + game_ids: Vec, } #[post("/link")] pub(crate) async fn create_link( - body: web::Json, - datastore: web::Data>, + body: web::Json, + datastore: web::Data>, + sender: web::Data>, ) -> Result { - datastore.link(&body.game_id, &body.card_id)?; + datastore.link(&body.game_id, &body.card_id)?; - Ok(HttpResponse::Ok()) + _ = sender.send(CardEvent::Updated); + Ok(HttpResponse::Ok()) } #[post("/linkmany")] pub(crate) async fn create_links( - body: web::Json, - datastore: web::Data>, + body: web::Json, + datastore: web::Data>, + sender: web::Data>, ) -> Result { - let data = body.into_inner(); - for game_id in data.game_ids.iter() { + for game_id in data.game_ids.iter() { datastore.link(&game_id, &data.card_id)?; } - Ok(HttpResponse::Ok()) + _ = sender.send(CardEvent::Updated); + Ok(HttpResponse::Ok()) } #[post("/unlink")] pub(crate) async fn delete_link( - body: web::Json, - datastore: web::Data>, + body: web::Json, + datastore: web::Data>, + sender: web::Data>, ) -> Result { - datastore.unlink(&body.game_id, &body.card_id)?; + datastore.unlink(&body.game_id, &body.card_id)?; - Ok(HttpResponse::Ok()) + _ = sender.send(CardEvent::Updated); + Ok(HttpResponse::Ok()) } #[post("/unlinkmany")] pub(crate) async fn delete_links( - body: web::Json, - datastore: web::Data>, + body: web::Json, + datastore: web::Data>, + sender: web::Data>, ) -> Result { - let data = body.into_inner(); - for game_id in data.game_ids.iter() { + for game_id in data.game_ids.iter() { datastore.unlink(&game_id, &data.card_id)?; } - Ok(HttpResponse::Ok()) + _ = sender.send(CardEvent::Updated); + Ok(HttpResponse::Ok()) } #[post("/save")] pub(crate) async fn save(datastore: web::Data>) -> Result { - datastore.write_to_file()?; + datastore.write_to_file()?; - Ok(HttpResponse::Ok()) + Ok(HttpResponse::Ok()) } diff --git a/backend/src/ds.rs b/backend/src/ds.rs index a1170e8..934119a 100644 --- a/backend/src/ds.rs +++ b/backend/src/ds.rs @@ -1,293 +1,355 @@ use crate::{ - dto::{Game, MicroSDCard}, - err::Error, + dto::{Game, MicroSDCard}, + env::PACKAGE_VERSION, + err::Error, + sdcard::get_steam_acf_files, }; -use log::error; +use log::{error, info}; +use semver::Version; use serde::{Deserialize, Serialize}; use slotmap::{DefaultKey, SlotMap}; use std::{ - collections::{HashMap, HashSet}, - fs::{read_to_string, write}, - path::PathBuf, - sync::RwLock, borrow::BorrowMut, + borrow::BorrowMut, + collections::{hash_map::DefaultHasher, HashMap, HashSet}, + fs::{self, read_to_string, write}, + hash::{Hash, Hasher}, + path::PathBuf, + sync::RwLock, }; -#[derive(Serialize, Deserialize, Clone)] -enum StoreElement { - Game(Game), - Card(MicroSDCard), +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) enum StoreElement { + Game(Game), + Card(MicroSDCard), } impl StoreElement { - fn as_game(&self) -> Option { - match self { - Self::Game(game) => Some(game.clone()), - _ => None, - } - } - - fn as_card(&self) -> Option { - match self { - Self::Card(card) => Some(card.clone()), - _ => None, - } - } + pub fn as_game(&self) -> Option { + match self { + Self::Game(game) => Some(game.clone()), + _ => None, + } + } + + pub fn as_card(&self) -> Option { + match self { + Self::Card(card) => Some(card.clone()), + _ => None, + } + } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] struct Node { - element: StoreElement, - links: HashSet, + pub(crate) element: StoreElement, + pub(crate) links: HashSet, } impl Node { - pub fn from_card(card: MicroSDCard) -> Self { - Node { - element: StoreElement::Card(card), - links: HashSet::new(), - } - } - pub fn from_game(game: Game) -> Self { - Node { - element: StoreElement::Game(game), - links: HashSet::new(), - } - } + pub fn from_card(card: MicroSDCard) -> Self { + Node { + element: StoreElement::Card(card), + links: HashSet::new(), + } + } + pub fn from_game(game: Game) -> Self { + Node { + element: StoreElement::Game(game), + links: HashSet::new(), + } + } +} + +fn default_version() -> Version { + Version::parse(PACKAGE_VERSION).unwrap() } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct StoreData { - nodes: SlotMap, - node_ids: HashMap, + #[serde(default="default_version")] + version: Version, + nodes: SlotMap, + node_ids: HashMap, + #[serde(default)] + hashes: HashMap, } impl StoreData { - pub fn add_card(&mut self, id: String, card: MicroSDCard) { - self.node_ids - .entry(id) - .or_insert_with(|| self.nodes.insert(Node::from_card(card))); - } - - pub fn add_game(&mut self, id: String, game: Game) { - self.node_ids - .entry(id) - .or_insert_with(|| self.nodes.insert(Node::from_game(game))); - } - - pub fn update_card(&mut self, card_id: &str, mut func: F) -> Result<(), Error> - where - F: FnMut(&mut MicroSDCard) -> Result<(), Error>, - { - let node = self - .node_ids - .get(card_id) - .ok_or(Error::Error("Card Id not present".into()))?; - - match self.nodes.get_mut(*node).unwrap().element { - StoreElement::Card(ref mut card) => { - func(card)?; - } - StoreElement::Game(_) => return Err(Error::Error("Expected Card, got Game".into())), - } - - Ok(()) - } - - pub fn link(&mut self, a_id: &str, b_id: &str) -> Result<(), Error> { - let a_key = self.node_ids.get(a_id); - let b_key = self.node_ids.get(b_id); - let (a_key, b_key) = a_key - .zip(b_key) - .ok_or_else(|| Error::from_str("Either Game or Card could not be found"))?; - - self.nodes[*a_key].links.insert(*b_key); - self.nodes[*b_key].links.insert(*a_key); - - Ok(()) - } + pub fn add_card(&mut self, id: String, card: MicroSDCard) { + self.node_ids + .entry(id) + .or_insert_with(|| self.nodes.insert(Node::from_card(card))); + } + + pub fn add_game(&mut self, id: String, game: Game) { + self.node_ids + .entry(id) + .or_insert_with(|| self.nodes.insert(Node::from_game(game))); + } + + pub fn update_card(&mut self, card_id: &str, mut func: F) -> Result<(), Error> + where + F: FnMut(&mut MicroSDCard) -> Result<(), Error>, + { + let node = self + .node_ids + .get(card_id) + .ok_or(Error::Error("Card Id not present".into()))?; + + match self.nodes.get_mut(*node).unwrap().element { + StoreElement::Card(ref mut card) => { + func(card)?; + } + StoreElement::Game(_) => return Err(Error::Error("Expected Card, got Game".into())), + } + + Ok(()) + } + + pub fn link(&mut self, a_id: &str, b_id: &str) -> Result<(), Error> { + let a_key = self.node_ids.get(a_id); + let b_key = self.node_ids.get(b_id); + let (a_key, b_key) = a_key + .zip(b_key) + .ok_or_else(|| Error::from_str("Either Game or Card could not be found"))?; + + self.nodes[*a_key].links.insert(*b_key); + self.nodes[*b_key].links.insert(*a_key); + + Ok(()) + } pub fn unlink(&mut self, a_id: &str, b_id: &str) -> Result<(), Error> { - let game_key = self.node_ids.get(a_id); - let card_key = self.node_ids.get(b_id); - let (game_key, card_key) = game_key - .zip(card_key) - .ok_or_else(|| Error::from_str("Either Game or Card could not be found"))?; - - self.nodes[*game_key].links.remove(card_key); - self.nodes[*card_key].links.remove(game_key); - - Ok(()) - } - - pub fn remove_item(&mut self, id: &str) -> Result<(), Error> { - let element_key = self - .node_ids - .remove(id) - .ok_or_else(|| Error::from_str("Id not present"))?; - - for key in self.nodes.remove(element_key).unwrap().links { - self.nodes[key].links.remove(&element_key); - } - - Ok(()) - } - - pub fn contains_element(&self, card_id: &str) -> bool { - self.node_ids.contains_key(card_id) - } - - pub fn get_card(&self, card_id: &str) -> Result { - self.node_ids - .get(card_id) - .map_or(Error::new_res("Card Id not present"), |key| { - Ok(self.nodes[*key] - .element - .as_card() - .expect("Expected card but game was returned")) - }) - } - - pub fn get_game(&self, game_id: &str) -> Result { - self.node_ids - .get(game_id) - .map_or(Error::new_res("Game Id not present"), |key| { - Ok(self.nodes[*key] - .element - .as_game() - .expect("Expected game but card was returned")) - }) - } - - - pub fn get_card_and_games(&self, card_id: &str) -> Result<(MicroSDCard, Vec), Error> { - let card_key = self - .node_ids - .get(card_id) - .ok_or_else(|| Error::from_str("Card Id not present"))?; + let game_key = self.node_ids.get(a_id); + let card_key = self.node_ids.get(b_id); + let (game_key, card_key) = game_key + .zip(card_key) + .ok_or_else(|| Error::from_str("Either Game or Card could not be found"))?; + + self.nodes[*game_key].links.remove(card_key); + self.nodes[*card_key].links.remove(game_key); + + Ok(()) + } + + pub fn remove_item(&mut self, id: &str) -> Result<(), Error> { + let element_key = self + .node_ids + .remove(id) + .ok_or_else(|| Error::from_str("Id not present"))?; + + for key in self.nodes.remove(element_key).unwrap().links { + self.nodes[key].links.remove(&element_key); + } + + Ok(()) + } + + pub fn contains_element(&self, card_id: &str) -> bool { + self.node_ids.contains_key(card_id) + } + + pub fn get_card(&self, card_id: &str) -> Result { + self.node_ids + .get(card_id) + .map_or(Error::new_res("Card Id not present"), |key| { + Ok(self.nodes[*key] + .element + .as_card() + .expect("Expected card but game was returned")) + }) + } + + pub fn get_game(&self, game_id: &str) -> Result { + self.node_ids + .get(game_id) + .map_or(Error::new_res("Game Id not present"), |key| { + Ok(self.nodes[*key] + .element + .as_game() + .expect("Expected game but card was returned")) + }) + } + + pub fn get_card_and_games(&self, card_id: &str) -> Result<(MicroSDCard, Vec), Error> { + let card_key = self + .node_ids + .get(card_id) + .ok_or_else(|| Error::from_str("Card Id not present"))?; let node = &self.nodes[*card_key]; - let card = node.element.as_card().ok_or(Error::from_str("Element was not a card"))?; - let games = node - .links - .iter() - .filter_map(|game_key| self.nodes[*game_key].element.as_game()) - .collect(); - - Ok((card, games)) - } - - pub fn get_games_on_card(&self, card_id: &str) -> Result, Error> { - let card_key = self - .node_ids - .get(card_id) - .ok_or_else(|| Error::from_str("Card Id not present"))?; - - let games = self.nodes[*card_key] - .links - .iter() - .filter_map(|game_key| self.nodes[*game_key].element.as_game()) - .collect(); - - Ok(games) - } - - pub fn get_cards_for_game(&self, game_id: &str) -> Result, Error> { - let game_key = self - .node_ids - .get(game_id) - .ok_or_else(|| Error::from_str("Game Id not present"))?; - - let cards = self.nodes[*game_key] - .links - .iter() - .filter_map(|game_key| self.nodes[*game_key].element.as_card()) - .collect(); - - Ok(cards) - } - - pub fn list_cards(&self) -> Vec { - self.nodes - .iter() - .filter_map(|node| node.1.element.as_card()) - .collect() - } - - pub fn list_games(&self) -> Vec { - self.nodes - .iter() - .filter_map(|node| node.1.element.as_game()) - .collect() - } - - pub fn list_cards_with_games(&self) -> Vec<(MicroSDCard, Vec)> { - self.nodes - .iter() - .filter_map(|node| { - node.1.element.as_card().map(|v| { - (v, { - node.1 - .links - .iter() - .filter_map(|key: &DefaultKey| self.nodes[*key].element.as_game()) - .collect() - }) - }) - }) - .collect() - } + let card = node + .element + .as_card() + .ok_or(Error::from_str("Element was not a card"))?; + let games = node + .links + .iter() + .filter_map(|game_key| self.nodes[*game_key].element.as_game()) + .collect(); + + Ok((card, games)) + } + + pub fn get_games_on_card(&self, card_id: &str) -> Result, Error> { + let card_key = self + .node_ids + .get(card_id) + .ok_or_else(|| Error::from_str("Card Id not present"))?; + + let games = self.nodes[*card_key] + .links + .iter() + .filter_map(|game_key| self.nodes[*game_key].element.as_game()) + .collect(); + + Ok(games) + } + + pub fn get_cards_for_game(&self, game_id: &str) -> Result, Error> { + let game_key = self + .node_ids + .get(game_id) + .ok_or_else(|| Error::from_str("Game Id not present"))?; + + let cards = self.nodes[*game_key] + .links + .iter() + .filter_map(|game_key| self.nodes[*game_key].element.as_card()) + .collect(); + + Ok(cards) + } + + pub fn list_cards(&self) -> Vec { + self.nodes + .iter() + .filter_map(|node| node.1.element.as_card()) + .collect() + } + + pub fn list_games(&self) -> Vec { + self.nodes + .iter() + .filter_map(|node| node.1.element.as_game()) + .collect() + } + + pub fn list_cards_with_games(&self) -> Vec<(MicroSDCard, Vec)> { + self.nodes + .iter() + .filter_map(|node| { + node.1.element.as_card().map(|v| { + (v, { + node.1 + .links + .iter() + .filter_map(|key: &DefaultKey| self.nodes[*key].element.as_game()) + .collect() + }) + }) + }) + .collect() + } +} + +impl StoreData { + pub fn delete_hash(&mut self, key: &str) { + self.hashes.remove(key); + } + + pub fn update_hash(&mut self, key: &str, hash: u64) { + *self.hashes.entry(key.to_string()).or_insert(0) = hash; + } + + pub fn is_hash_changed(&self, id: &'_ str) -> Option { + info!("Checking Hashes. Current values: {:?}", self.hashes); + + let file_metadata: Vec<_> = get_steam_acf_files() + .ok()? + .filter_map(|f| fs::metadata(f.path()).ok()) + .collect(); + + let mut s = DefaultHasher::new(); + + for metadata in file_metadata { + metadata.len().hash(&mut s); + metadata + .modified() + .expect("Last Modified time to exist") + .hash(&mut s); + } + + let hash = s.finish(); + + match self.hashes.get(id) { + // Nothing is present for this card. + None => Some(hash), + Some(value) => { + // Hashes match so we have no updates + if *value == hash { + None + } else { + Some(hash) + } + } + } + } } +#[derive(Debug)] pub struct Store { - data: RwLock, - file: Option, + data: RwLock, + file: Option, } impl Store { - pub fn new(file: Option) -> Self { - Store { - data: RwLock::new(StoreData { - nodes: SlotMap::new(), - node_ids: HashMap::new(), - }), - file, - } - } - - pub fn read_from_file(file: PathBuf) -> Result { - let contents = read_to_string(&file).map_err(|e| Error::from(e))?; - let data: StoreData = serde_json::from_str(&contents).map_err(|e| Error::from(e))?; - Ok(Store { - data: RwLock::new(data), - file: Some(file), - }) - } - - #[allow(dead_code)] - pub fn set_file(&mut self, file: PathBuf) { - self.file = Some(file); - } - - pub fn write_to_file(&self) -> Result<(), Error> { - write( - self.file - .as_ref() - .ok_or(Error::from_str("No Path specified"))?, - serde_json::to_string(&self.data)?, - )?; - Ok(()) - } - - fn try_write_to_file(&self) { - if self.file.is_none() { - return; - } - - if let Err(err) = self.write_to_file() { - error!("Unable to write datastore to file: {}", err); - } - } + pub fn new(file: Option) -> Self { + Store { + data: RwLock::new(StoreData { + version: Version::parse(PACKAGE_VERSION).unwrap(), + nodes: SlotMap::new(), + node_ids: HashMap::new(), + hashes: HashMap::new(), + }), + file, + } + } + + pub fn read_from_file(file: PathBuf) -> Result { + let contents = read_to_string(&file).map_err(|e| Error::from(e))?; + let store_data: StoreData = serde_json::from_str(&contents).map_err(|e| Error::from(e))?; + Ok(Store { + data: RwLock::new(store_data), + file: Some(file), + }) + } + + #[allow(dead_code)] + pub fn set_file(&mut self, file: PathBuf) { + self.file = Some(file); + } + + pub fn write_to_file(&self) -> Result<(), Error> { + write( + self.file + .as_ref() + .ok_or(Error::from_str("No Path specified"))?, + serde_json::to_string(&self.data)?, + )?; + Ok(()) + } + + fn try_write_to_file(&self) { + if self.file.is_none() { + return; + } + + if let Err(err) = self.write_to_file() { + error!("Unable to write datastore to file: {}", err); + } + } pub fn validate(&self) -> bool { let data = self.data.read().unwrap(); @@ -315,7 +377,11 @@ impl Store { pub fn clean_up(&self) { let mut data = self.data.write().unwrap(); - let cleaned_node_ids: HashMap = data.node_ids.iter().map(|f| (f.0.trim().to_string(), *f.1)).collect(); + let cleaned_node_ids: HashMap = data + .node_ids + .iter() + .map(|f| (f.0.trim().to_string(), *f.1)) + .collect(); data.node_ids = cleaned_node_ids; @@ -323,85 +389,94 @@ impl Store { match node.1.element { StoreElement::Card(ref mut card) => { card.uid = card.uid.trim().to_string(); - }, - StoreElement::Game(_) => {}, + } + StoreElement::Game(_) => {} } } } - pub fn add_card(&self, id: String, card: MicroSDCard) { - self.data.write().unwrap().add_card(id, card); - self.try_write_to_file() - } + pub fn add_card(&self, id: String, card: MicroSDCard) { + self.data.write().unwrap().add_card(id, card); + self.try_write_to_file() + } - pub fn add_game(&self, id: String, game: Game) { - self.data.write().unwrap().add_game(id, game); - self.try_write_to_file() - } + pub fn add_game(&self, id: String, game: Game) { + self.data.write().unwrap().add_game(id, game); + self.try_write_to_file() + } - pub fn update_card(&self, card_id: &str, func: F) -> Result<(), Error> - where - F: FnMut(&mut MicroSDCard) -> Result<(), Error>, - { - self.data.write().unwrap().update_card(card_id, func)?; - self.try_write_to_file(); - Ok(()) - } + pub fn update_card(&self, card_id: &str, func: F) -> Result<(), Error> + where + F: FnMut(&mut MicroSDCard) -> Result<(), Error>, + { + self.data.write().unwrap().update_card(card_id, func)?; + self.try_write_to_file(); + Ok(()) + } - pub fn link(&self, a_id: &str, b_id: &str) -> Result<(), Error> { - self.data.write().unwrap().link(a_id, b_id)?; - self.try_write_to_file(); - Ok(()) - } + pub fn link(&self, a_id: &str, b_id: &str) -> Result<(), Error> { + self.data.write().unwrap().link(a_id, b_id)?; + self.try_write_to_file(); + Ok(()) + } pub fn unlink(&self, a_id: &str, b_id: &str) -> Result<(), Error> { - self.data - .write() - .unwrap() - .unlink(a_id, b_id)?; - self.try_write_to_file(); - Ok(()) - } - - pub fn remove_element(&self, game_id: &str) -> Result<(), Error> { - self.data.write().unwrap().remove_item(game_id)?; - self.try_write_to_file(); - Ok(()) - } - - pub fn contains_element(&self, id: &str) -> bool { - self.data.read().unwrap().contains_element(id) - } - - pub fn get_card(&self, card_id: &str) -> Result { - self.data.read().unwrap().get_card(card_id) - } - - pub fn get_game(&self, game_id: &str) -> Result { - self.data.read().unwrap().get_game(game_id) - } - - pub fn get_card_and_games(&self, card_id: &str) -> Result<(MicroSDCard, Vec), Error> { - self.data.read().unwrap().get_card_and_games(card_id) - } - - pub fn get_games_on_card(&self, card_id: &str) -> Result, Error> { - self.data.read().unwrap().get_games_on_card(card_id) - } - - pub fn get_cards_for_game(&self, game_id: &str) -> Result, Error> { - self.data.read().unwrap().get_cards_for_game(game_id) - } - - pub fn list_cards(&self) -> Vec { - self.data.read().unwrap().list_cards() - } - - pub fn list_games(&self) -> Vec { - self.data.read().unwrap().list_games() - } - - pub fn list_cards_with_games(&self) -> Vec<(MicroSDCard, Vec)> { - self.data.read().unwrap().list_cards_with_games() - } + self.data.write().unwrap().unlink(a_id, b_id)?; + self.try_write_to_file(); + Ok(()) + } + + pub fn remove_element(&self, id: &str) -> Result<(), Error> { + { + let mut lock = self.data.write().unwrap(); + lock.remove_item(id)?; + lock.delete_hash(id); + } + self.try_write_to_file(); + Ok(()) + } + + pub fn contains_element(&self, id: &str) -> bool { + self.data.read().unwrap().contains_element(id) + } + + pub fn get_card(&self, card_id: &str) -> Result { + self.data.read().unwrap().get_card(card_id) + } + + pub fn get_game(&self, game_id: &str) -> Result { + self.data.read().unwrap().get_game(game_id) + } + + pub fn get_card_and_games(&self, card_id: &str) -> Result<(MicroSDCard, Vec), Error> { + self.data.read().unwrap().get_card_and_games(card_id) + } + + pub fn get_games_on_card(&self, card_id: &str) -> Result, Error> { + self.data.read().unwrap().get_games_on_card(card_id) + } + + pub fn get_cards_for_game(&self, game_id: &str) -> Result, Error> { + self.data.read().unwrap().get_cards_for_game(game_id) + } + + pub fn list_cards(&self) -> Vec { + self.data.read().unwrap().list_cards() + } + + pub fn list_games(&self) -> Vec { + self.data.read().unwrap().list_games() + } + + pub fn list_cards_with_games(&self) -> Vec<(MicroSDCard, Vec)> { + self.data.read().unwrap().list_cards_with_games() + } + + pub fn is_hash_changed(&self, key: &str) -> Option { + self.data.read().unwrap().is_hash_changed(key) + } + + pub fn update_hash(&self, key: &str, hash: u64) { + self.data.write().unwrap().update_hash(key, hash) + } } diff --git a/backend/src/dto.rs b/backend/src/dto.rs index 9cfcc09..c0cd43a 100644 --- a/backend/src/dto.rs +++ b/backend/src/dto.rs @@ -1,25 +1,11 @@ -use serde::{Deserialize, Serialize}; use crate::err::Error; +use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Name { - pub name: String, -} - -impl From<&str> for Name { - fn from(value: &str) -> Self { - return Name { - name: value.to_string(), - }; - } -} - -impl From for Name { - fn from(value: String) -> Self { - return Name { - name: value.to_string(), - }; - } +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +pub enum CardEvent { + Inserted, + Removed, + Updated } fn default_true() -> bool { @@ -28,10 +14,10 @@ fn default_true() -> bool { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct MicroSDCard { - pub uid: String, - pub libid: String, - - pub name: String, + pub uid: String, + pub libid: String, + + pub name: String, #[serde(default)] pub position: u32, #[serde(default)] @@ -43,7 +29,7 @@ impl MicroSDCard { if self.uid != other.uid { return Error::new_res("uid's did not match"); } - + if self.libid != other.libid { return Error::new_res("libid's did not match"); } @@ -58,10 +44,10 @@ impl MicroSDCard { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Game { - pub uid: String, - pub name: String, - pub size: u64, - - #[serde(default="default_true")] - pub is_steam: bool + pub uid: String, + pub name: String, + pub size: u64, + + #[serde(default = "default_true")] + pub is_steam: bool, } diff --git a/backend/src/env.rs b/backend/src/env.rs index d5891d4..b8a5034 100644 --- a/backend/src/env.rs +++ b/backend/src/env.rs @@ -1,5 +1,5 @@ -use std::path::Path; use log::warn; +use std::path::Path; pub const PORT: u16 = 12412; // TODO replace with something unique @@ -10,40 +10,42 @@ pub const PACKAGE_AUTHORS: &'static str = env!("CARGO_PKG_AUTHORS"); const TEMPDIR: &'static str = "/tmp/MicroSDeck"; pub fn get_data_dir() -> String { - return match std::env::var("DECKY_PLUGIN_RUNTIME_DIR") { - Ok(loc) => loc.to_string(), - Err(_) => { - warn!("Unable to find \"DECKY_PLUGIN_RUNTIME_DIR\" in env. Assuming Dev mode & using temporary directory"); - TEMPDIR.to_string() + "/data" - }, - }; + return match std::env::var("DECKY_PLUGIN_RUNTIME_DIR") { + Ok(loc) => loc.to_string(), + Err(_) => { + warn!("Unable to find \"DECKY_PLUGIN_RUNTIME_DIR\" in env. Assuming Dev mode & using temporary directory"); + TEMPDIR.to_string() + "/data" + } + }; } pub fn get_log_dir() -> String { - return match std::env::var("DECKY_PLUGIN_LOG_DIR") { - Ok(loc) => loc.to_string(), - Err(_) => { - warn!("Unable to find \"DECKY_PLUGIN_LOG_DIR\" in env. Assuming Dev mode & using temporary directory"); - TEMPDIR.to_string() + "/log" - }, - }; + return match std::env::var("DECKY_PLUGIN_LOG_DIR") { + Ok(loc) => loc.to_string(), + Err(_) => { + warn!("Unable to find \"DECKY_PLUGIN_LOG_DIR\" in env. Assuming Dev mode & using temporary directory"); + TEMPDIR.to_string() + "/log" + } + }; } pub fn get_file_path(file_name: &str, get_base_dir: &dyn Fn() -> String) -> Option { - let dir = get_base_dir(); - + let dir = get_base_dir(); - Path::new(dir.as_str()).join(file_name).to_str().map(|v| v.to_string()) + Path::new(dir.as_str()) + .join(file_name) + .to_str() + .map(|v| v.to_string()) } pub fn get_file_path_and_create_directory( - file_name: &str, - get_base_dir: &dyn Fn() -> String, + file_name: &str, + get_base_dir: &dyn Fn() -> String, ) -> Option { - let dir = get_base_dir(); + let dir = get_base_dir(); - if let Err(_) = std::fs::create_dir_all(Path::new(dir.as_str())) { - return None; - } + if let Err(_) = std::fs::create_dir_all(Path::new(dir.as_str())) { + return None; + } - get_file_path(file_name, get_base_dir) + get_file_path(file_name, get_base_dir) } diff --git a/backend/src/err.rs b/backend/src/err.rs index 2d5bf97..05ba25b 100644 --- a/backend/src/err.rs +++ b/backend/src/err.rs @@ -7,57 +7,57 @@ use actix_web::ResponseError; #[derive(Debug)] pub enum Error { - Error(String), + Error(String), } impl Error { - pub fn new_boxed(value: &str) -> Box { - Box::new(Error::Error(value.to_string())) - } + pub fn new_boxed(value: &str) -> Box { + Box::new(Error::Error(value.to_string())) + } - pub fn from_str(value: &str) -> Self { - Error::Error(value.to_string()) - } + pub fn from_str(value: &str) -> Self { + Error::Error(value.to_string()) + } - pub fn new_res(value: &str) -> Result { - Err(Error::Error(value.to_string())) - } + pub fn new_res(value: &str) -> Result { + Err(Error::Error(value.to_string())) + } } impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - // Both underlying errors already impl `Display`, so we defer to - // their implementations. - Error::Error(err) => write!(f, "Error: {}", err), - } - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + // Both underlying errors already impl `Display`, so we defer to + // their implementations. + Error::Error(err) => write!(f, "Error: {}", err), + } + } } impl From for Error { - fn from(e: T) -> Self { - Error::Error(e.to_string()) - } + fn from(e: T) -> Self { + Error::Error(e.to_string()) + } } impl ResponseError for Error { - fn status_code(&self) -> actix_web::http::StatusCode { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - - fn error_response(&self) -> actix_web::HttpResponse { - let res = actix_web::HttpResponse::new(self.status_code()); - res.set_body(actix_web::body::BoxBody::new(format!("{}", self))) - } + fn status_code(&self) -> actix_web::http::StatusCode { + actix_web::http::StatusCode::INTERNAL_SERVER_ERROR + } + + fn error_response(&self) -> actix_web::HttpResponse { + let res = actix_web::HttpResponse::new(self.status_code()); + res.set_body(actix_web::body::BoxBody::new(format!("{}", self))) + } } impl ResponseError for Box { - fn status_code(&self) -> actix_web::http::StatusCode { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - - fn error_response(&self) -> actix_web::HttpResponse { - let res = actix_web::HttpResponse::new(self.status_code()); - res.set_body(actix_web::body::BoxBody::new(format!("{}", self))) - } + fn status_code(&self) -> actix_web::http::StatusCode { + actix_web::http::StatusCode::INTERNAL_SERVER_ERROR + } + + fn error_response(&self) -> actix_web::HttpResponse { + let res = actix_web::HttpResponse::new(self.status_code()); + res.set_body(actix_web::body::BoxBody::new(format!("{}", self))) + } } diff --git a/backend/src/log.rs b/backend/src/log.rs index 9a7ca6c..19bbfea 100644 --- a/backend/src/log.rs +++ b/backend/src/log.rs @@ -1,62 +1,76 @@ use chrono; use log::{Level, Metadata, Record}; +use std::env; use std::fs::{File, OpenOptions}; use std::io::prelude::*; +use std::str::FromStr; use crate::env::{get_file_path_and_create_directory, get_log_dir}; +use crate::err::Error; -pub struct Logger(File); +pub struct Logger { + file: File, + max_level: Level, +} impl Logger { - pub fn to_file(&self) -> &File { - &self.0 - } - - pub fn new() -> Option { - OpenOptions::new() - .write(true) - .append(true) - .create(true) - .open( - get_file_path_and_create_directory("backend.log", &get_log_dir) - .expect("The log file to exist."), - ) - .map(|f| Logger(f)) - .ok() - } + pub fn to_file(&self) -> &File { + &self.file + } + + pub fn new() -> Option { + let file_path = get_file_path_and_create_directory("backend.log", &get_log_dir) + .expect("to retrieve the log file path"); + + let file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(&file_path) + .ok()?; + + let max_level = env::var("LOG_LEVEL") + .map_err(|e| Error::from(e)) + .and_then(|v| Level::from_str(&v).map_err(|e| Error::from(e))) + .unwrap_or(Level::Info); + + println!("Logging enabled to {file_path} with level {max_level}"); + + Some(Logger { file, max_level }) + } } impl log::Log for Logger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= Level::Info && metadata.target() != "tracing::span" - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - let current_time = chrono::offset::Local::now(); - - println!( - "{} {}: {}", - record.level(), - current_time.format("%d/%m/%Y %H:%M:%S"), - record.args() - ); - - let message = format!( - "{} {} @ {}:{} {} \"{}\"", - current_time.naive_utc(), - record.level(), - record.file().unwrap_or("UNKNOWN"), - record.line().unwrap_or(0), - record.metadata().target(), - record.args() - ); - - if let Err(e) = writeln!(self.to_file(), "{message}") { - eprintln!("Couldn't write to file: {}", e); - } - } - } - - fn flush(&self) {} + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= self.max_level // && metadata.target() != "tracing::span" + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + let current_time = chrono::offset::Local::now(); + + println!( + "{} {}: {}", + current_time.format("%H:%M:%S"), + record.level(), + record.args() + ); + + let message = format!( + "{} {} @ {}:{} {} \"{}\"", + current_time.naive_utc(), + record.level(), + record.file().unwrap_or("UNKNOWN"), + record.line().unwrap_or(0), + record.metadata().target(), + record.args() + ); + + if let Err(e) = writeln!(self.to_file(), "{message}") { + eprintln!("Couldn't write to file: {}", e); + } + } + } + + fn flush(&self) {} } diff --git a/backend/src/main.rs b/backend/src/main.rs index 08c4671..65e4234 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -4,16 +4,16 @@ mod dto; mod env; mod err; mod log; -mod watch; mod sdcard; mod steam; +mod watch; -use crate::api::config; +use crate::{api::config, dto::CardEvent}; use crate::ds::Store; use crate::env::*; use crate::log::Logger; use crate::watch::start_watch; -use ::log::{info, trace, error}; +use ::log::{debug, error, info}; use actix_cors::Cors; use actix_web::{web, App, HttpServer}; use env::get_data_dir; @@ -21,76 +21,73 @@ use err::Error; use futures::{pin_mut, select, FutureExt}; use once_cell::sync::Lazy; use simplelog::LevelFilter; -use tokio::sync::broadcast::{self, Sender}; use std::path::PathBuf; use std::process::exit; use std::sync::Arc; +use tokio::sync::broadcast::{self, Sender}; static LOGGER: Lazy = Lazy::new(|| Logger::new().expect("Logger to be created")); pub fn init() -> Result<(), ::log::SetLoggerError> { - ::log::set_logger(&*LOGGER).map(|()| ::log::set_max_level(LevelFilter::Trace)) + ::log::set_logger(&*LOGGER).map(|()| ::log::set_max_level(LevelFilter::Trace)) } type MainResult = Result<(), Error>; -async fn run_server(datastore: Arc, sender: Sender<()>) -> MainResult { - // let log_filepath = format!("/tmp/{}.log", PACKAGE_NAME); - // WriteLogger::init( - // #[cfg(debug_assertions)] - // { - // LevelFilter::Debug - // }, - // #[cfg(not(debug_assertions))] - // { - // LevelFilter::Info - // }, - // Default::default(), - // std::fs::File::create(&log_filepath).unwrap(), - // ) - // .unwrap(); - - info!("Starting HTTP server..."); - - HttpServer::new(move || { - - let cors = Cors::default() - .allow_any_header() - .allow_any_method() - .allow_any_origin() - .max_age(3600); - - App::new() - .wrap(cors) - // .app_data(web::Data::new(api::AppState{datastore: datastore.clone()})) - .app_data(web::Data::new(datastore.clone())) - .app_data(web::Data::new(sender.clone())) - .configure(config) - }) - .workers(1) - .bind(("0.0.0.0", PORT))? - .run() - .await - .map_err(|err| err.into()) +async fn run_server(datastore: Arc, sender: Sender) -> MainResult { + + info!("Starting HTTP server..."); + + HttpServer::new(move || { + let cors = Cors::default() + .allow_any_header() + .allow_any_method() + .allow_any_origin() + .max_age(3600); + + App::new() + .wrap(cors) + // .app_data(web::Data::new(api::AppState{datastore: datastore.clone()})) + .app_data(web::Data::new(datastore.clone())) + .app_data(web::Data::new(sender.clone())) + .configure(config) + }) + .workers(1) + .bind(("0.0.0.0", PORT))? + .run() + .await + .map_err(|err| err.into()) } - #[tokio::main(worker_threads = 1)] async fn main() { - if cfg!(debug_assertions) { - std::env::set_var("RUST_BACKTRACE", "1"); - } - - let store_path = PathBuf::from( - &std::env::var("STORE_PATH").unwrap_or( - get_file_path_and_create_directory("store", &get_data_dir) - .expect("should retrieve data directory"), - ), - ); + if cfg!(debug_assertions) { + std::env::set_var("RUST_BACKTRACE", "1"); + } - println!("Loading from store \"{:?}\"", store_path); - let store: Arc = - Arc::new(Store::read_from_file(store_path.clone()).unwrap_or(Store::new(Some(store_path)))); + match init() { + Err(err) => { + error!("Unable to Initialize:\n{}", err); + return; + } + Ok(()) => debug!("Initialized..."), + } + + info!( + "{}@{} by {}", + PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_AUTHORS + ); + + let store_path = PathBuf::from( + &std::env::var("STORE_PATH").unwrap_or( + get_file_path_and_create_directory("store", &get_data_dir) + .expect("should retrieve data directory"), + ), + ); + + debug!("Loading from store {:?}", store_path); + let store: Arc = + Arc::new(Store::read_from_file(store_path.clone()).unwrap_or(Store::new(Some(store_path)))); store.clean_up(); @@ -99,38 +96,32 @@ async fn main() { exit(1); } - info!("Database Started..."); - - match init() { - Err(err) => { - eprintln!("Unable to Initialize:\n{}", err); - return; - } - Ok(()) => trace!("Initialized..."), - } + info!("Database Started..."); + info!("Starting Program..."); - info!( - "{}@{} by {}", - PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_AUTHORS - ); + let (txtx, _) = broadcast::channel::(1); - info!("Starting Program..."); + let server_future = run_server(store.clone(), txtx.clone()).fuse(); - let (txtx, _) = broadcast::channel::<()>(1); + let watch_future = start_watch(store.clone(), txtx.clone()).fuse(); - let server_future = run_server(store.clone(), txtx.clone()).fuse(); + pin_mut!(server_future, watch_future); - let watch_future = start_watch(store.clone(), txtx.clone()).fuse(); + select! { + result = server_future => match result { + Ok(_) => info!("Server ran to completion..."), + Err(err) => error!("Server exited with error: {err}") + }, + result = watch_future => match result { + Ok(_) => info!("Watch ran to completion.."), + Err(err) => error!("Watch exited with error: {err}"), + }, + }; - pin_mut!(server_future, watch_future); - - select! { - result = server_future => result.expect("Server Exited..."), - result = watch_future => result.expect("Watch Exited..."), - }; - - info!("Saving Database"); - store.write_to_file().expect("Saving Datatbase to succeed"); + info!("Saving Database"); + if let Err(err) = store.write_to_file() { + error!("Failed to write datastore to file {err}"); + } - info!("Exiting..."); + info!("Exiting..."); } diff --git a/backend/src/sdcard.rs b/backend/src/sdcard.rs index e57426e..e09f913 100644 --- a/backend/src/sdcard.rs +++ b/backend/src/sdcard.rs @@ -1,14 +1,28 @@ -use std::fs::read_to_string; +use std::fs::{self, read_to_string, DirEntry}; + +use crate::err::Error; + +pub const STEAM_LIB_FILE: &'static str = "/run/media/mmcblk0p1/libraryfolder.vdf"; +pub const STEAM_LIB_FOLDER: &'static str = "/run/media/mmcblk0p1/steamapps/"; pub fn is_card_inserted() -> bool { - std::fs::metadata("/sys/block/mmcblk0").is_ok() + std::fs::metadata("/sys/block/mmcblk0").is_ok() } // Based on https://www.cameramemoryspeed.com/sd-memory-card-faq/reading-sd-card-cid-serial-psn-internal-numbers/ pub fn get_card_cid() -> Option { - read_to_string("/sys/block/mmcblk0/device/cid").map(|v| v.trim().to_string()).ok() + read_to_string("/sys/block/mmcblk0/device/cid") + .map(|v| v.trim().to_string()) + .ok() } pub fn is_card_steam_formatted() -> bool { - std::fs::metadata("/run/media/mmcblk0p1/libraryfolder.vdf").is_ok() -} \ No newline at end of file + std::fs::metadata("/run/media/mmcblk0p1/libraryfolder.vdf").is_ok() +} + +pub fn get_steam_acf_files() -> Result, Error> { + Ok(fs::read_dir(STEAM_LIB_FOLDER)? + .into_iter() + .filter_map(Result::ok) + .filter(|f| f.path().extension().unwrap_or_default().eq("acf"))) +} diff --git a/backend/src/steam.rs b/backend/src/steam.rs index c5f1ae9..a6f2de4 100644 --- a/backend/src/steam.rs +++ b/backend/src/steam.rs @@ -1,33 +1,33 @@ -use std::fmt::{Display, Debug}; +use std::fmt::{Debug, Display}; use serde::Deserialize; #[derive(Deserialize, Debug)] pub struct LibraryFolder { - pub contentid: String, - pub label: String, + pub contentid: String, + pub label: String, } #[derive(Deserialize)] pub struct AppState { - pub appid: String, - pub universe: i32, - pub name: String, - #[serde(rename(deserialize = "StateFlags"))] - pub state_flags: Option, - pub installdir: String, - #[serde(rename(deserialize = "SizeOnDisk"))] - pub size_on_disk: u64, + pub appid: String, + pub universe: i32, + pub name: String, + #[serde(rename(deserialize = "StateFlags"))] + pub state_flags: Option, + pub installdir: String, + #[serde(rename(deserialize = "SizeOnDisk"))] + pub size_on_disk: u64, } impl Display for AppState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "({}, {})", self.appid, self.name) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.appid, self.name) + } } impl Debug for AppState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "({}, {})", self.appid, self.name) - } -} \ No newline at end of file + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.appid, self.name) + } +} diff --git a/backend/src/watch.rs b/backend/src/watch.rs index 6d37715..ffa7f8a 100644 --- a/backend/src/watch.rs +++ b/backend/src/watch.rs @@ -1,187 +1,128 @@ use crate::{ds::Store, dto::*, err::Error, sdcard::*, steam::*}; -use log::{error, info, trace}; -use std::collections::hash_map::DefaultHasher; -use std::fs::DirEntry; -use std::hash::{Hash, Hasher}; -use std::{borrow::Borrow, collections::HashMap, fs, sync::Arc, time::Duration}; +use log::{error, info, trace, debug}; +use std::{borrow::Borrow, fs, sync::Arc, time::Duration}; use tokio::sync::broadcast::Sender; use tokio::time::interval; -const STEAM_LIB_FILE: &'static str = "/run/media/mmcblk0p1/libraryfolder.vdf"; -const STEAM_LIB_FOLDER: &'static str = "/run/media/mmcblk0p1/steamapps/"; - -fn get_steam_acf_files() -> Result, Error> { - Ok(fs::read_dir(STEAM_LIB_FOLDER)? - .into_iter() - .filter_map(Result::ok) - .filter(|f| f.path().extension().unwrap_or_default().eq("acf"))) +fn read_msd_directory(datastore: &Store) -> Result<(), Error> { + let cid = get_card_cid().ok_or(Error::from_str("Unable to retrieve CID from MicroSD card"))?; + let res = fs::read_to_string(STEAM_LIB_FILE)?; + + let library: LibraryFolder = keyvalues_serde::from_str(res.as_str())?; + + trace!("contentid: {}", library.contentid); + + let games: Vec = get_steam_acf_files()? + .filter_map(|f| fs::read_to_string(f.path()).ok()) + .filter_map(|s| keyvalues_serde::from_str(s.as_str()).ok()) + .collect(); + + trace!("Retrieved {} Games: {:?}", games.len(), games); + + if !datastore.contains_element(&cid) { + datastore.add_card( + cid.clone(), + MicroSDCard { + uid: cid.clone(), + libid: library.contentid.clone(), + name: library.label, + position: 0, + hidden: false, + }, + ); + } + + // Remove any games that are linked to the card in the database but on the card + let current_games = datastore.get_games_on_card(&cid)?; + for deleted_game in current_games + .iter() + .filter(|v| !games.iter().any(|g| g.appid == v.uid)) + { + datastore.unlink(&deleted_game.uid, &cid)? + } + + for game in games.iter() { + if !datastore.contains_element(&game.appid) { + datastore.add_game( + game.appid.clone(), + Game { + uid: game.appid.clone(), + name: game.name.clone(), + size: game.size_on_disk, + is_steam: true, + }, + ); + } + + datastore.link(&game.appid, &cid).expect("game to be added") + } + + Ok(()) } -struct ChangeSet { - hashes: HashMap, -} +pub async fn start_watch(datastore: Arc, sender: Sender) -> Result<(), Error> { + let mut interval = interval(Duration::from_secs(5)); -impl ChangeSet { - pub fn new() -> Self { - ChangeSet { - hashes: HashMap::new(), - } - } - - pub fn update(&mut self, key: &String, hash: u64) { - *self.hashes.entry(key.clone()).or_insert(0) = hash; - } - - pub fn is_changed(&mut self, id: &String) -> Option { - let file_metadata: Vec<_> = get_steam_acf_files() - .ok()? - .filter_map(|f| fs::metadata(f.path()).ok()) - .collect(); - - let mut s = DefaultHasher::new(); - - for metadata in file_metadata { - metadata.len().hash(&mut s); - metadata - .modified() - .expect("Last Modified time to exist") - .hash(&mut s); - } - - let hash = s.finish(); - - match self.hashes.get(id) { - // Nothing is present for this card. - None => Some(hash), - Some(value) => { - // Hashes match so we have no updates - if *value == hash { - None - } else { - Some(hash) - } - } - } - } -} + let mut card_inserted = false; -fn read_msd_directory(datastore: &Store) -> Result<(), Error> { - let cid = get_card_cid().ok_or(Error::from_str("Unable to retrieve CID from MicroSD card"))?; - let res = fs::read_to_string(STEAM_LIB_FILE)?; - - let library: LibraryFolder = keyvalues_serde::from_str(res.as_str())?; - - trace!("contentid: {}", library.contentid); - - let games: Vec = get_steam_acf_files()? - .filter_map(|f| fs::read_to_string(f.path()).ok()) - .filter_map(|s| keyvalues_serde::from_str(s.as_str()).ok()) - .collect(); - - trace!("Retrieved {} Games: {:?}", games.len(), games); - - if !datastore.contains_element(&cid) { - datastore.add_card( - cid.clone(), - MicroSDCard { - uid: cid.clone(), - libid: library.contentid.clone(), - name: library.label, - position: 0, - hidden: false, - }, - ); - } - - // Remove any games that are linked to the card in the database but on the card - let current_games = datastore.get_games_on_card(&cid)?; - for deleted_game in current_games - .iter() - .filter(|v| !games.iter().any(|g| g.appid == v.uid)) - { - datastore.unlink(&deleted_game.uid, &cid)? - } - - for game in games.iter() { - if !datastore.contains_element(&game.appid) { - datastore.add_game( - game.appid.clone(), - Game { - uid: game.appid.clone(), - name: game.name.clone(), - size: game.size_on_disk, - is_steam: true, - }, - ); - } - - datastore.link(&game.appid, &cid).expect("game to be added") - } - - Ok(()) -} + info!("Starting Watcher..."); + + loop { + interval.tick().await; + + debug!("Watch loop"); + + // No card no worries. + if !is_card_inserted() { + // The card has been removed since the last check + if card_inserted { + debug!("Card was removed"); + let _ = sender.send(CardEvent::Removed); + } + + card_inserted = false; + continue; + } + + // was the card inserted since the last check. + let card_changed = !card_inserted; + card_inserted = true; + + // There is no steam directory so it hasn't been formatted. + if !is_card_steam_formatted() { + debug!("card is not steam formatted"); + continue; + } + + let cid = match get_card_cid() { + Some(v) => v, + None => { + error!("Unable to read Card ID"); + continue; + } + }; + + if card_changed { + let _ = sender.send(CardEvent::Inserted); + } + + // Do we have changes in the steam directory. This should only occur when something has been added/deleted + let hash = match datastore.is_hash_changed(&cid) { + None => continue, + Some(v) => v, + }; + + info!("Watcher Detected update"); + + // Something went wrong during parsing. Not great + if let Err(err) = read_msd_directory(datastore.borrow()) { + error!("Problem reading MicroSD Card: \"{err}\""); + continue; + } + + // commit update + datastore.update_hash(&cid, hash); -pub async fn start_watch(datastore: Arc, sender: Sender<()>) -> Result<(), Error> { - let mut interval = interval(Duration::from_secs(5)); - - let mut changeset = ChangeSet::new(); - - let mut card_inserted = false; - - loop { - interval.tick().await; - - // No card no worries. - if !is_card_inserted() { - // The card has been removed since the last check - if card_inserted { - let _ = sender.send(()); - } - - card_inserted = false; - continue; - } - - // was the card inserted since the last check. - let card_changed = !card_inserted; - card_inserted = true; - - // There is no steam directory so it hasn't been formatted. - if !is_card_steam_formatted() { - continue; - } - - let cid = match get_card_cid() { - Some(v) => v, - None => { - error!("Unable to read Card ID"); - continue; - } - }; - - // Do we have changes in the steam directory. This should only occur when something has been added/deleted - let hash = match changeset.is_changed(&cid) { - None => { - // A new card has been inserted but no content on it changed. - if card_changed { - let _ = sender.send(()); - } - continue; - } - Some(v) => v, - }; - - info!("Watcher Detected update"); - - // Something went wrong during parsing. Not great - if let Err(err) = read_msd_directory(datastore.borrow()) { - error!("Problem reading MicroSD Card: \"{err}\""); - continue; - } - - // commit update - changeset.update(&cid, hash); - - let _ = sender.send(()); - } + let _ = sender.send(CardEvent::Updated); + } } diff --git a/lib/src/backend.ts b/lib/src/backend.ts index bf8df28..c53ca19 100644 --- a/lib/src/backend.ts +++ b/lib/src/backend.ts @@ -32,7 +32,7 @@ async function wrapFetch({ url, logger }: FetchProps, init?: RequestInit): Promi return undefined; } -export async function fetchEventPoll({ url, logger, signal }: FetchProps & { signal: AbortSignal }): Promise { +export async function fetchEventPoll({ url, logger, signal }: FetchProps & { signal: AbortSignal }): Promise { try { const result = await fetch(`${url}/listen`, { keepalive: true, @@ -44,7 +44,7 @@ export async function fetchEventPoll({ url, logger, signal }: FetchProps & { sig return false; } - return true; + return await result.json(); } catch (err) { logger?.Error("Fetch failed with error {err}", { err }); diff --git a/lib/src/state/MicoSDeckManager.ts b/lib/src/state/MicoSDeckManager.ts index aa314fe..85f4eac 100644 --- a/lib/src/state/MicoSDeckManager.ts +++ b/lib/src/state/MicoSDeckManager.ts @@ -103,7 +103,7 @@ export class MicroSDeckManager { // Server is down. Lets try again but back off a bit case undefined: this.logger?.Warn("Unable to contact Server. Backing off and waiting {sleepDelay}ms", { sleepDelay }); - await sleep(sleepDelay *= 1.5); + await sleep(sleepDelay = Math.min(sleepDelay * 1.5, 1000 * 60)); break; // We got an update. Time to refresh.