Community settings WIP
This commit is contained in:
parent
9e1f7c0620
commit
508c6ceb9e
15 changed files with 2010 additions and 0 deletions
65
backend/community_settings_core/Cargo.lock
generated
Normal file
65
backend/community_settings_core/Cargo.lock
generated
Normal 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"
|
9
backend/community_settings_core/Cargo.toml
Normal file
9
backend/community_settings_core/Cargo.toml
Normal 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"] }
|
1
backend/community_settings_core/src/lib.rs
Normal file
1
backend/community_settings_core/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod v1;
|
22
backend/community_settings_core/src/v1/metadata.rs
Normal file
22
backend/community_settings_core/src/v1/metadata.rs
Normal 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")
|
||||
}
|
||||
}
|
5
backend/community_settings_core/src/v1/mod.rs
Normal file
5
backend/community_settings_core/src/v1/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod metadata;
|
||||
mod setting;
|
||||
|
||||
pub use metadata::Metadata;
|
||||
pub use setting::*;
|
47
backend/community_settings_core/src/v1/setting.rs
Normal file
47
backend/community_settings_core/src/v1/setting.rs
Normal 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
1538
backend/community_settings_srv/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
backend/community_settings_srv/Cargo.toml
Normal file
21
backend/community_settings_srv/Cargo.toml
Normal 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"
|
7
backend/community_settings_srv/README.md
Normal file
7
backend/community_settings_srv/README.md
Normal 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.
|
120
backend/community_settings_srv/src/api/get_setting.rs
Normal file
120
backend/community_settings_srv/src/api/get_setting.rs
Normal 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))
|
||||
)
|
||||
}
|
||||
}
|
10
backend/community_settings_srv/src/api/mod.rs
Normal file
10
backend/community_settings_srv/src/api/mod.rs
Normal 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")
|
||||
}
|
68
backend/community_settings_srv/src/api/save_setting.rs
Normal file
68
backend/community_settings_srv/src/api/save_setting.rs
Normal 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())
|
||||
}
|
23
backend/community_settings_srv/src/cli.rs
Normal file
23
backend/community_settings_srv/src/cli.rs
Normal 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()
|
||||
}
|
||||
}
|
34
backend/community_settings_srv/src/file_util.rs
Normal file
34
backend/community_settings_srv/src/file_util.rs
Normal 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
|
||||
}
|
40
backend/community_settings_srv/src/main.rs
Normal file
40
backend/community_settings_srv/src/main.rs
Normal 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
|
||||
}
|
Loading…
Reference in a new issue