Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adding config api #40

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 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,8 +18,8 @@ outputs = ['build/bin/backend']
[tasks."build:frontend"]
description = 'Build the Frontend'
run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -o frontend"
sources = ['package.json', 'lib/package.json', '{src,lib}/**/*.{ts,tsx,codegen}']
outputs = ['dist/index.js']
# sources = ['package.json', 'lib/package.json', '{src,lib}/**/*.*']
# outputs = ['dist/index.js']

[tasks."build:collect"]
depends = ["build:backend", "build:frontend"]
Expand All @@ -36,4 +36,4 @@ run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -o copy"
[tasks."upload"]
depends = ["build"]
description = 'Upload MicroSDeck to the SteamDeck'
run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -o upload"
run = "node --no-warnings=ExperimentalWarning 'util/build.mjs' -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
130 changes: 119 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,95 @@ 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 compliant
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
Loading