Skip to content

Commit

Permalink
feat: Added config api (#40)
Browse files Browse the repository at this point in the history
* Added API to modify config properties
* Added prior art to comment
* Updated mise tool version
* Added frontend Docs prompt and JSON support for config api
* Fixed refresh & updated visuals
* Updated Styling
* Removed unessecary label
* Added Traceing to settings api
* Fixed whitespace and wording
* Bumped version to 0.10.12
  • Loading branch information
CEbbinghaus authored Dec 17, 2024
1 parent ff42da3 commit 75623f7
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 58 deletions.
6 changes: 3 additions & 3 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# specify single or multiple versions
pnpm = '9.5.0'
node = '22.4.0'
rust = '1.79.0'
rust = "1.80.1"


[tasks."build"]
Expand All @@ -18,7 +18,7 @@ outputs = ['build/bin/backend']
[tasks."build:frontend"]
description = 'Build the Frontend'
run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -q -o frontend"
sources = ['package.json', 'lib/package.json', '{src,lib}/**/*.{ts,tsx,codegen}']
sources = ['package.json', 'lib/package.json', '{src,lib}/**/*.*']
outputs = ['dist/index.js']

[tasks."build:collect"]
Expand All @@ -36,4 +36,4 @@ run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -q -o copy"
[tasks."upload"]
depends = ["build"]
description = 'Upload MicroSDeck to the SteamDeck'
run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -q -o upload"
run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -q -o upload"
30 changes: 23 additions & 7 deletions backend/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
use crate::{
ds::Store,
dto::{CardEvent, Game, MicroSDCard},
env::PACKAGE_VERSION,
err::Error,
event::Event,
sdcard::{get_card_cid, is_card_inserted},
cfg::CONFIG, ds::Store, dto::{CardEvent, Game, MicroSDCard}, env::PACKAGE_VERSION, err::Error, event::Event, sdcard::{get_card_cid, is_card_inserted}
};
use actix_web::{
delete, get, http::StatusCode, post, web, Either, HttpResponse, HttpResponseBuilder, Responder,
delete, get, http::StatusCode, post, web::{self, Bytes}, Either, HttpResponse, HttpResponseBuilder, Responder,
Result,
};
use futures::StreamExt;
Expand All @@ -23,6 +18,8 @@ pub(crate) fn config(cfg: &mut web::ServiceConfig) {
.service(version)
.service(listen)
.service(save)
.service(get_setting_by_name)
.service(set_setting_by_name)
.service(get_current_card)
.service(get_current_card_id)
.service(get_current_card_and_games)
Expand Down Expand Up @@ -75,6 +72,25 @@ pub(crate) async fn listen(sender: web::Data<Sender<CardEvent>>) -> Result<HttpR
.streaming(event_stream))
}

#[get("/setting/{name}")]
#[instrument]
pub(crate) async fn get_setting_by_name(name: web::Path<String>) -> Result<impl Responder> {
trace!("HTTP GET /setting/{name}");

let result = CONFIG.read().await.get_property(&name)?;
Ok(result)
}

#[post("/setting/{name}")]
#[instrument]
pub(crate) async fn set_setting_by_name(body: Bytes, name: web::Path<String>) -> Result<impl Responder> {
trace!("HTTP POST /setting/{name}");

let value = String::from_utf8(body.to_vec()).map_err(|_| Error::from_str("Unable to decode body as utf8"))?;
CONFIG.write().await.set_property(&name, &value)?;
Ok(HttpResponse::Ok())
}

#[get("/list")]
#[instrument(skip(datastore))]
pub(crate) async fn list_cards_with_games(datastore: web::Data<Arc<Store>>) -> impl Responder {
Expand Down
129 changes: 118 additions & 11 deletions backend/src/cfg.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
use anyhow::Result;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
};
use tracing::Level;

use crate::DATA_DIR;
use crate::{err::Error, CONFIG_PATH};

lazy_static! {
pub static ref CONFIG_PATH: PathBuf = DATA_DIR.join("config.toml");
pub static ref CONFIG: Config = Config::load().unwrap_or_else(|| {
pub static ref CONFIG: RwLock<Config> = RwLock::new(Config::load().unwrap_or_else(|| {
let result = Config::new();
result.write().expect("Write to succeed");
result
});
}));
}

#[allow(clippy::upper_case_acronyms)]
Expand All @@ -30,26 +30,50 @@ pub enum LogLevel {
ERROR = 4,
}

#[derive(Serialize, Deserialize, Default)]
pub struct Startup {
pub skip_validate: bool,
pub skip_clean: bool,
}

#[derive(Serialize, Deserialize, Default)]
pub struct Frontend {
pub dismissed_docs: bool,
}
#[derive(Serialize, Deserialize)]
pub struct Config {
pub struct Backend {
pub port: u16,
pub scan_interval: u64,
pub store_file: PathBuf,
pub log_file: PathBuf,
#[serde(with = "LogLevel")]
pub log_level: Level,
pub startup: Startup,
}

impl Config {
pub fn new() -> Self {
Config {
impl Default for Backend {
fn default() -> Self {
Backend {
port: 12412,
scan_interval: 5000,
log_file: "microsdeck.log".into(),
store_file: "store".into(),
log_level: Level::INFO,
startup: Default::default(),
}
}
}

#[derive(Serialize, Deserialize, Default)]
pub struct Config {
pub backend: Backend,
pub frontend: Frontend,
}

impl Config {
pub fn new() -> Self {
Default::default()
}
pub fn write(&self) -> Result<()> {
self.write_to_file(&CONFIG_PATH)
}
Expand All @@ -66,11 +90,94 @@ impl Config {
Self::load_from_file(&CONFIG_PATH)
}
pub fn load_from_file(path: &'_ PathBuf) -> Option<Self> {
fs::read_to_string(path)
.ok()
.and_then(|val| Self::load_from_str(&val).ok())
let content = fs::read_to_string(path)
.ok();

if let Some(ref content) = content {
let result = Self::load_from_str(content);

match result {
Ok(val) => return Some(val),
Err(ref err) => eprintln!("Unable to deserialize config: \"{}\"", err),
}
} else {
eprintln!("No content found at config path \"{}\"", path.to_string_lossy());
}
None
}
pub fn load_from_str(content: &'_ str) -> Result<Self> {
Ok(toml::de::from_str::<Self>(content)?)
}
}

// TODO: Turn this Impl into a macro that generates the get and set functions
// Possibly using https://github.com/0xDEADFED5/set_field as the base
impl Config {
pub fn get_property(&self, name: &'_ str) -> Result<String, Error> {
let parts: Vec<&str> = name.split(":").collect();

match parts[..] {
["*"] => Ok(serde_json::to_string(&self).unwrap()),
["backend"] => Ok(serde_json::to_string(&self.backend).unwrap()),
["backend", "port"] => Ok(self.backend.port.to_string()),
["backend", "scan_interval"] => Ok(self.backend.scan_interval.to_string()),
["backend", "store_file"] => Ok(self.backend.store_file.to_string_lossy().to_string()),
["backend", "log_file"] => Ok(self.backend.log_file.to_string_lossy().to_string()),
["backend", "log_level"] => Ok(self.backend.log_level.to_string()),
["backend", "startup"] => Ok(serde_json::to_string(&self.backend.startup).unwrap()),
["backend", "startup", "skip_validate"] => Ok(self.backend.startup.skip_validate.to_string()),
["backend", "startup", "skip_clean"] => Ok(self.backend.startup.skip_clean.to_string()),
["frontend"] => Ok(serde_json::to_string(&self.frontend).unwrap()),
["frontend", "dismissed_docs"] => Ok(self.frontend.dismissed_docs.to_string()),
_ => Err(Error::from_str("Invalid property Name")),
}
}

pub fn set_property(&mut self, name: &'_ str, value: &'_ str) -> Result<(), Error> {
let parts: Vec<&str> = name.split(":").collect();

let wrong_value_err = Error::from_str(&format!("The value provided \"{value}\" did not match the expected type"));

match parts[..] {
["*"] => {
*self = serde_json::from_str(value).map_err(|_| wrong_value_err)?;
}
["backend"] => {
self.backend = serde_json::from_str(value).map_err(|_| wrong_value_err)?;
}
["backend", "port"] => {
self.backend.port = value.parse().map_err(|_| wrong_value_err)?;
}
["backend", "scan_interval"] => {
self.backend.scan_interval = value.parse().map_err(|_| wrong_value_err)?;
}
["backend", "store_file"] => {
self.backend.store_file = value.into();
}
["backend", "log_file"] => {
self.backend.log_file = value.into();
}
["backend", "log_level"] => {
self.backend.log_level = value.parse().map_err(|_| wrong_value_err)?;
}
["backend", "startup"] => {
self.backend.startup = serde_json::from_str(value).map_err(|_| wrong_value_err)?;
}
["backend", "startup", "skip_validate"] => {
self.backend.startup.skip_validate = value.parse().map_err(|_| wrong_value_err)?;
}
["backend", "startup", "skip_clean"] => {
self.backend.startup.skip_clean = value.parse().map_err(|_| wrong_value_err)?;
}
["frontend"] => {
self.frontend = serde_json::from_str(value).map_err(|_| wrong_value_err)?;
}
["frontend", "dismissed_docs"] => {
self.frontend.dismissed_docs = value.parse().map_err(|_| wrong_value_err)?;
}
_ => return Err(Error::from_str("Invalid property Name")),
}

self.write().map_err(|err| Error::from_str(&err.to_string()))
}
}
7 changes: 4 additions & 3 deletions backend/src/ds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ pub struct StoreData {
hashes: HashMap<String, u64>,
}

impl StoreData {
impl StoreData {
#[instrument(skip(self))]
pub fn add_card(&mut self, id: String, card: MicroSDCard) {
self.node_ids
Expand Down Expand Up @@ -389,10 +389,11 @@ impl Store {
result
}

/// Removes any whitespace from the card uid
/// cleans up any data to make it consistent with what we expect.
pub fn clean_up(&self) {
let mut data = self.data.write().unwrap();


// Removes any whitespace from the card uid
let cleaned_node_ids: HashMap<String, DefaultKey> = data
.node_ids
.iter()
Expand Down
5 changes: 5 additions & 0 deletions backend/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ lazy_static! {
TEMPDIR.to_string() + "/log"
}
});

pub static ref CONFIG_PATH: PathBuf = match std::env::var("DECKY_CONFIG_PATH") {
Ok(loc) => PathBuf::from(loc),
Err(_) => DATA_DIR.join("config.toml")
};
}

pub fn get_file_path_and_create_directory(
Expand Down
15 changes: 10 additions & 5 deletions backend/src/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ const IGNORED_MODULES: [&'static str; 6] = [
"mio::poll",
];

pub fn create_subscriber() {
let log_file_path = get_file_path_and_create_directory(&CONFIG.log_file, &LOG_DIR)
.expect("Log file to be created");
pub async fn create_subscriber() {
let (log_file, log_level) = {
let config = CONFIG.read().await;
(config.backend.log_file.clone(), config.backend.log_level)
};

let log_file_path =
get_file_path_and_create_directory(&log_file, &LOG_DIR).expect("Log file to be created");

let file = std::fs::OpenOptions::new()
.create(true)
Expand All @@ -27,7 +32,7 @@ pub fn create_subscriber() {
.json()
.with_writer(file)
.with_filter(tracing_subscriber::filter::LevelFilter::from_level(
CONFIG.log_level,
log_level,
))
.with_filter(filter::filter_fn(|metadata| {
metadata
Expand All @@ -43,7 +48,7 @@ pub fn create_subscriber() {
tracing_subscriber::fmt::layer()
.pretty()
.with_filter(tracing_subscriber::filter::LevelFilter::from_level(
CONFIG.log_level,
log_level,
))
.with_filter(filter::filter_fn(|metadata| {
metadata
Expand Down
Loading

0 comments on commit 75623f7

Please sign in to comment.