Community settings WIP

This commit is contained in:
NGnius (Graham) 2023-12-22 16:26:50 -05:00
parent 9e1f7c0620
commit 508c6ceb9e
15 changed files with 2010 additions and 0 deletions

View file

@ -0,0 +1,65 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "community_settings_core"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

View file

@ -0,0 +1,9 @@
[package]
name = "community_settings_core"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0", features = ["derive"] }

View file

@ -0,0 +1 @@
pub mod v1;

View file

@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct Metadata {
pub name: String,
pub steam_app_id: u32,
pub steam_user_id: u64,
pub stream_username: String,
/// Should always be a valid u128, but some parsers do not support that
pub id: String,
pub config: super::Config,
}
impl Metadata {
pub fn set_id(&mut self, id: u128) {
self.id = id.to_string()
}
pub fn get_id(&self) -> u128 {
self.id.parse().expect("metadata id must be u128")
}
}

View file

@ -0,0 +1,5 @@
mod metadata;
mod setting;
pub use metadata::Metadata;
pub use setting::*;

View file

@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
/// Base setting file containing all information for all components
#[derive(Serialize, Deserialize, Clone)]
pub struct Config {
pub cpus: Vec<Cpu>,
pub gpu: Gpu,
pub battery: Battery,
}
#[derive(Serialize, Deserialize, Clone, Copy)]
pub struct MinMax<T> {
pub max: Option<T>,
pub min: Option<T>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct Battery {
pub charge_rate: Option<u64>,
pub charge_mode: Option<String>,
#[serde(default)]
pub events: Vec<BatteryEvent>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct BatteryEvent {
pub trigger: String,
pub charge_rate: Option<u64>,
pub charge_mode: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct Cpu {
pub online: bool,
pub clock_limits: Option<MinMax<u64>>,
pub governor: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct Gpu {
pub fast_ppt: Option<u64>,
pub slow_ppt: Option<u64>,
pub tdp: Option<u64>,
pub tdp_boost: Option<u64>,
pub clock_limits: Option<MinMax<u64>>,
pub slow_memory: bool,
}

1538
backend/community_settings_srv/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
[package]
name = "community_settings_srv"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
community_settings_core = { version = "0.1", path = "../community_settings_core" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
ron = "0.8"
clap = { version = "4", features = ["derive", "std", "color"], default-features = false }
tokio = { version = "1", features = ["full"] }
actix-web = { version = "4.4" }
mime = { version = "0.3.17" }
# logging
log = "0.4"
simplelog = "0.12"

View file

@ -0,0 +1,7 @@
# community_settings_srv
Back-end for browsing community-contributed settings files for games.
### Technical
This does not use a database because I'm trying to speedrun the destruction of any semblance of performance for my filesystem. Everything is stored as a file, with symlinks to make it possible to find files multiple ways.

View file

@ -0,0 +1,120 @@
use actix_web::{get, web, Responder, http::header};
use crate::cli::Cli;
use crate::file_util;
fn special_settings() -> community_settings_core::v1::Metadata {
community_settings_core::v1::Metadata {
name: "Zeroth the Least".to_owned(),
steam_app_id: 1675200,
steam_user_id: 76561198116690523,
stream_username: "NGnius".to_owned(),
id: 0.to_string(),
config: community_settings_core::v1::Config {
cpus: vec![
community_settings_core::v1::Cpu {
online: true,
clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }),
governor: "Michaëlle Jean".to_owned(),
},
community_settings_core::v1::Cpu {
online: false,
clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }),
governor: "Adrienne Clarkson".to_owned(),
},
community_settings_core::v1::Cpu {
online: true,
clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }),
governor: "Michael Collins".to_owned(),
}
],
gpu: community_settings_core::v1::Gpu {
fast_ppt: Some(1),
slow_ppt: Some(1),
tdp: None,
tdp_boost: None,
clock_limits: Some(community_settings_core::v1::MinMax { max: Some(1), min: Some(0) }),
slow_memory: false,
},
battery: community_settings_core::v1::Battery {
charge_rate: Some(42),
charge_mode: Some("nuclear fusion".to_owned()),
events: vec![
community_settings_core::v1::BatteryEvent {
trigger: "anything but one on a gun".to_owned(),
charge_rate: Some(42),
charge_mode: Some("neutral".to_owned()),
}
],
}
}
}
}
#[get("/api/setting/{id}")]
pub async fn get_setting_handler(
id: web::Path<String>,
accept: web::Header<header::Accept>,
cli: web::Data<&'static Cli>,
) -> std::io::Result<impl Responder> {
println!("Accept: {}", accept.to_string());
let id: u128 = match id.parse() {
Ok(x) => x,
Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("invalid setting id `{}` (should be u128): {}", id, e))),
};
let preferred = accept.preference();
if super::is_mime_type_ron_capable(&preferred) {
// Send RON
let ron = if id != 0 {
let path = file_util::setting_path_by_id(&cli.folder, id, file_util::RON_EXTENSION);
if !path.exists() {
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, format!("setting id {} does not exist", id)));
}
// TODO? cache this instead of always loading it from file
let reader = std::io::BufReader::new(std::fs::File::open(path)?);
match ron::de::from_reader(reader) {
Ok(x) => x,
Err(e) => {
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
}
} else {
special_settings()
};
// TODO don't dump to string
let result_body = ron::ser::to_string(&ron).unwrap();
Ok(actix_web::HttpResponse::Ok()
//.insert_header(header::ContentType("application/ron".parse().unwrap()))
.insert_header(header::ContentType(mime::STAR_STAR))
.body(actix_web::body::BoxBody::new(result_body))
)
} else {
// Send JSON (fallback)
let json = if id != 0 {
let path = file_util::setting_path_by_id(&cli.folder, id, file_util::JSON_EXTENSION);
// TODO? cache this instead of always loading it from file
let reader = std::io::BufReader::new(std::fs::File::open(path)?);
match serde_json::from_reader(reader) {
Ok(x) => x,
Err(e) => {
let e_msg = format!("{}", e);
if let Some(io_e) = e.io_error_kind() {
return Err(std::io::Error::new(io_e, e_msg));
} else {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
}
}
} else {
special_settings()
};
// TODO don't dump to string
let result_body = serde_json::to_string(&json).unwrap();
Ok(actix_web::HttpResponse::Ok()
.insert_header(header::ContentType::json())
.body(actix_web::body::BoxBody::new(result_body))
)
}
}

View file

@ -0,0 +1,10 @@
mod get_setting;
mod save_setting;
pub use get_setting::get_setting_handler as get_setting_by_id;
pub use save_setting::save_setting_handler as save_setting_with_new_id;
pub(self) fn is_mime_type_ron_capable(mimetype: &mime::Mime) -> bool {
(mimetype.type_() == "application" || mimetype.type_() == mime::STAR)
&& (mimetype.subtype() == "ron" || mimetype.subtype() == "cc.replicated.ron" || mimetype.subtype() == "w-ron")
}

View file

@ -0,0 +1,68 @@
use actix_web::{post, web, Responder, http::header};
use crate::cli::Cli;
use crate::file_util;
const PAYLOAD_LIMIT: usize = 10_000_000; // 10 Megabyte
#[post("/api/setting")]
pub async fn save_setting_handler(
data: web::Payload,
content_type: web::Header<header::ContentType>,
cli: web::Data<&'static Cli>,
) -> std::io::Result<impl Responder> {
println!("Content-Type: {}", content_type.to_string());
let bytes = match data.to_bytes_limited(PAYLOAD_LIMIT).await {
Ok(Ok(x)) => x,
Ok(Err(e)) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("wut: {}", e))),
Err(_e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "too many bytes in payload")),
};
let next_id = file_util::next_setting_id(&cli.folder);
let parsed_data = if super::is_mime_type_ron_capable(&content_type) {
// Parse as RON
match ron::de::from_reader(bytes.as_ref()) {
Ok(x) => x,
Err(e) => {
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
}
} else {
// Parse JSON (fallback)
match serde_json::from_reader(bytes.as_ref()) {
Ok(x) => x,
Err(e) => {
let e_msg = format!("{}", e);
if let Some(io_e) = e.io_error_kind() {
return Err(std::io::Error::new(io_e, e_msg));
} else {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
}
}
};
// TODO validate user and app id
// Reject blocked users and apps
let path = file_util::setting_path_by_id(&cli.folder, next_id, file_util::RON_EXTENSION);
let writer = std::io::BufWriter::new(std::fs::File::create(path)?);
if let Err(e) = ron::ser::to_writer(writer, &parsed_data) {
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
let path = file_util::setting_path_by_id(&cli.folder, next_id, file_util::JSON_EXTENSION);
let writer = std::io::BufWriter::new(std::fs::File::create(path)?);
if let Err(e) = serde_json::to_writer(writer, &parsed_data) {
let e_msg = format!("{}", e);
if let Some(io_e) = e.io_error_kind() {
return Err(std::io::Error::new(io_e, e_msg));
} else {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
}
// TODO create symlinks for other ways of looking up these settings files
Ok(actix_web::HttpResponse::NoContent())
}

View file

@ -0,0 +1,23 @@
use clap::Parser;
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// Root folder to store contributed setting files
#[arg(short, long, default_value = "./community_settings")]
pub folder: std::path::PathBuf,
/// Server port
#[arg(short, long, default_value_t = 8080)]
pub port: u16,
/// Log file location
#[arg(short, long, default_value = "/tmp/powertools_community_settings_srv.log")]
pub log: std::path::PathBuf,
}
impl Cli {
pub fn get() -> Self {
Self::parse()
}
}

View file

@ -0,0 +1,34 @@
use std::path::{Path, PathBuf};
use std::sync::Mutex;
pub const RON_EXTENSION: &'static str = "ron";
pub const JSON_EXTENSION: &'static str = "json";
const SETTING_FOLDER: &'static str = "settings";
const ID_FOLDER: &'static str = "by_id";
static LAST_SETTING_ID: Mutex<u128> = Mutex::new(0);
pub fn setting_path_by_id(root: impl AsRef<Path>, id: u128, ext: &str) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(ID_FOLDER)
.join(format!("{}.{}", id, ext))
}
pub fn next_setting_id(root: impl AsRef<Path>) -> u128 {
let mut lock = LAST_SETTING_ID.lock().unwrap();
let mut last_id = *lock;
if last_id == 0 {
// needs init
let mut path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION);
while path.exists() {
last_id += 1;
path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION);
}
*lock = last_id;
println!("setting id initialized to {}", last_id);
}
*lock += 1;
*lock
}

View file

@ -0,0 +1,40 @@
mod api;
mod cli;
mod file_util;
use actix_web::{web, App, HttpServer};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let args = cli::Cli::get();
println!("cli: {:?}", args);
simplelog::WriteLogger::init(
#[cfg(debug_assertions)]
{
log::LevelFilter::Debug
},
#[cfg(not(debug_assertions))]
{
log::LevelFilter::Info
},
Default::default(),
std::fs::File::create(&args.log).expect("Failed to create log file"),
//std::fs::File::create("/home/deck/powertools-rs.log").unwrap(),
)
.unwrap();
log::debug!("Logging to: {}", args.log.display());
let leaked_args: &'static cli::Cli = Box::leak::<'static>(Box::new(args));
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(leaked_args))
//.app_data(web::Data::new(IndexPage::load("dist/index.html").unwrap()))
//.app_data(basic::Config::default().realm("Restricted area"))
.service(api::get_setting_by_id)
.service(api::save_setting_with_new_id)
})
.bind(("0.0.0.0", leaked_args.port))?
.run()
.await
}