diff --git a/web/Cargo.toml b/web/Cargo.toml index 860e1ca..567113a 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -12,6 +12,8 @@ log = "0.4" env_logger = "0.9" askama = "0.10" argh = "0.1.6" +serde = "1.0" +serde_yaml = "0.8" rusttype = { version = "0.9.2", optional = true } image = { version = "0.23.14", optional = true } lazy_static = { version = "1.4.0", optional = true } diff --git a/web/src/configuration.rs b/web/src/configuration.rs index d73007d..1063555 100644 --- a/web/src/configuration.rs +++ b/web/src/configuration.rs @@ -1,4 +1,9 @@ +use std::fs::OpenOptions; +use std::fs::File; +use std::io::Write; +use pronouns_today::InstanceSettings; use argh::FromArgs; +use serde::{Deserialize, Serialize}; #[derive(FromArgs, PartialEq, Debug)] /// Everything you need to selfhost a pronouns.today server @@ -45,12 +50,14 @@ pub struct Run { option, default = "String::from(\"/etc/pronouns_today.yml\")", short = 'c'))] - /// path to the config file (generated if needed) + /// path to the config file (generated if needed). Defaults to + /// /etc/pronouns_today.yml pub config: String, #[argh(switch)] - /// don't generate a config file if not present - pub no_generate_cfg: bool, + /// don't attempt to read or generate a config file, just use command line args. Missing + /// options will be filled in using sane defaults + pub no_read_cfg: bool, #[argh(option, short = 'p')] /// the port to listen on @@ -61,3 +68,81 @@ pub struct Run { /// used in the custom URLs, e.g. "acaqeawdym") pub default_prefstr: Option, } + +impl Run { + /// Read the config from the provided file, and apply overrides from args + /// + /// If `--no-read-cfg` was passed, no config file is present, and there aren't + /// enough args to populate the configuration, returns an [`Err`] with a list of + /// arguments that were missing. + pub fn load_config(&self) -> Result { + if !self.no_read_cfg { + match File::open(&self.config) { + Ok(raw_cfg) => { + serde_yaml::from_reader(raw_cfg) + .map_err(ConfigError::MalformedConfig) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + OpenOptions::new() + .write(true) + .create(true) + .open(&self.config) + .and_then(|mut c| + c.write_all(include_bytes!("../assets/default_config.yml")) + ) + .map_err(ConfigError::IoError)?; + Err(ConfigError::ConfigCreated(self.config.clone())) + } + Err(e) => { + Err(ConfigError::IoError(e)) + } + } + } else { + Ok(Conf::default()) + }.map(|config| config.update_with(&self)) + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +/// A parsed config file for Pronouns.Today web +pub struct Conf { + + #[serde(flatten)] + /// General settings for the pronouns.today library + pub instance_settings: InstanceSettings, + + /// The port for the server to bind to. Defaults to 1312 + pub port: u16, +} + +impl Conf { + fn update_with(mut self, args: &Run) -> Conf { + self.port = args.port.unwrap_or(self.port); + self + } +} + +impl Default for Conf { + fn default() -> Self { + Conf { + instance_settings: InstanceSettings::default(), + port: 1312, + } + } +} + +/// An error occured while trying to load the configuration +pub enum ConfigError { + + /// There wase a problem reading/writing the config file + IoError(std::io::Error), + + /// There wase a problem deserializing the config file + MalformedConfig(serde_yaml::Error), + + /// The config file was missing, but has been created + /// + /// The program should now prompt the user to fill the config in, and then exit. The + /// provided [`String`] contains the path to the new config file + ConfigCreated(String), +} diff --git a/web/src/main.rs b/web/src/main.rs index ce0898e..513462f 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -3,6 +3,9 @@ pub mod contrast; pub mod statics; pub mod configuration; +use configuration::ConfigError; + +use std::process::exit; use std::collections::HashMap; use std::fmt::{self, Display}; @@ -142,6 +145,7 @@ async fn not_found() -> impl Responder { #[actix_web::main] async fn main() -> std::io::Result<()> { + env_logger::init(); let args: configuration::PronounsTodayArgs = argh::from_env(); match args.command { @@ -149,14 +153,30 @@ async fn main() -> std::io::Result<()> { println!("Support for dumping statics not yet implemented"); Ok(()) } - configuration::SubCommand::Run(_subargs) => { + configuration::SubCommand::Run(subargs) => { + let config = match subargs.load_config() { + Ok(config) => config, + Err(ConfigError::IoError(e)) => { + return Err(e); + } + Err(ConfigError::MalformedConfig(e)) => { + eprintln!("Error parsing config file:\n{}", e); + exit(1001); + } + Err(ConfigError::ConfigCreated(path)) => { + println!("A config file has been generated at {}! Please check it out + and modify it to your liking, and then run this command + again", path); + exit(0); + } + }; + log::info!("Starting with configuration {:?}", config); start_server().await } } } async fn start_server() -> std::io::Result<()> { - env_logger::init(); println!("Starting pronouns-today-web on 127.0.0.1:8080"); HttpServer::new(|| { let logger = Logger::default();