diff --git a/fishctl/src/command/config.rs b/fishctl/src/command/config.rs index 3f9f6a2..45f22f0 100644 --- a/fishctl/src/command/config.rs +++ b/fishctl/src/command/config.rs @@ -16,6 +16,7 @@ use std::{ pub(crate) enum Commands { /// Convert old config files to the new format Update { revision: Option }, + /// Validate the config files Validate { #[arg(short, long)] @@ -33,10 +34,10 @@ pub(crate) enum ConfigError { Validate(#[from] validate::ValidationError), } -pub(super) async fn run(command: Commands) -> Result<(), ConfigError> { +pub(super) async fn run(command: Commands, base_dir: &Path) -> Result<(), ConfigError> { match command { - Commands::Update { revision } => update::run(revision).await?, - Commands::Validate { offline } => validate::run(offline).await?, + Commands::Update { revision } => update::run(revision, base_dir).await?, + Commands::Validate { offline } => validate::run(offline, base_dir).await?, } Ok(()) } @@ -57,7 +58,7 @@ pub(crate) enum ReadError { InvalidFormat(#[from] toml::de::Error), } -fn read_file_as_string(path: &str) -> Result { +fn read_file_as_string(path: &Path) -> Result { let mut file = fs::File::open(path)?; let mut result = String::new(); file.read_to_string(&mut result)?; @@ -65,24 +66,30 @@ fn read_file_as_string(path: &str) -> Result { Ok(result) } -fn read_server_config_as() -> Result +fn read_server_config_as(base_dir: &Path) -> Result where T: serde::de::DeserializeOwned, { - toml::from_str(&read_file_as_string(SERVER_CONFIG_PATH)?).map_err(ReadError::InvalidFormat) + toml::from_str(&read_file_as_string(Path::new( + &base_dir.join(SERVER_CONFIG_PATH), + ))?) + .map_err(ReadError::InvalidFormat) } -fn read_client_config_as() -> Result +fn read_client_config_as(base_dir: &Path) -> Result where T: serde::de::DeserializeOwned, { - toml::from_str(&read_file_as_string(CLIENT_CONFIG_PATH)?).map_err(ReadError::InvalidFormat) + toml::from_str(&read_file_as_string(Path::new( + &base_dir.join(CLIENT_CONFIG_PATH), + ))?) + .map_err(ReadError::InvalidFormat) } -fn current_revision() -> Result { - let old_config_exists = Path::new(".config/default.yml").is_file(); - let server_config_exists = Path::new(SERVER_CONFIG_PATH).is_file(); - let client_config_exists = Path::new(CLIENT_CONFIG_PATH).is_file(); +fn current_revision(base_dir: &Path) -> Result { + let old_config_exists = Path::new(&base_dir.join(".config/default.yml")).is_file(); + let server_config_exists = Path::new(&base_dir.join(SERVER_CONFIG_PATH)).is_file(); + let client_config_exists = Path::new(&base_dir.join(CLIENT_CONFIG_PATH)).is_file(); if server_config_exists && !client_config_exists { return Err(RevisionCheckError::UnknownRevision( @@ -109,8 +116,8 @@ fn current_revision() -> Result { config_revision: Revision, } - let server_config_revision = read_server_config_as::()?.config_revision; - let client_config_revision = read_server_config_as::()?.config_revision; + let server_config_revision = read_server_config_as::(base_dir)?.config_revision; + let client_config_revision = read_client_config_as::(base_dir)?.config_revision; if server_config_revision != client_config_revision { return Err(RevisionCheckError::UnknownRevision( diff --git a/fishctl/src/command/config/update.rs b/fishctl/src/command/config/update.rs index 4c25cca..ec3ab5d 100644 --- a/fishctl/src/command/config/update.rs +++ b/fishctl/src/command/config/update.rs @@ -9,7 +9,7 @@ use crate::{ use chrono::Local; use color_print::cprintln; use enum_iterator::Sequence; -use std::{fs, io}; +use std::{fs, io, path::Path}; /// Errors that happen in `config update` subcommand #[derive(thiserror::Error, Debug)] @@ -24,24 +24,24 @@ pub(crate) enum UpdateError { V1(#[from] v1::Error), } -pub(super) async fn run(revision: Option) -> Result<(), UpdateError> { - let current = current_revision()?; +pub(super) async fn run(revision: Option, base_dir: &Path) -> Result<(), UpdateError> { + let current = current_revision(base_dir)?; if current.next().is_none() { println!("Your config files are already up-to-date! (as of this fishctl release)"); return Ok(()); } if current != Revision::V0 { - take_backup()?; + take_backup(base_dir)?; } match revision { - Some(target) => update(current, target).await, - None => update(current, Revision::last().unwrap()).await, + Some(target) => update(current, target, base_dir).await, + None => update(current, Revision::last().unwrap(), base_dir).await, } } -fn take_backup() -> Result<(), UpdateError> { +fn take_backup(base_dir: &Path) -> Result<(), UpdateError> { let current_time = Local::now().format("%Y%m%d%H%M%S").to_string(); let server_backup_filename = format!("{}-backup-{}", SERVER_CONFIG_PATH, current_time); let client_backup_filename = format!("{}-backup-{}", CLIENT_CONFIG_PATH, current_time); @@ -50,39 +50,43 @@ fn take_backup() -> Result<(), UpdateError> { "Saving server config backup as {}...", server_backup_filename ); - fs::copy(SERVER_CONFIG_PATH, server_backup_filename)?; + fs::copy(&base_dir.join(SERVER_CONFIG_PATH), server_backup_filename)?; cprintln!( "Saving client config backup as {}...", client_backup_filename ); - fs::copy(CLIENT_CONFIG_PATH, client_backup_filename)?; + fs::copy(&base_dir.join(CLIENT_CONFIG_PATH), client_backup_filename)?; Ok(()) } /// Updates config files to the specified revision. -async fn update(mut current: Revision, target: Revision) -> Result<(), UpdateError> { +async fn update( + mut current: Revision, + target: Revision, + base_dir: &Path, +) -> Result<(), UpdateError> { if current > target { return Err(UpdateError::Downgrade); } while current < target { let next = current.next().unwrap(); - update_one_revision(next.clone()).await?; + update_one_revision(next.clone(), base_dir).await?; current = next; } Ok(()) } /// Updates config file revision from `target - 1` to `target`. -async fn update_one_revision(target: Revision) -> Result<(), UpdateError> { +async fn update_one_revision(target: Revision, base_dir: &Path) -> Result<(), UpdateError> { println!( "Updating the config to revision {}...", target.clone() as u8 ); match target { Revision::V0 => unreachable!(), - Revision::V1 => v1::run().await?, + Revision::V1 => v1::run(base_dir).await?, } Ok(()) } diff --git a/fishctl/src/command/config/update/v1.rs b/fishctl/src/command/config/update/v1.rs index 1fa1f8d..5a52173 100644 --- a/fishctl/src/command/config/update/v1.rs +++ b/fishctl/src/command/config/update/v1.rs @@ -31,18 +31,20 @@ pub(crate) enum Error { InvalidUrl(#[from] url::ParseError), } -pub(super) async fn run() -> Result<(), Error> { - let (default_yml, meta) = read_old_config().await?; +pub(super) async fn run(base_dir: &Path) -> Result<(), Error> { + let (default_yml, meta) = read_old_config(base_dir).await?; let server_config = create_new_server_config(default_yml, &meta)?; let client_config = create_new_client_config(meta)?; - if !Path::new("config").exists() { - fs::create_dir("config").map_err(WriteTomlConfigError::Mkdir)?; + let config_dir = base_dir.join("config"); + + if !config_dir.exists() { + fs::create_dir(&config_dir).map_err(WriteTomlConfigError::Mkdir)?; } - let mut server_toml = - fs::File::create(SERVER_CONFIG_PATH).map_err(WriteTomlConfigError::ServerWrite)?; + let mut server_toml = fs::File::create(&config_dir.join(SERVER_CONFIG_PATH)) + .map_err(WriteTomlConfigError::ServerWrite)?; server_toml .write( toml::to_string_pretty(&server_config) @@ -52,8 +54,8 @@ pub(super) async fn run() -> Result<(), Error> { .map_err(WriteTomlConfigError::ServerWrite)?; cprintln!("{} has been created!", SERVER_CONFIG_PATH); - let mut client_toml = - fs::File::create(CLIENT_CONFIG_PATH).map_err(WriteTomlConfigError::ClientWrite)?; + let mut client_toml = fs::File::create(&config_dir.join(CLIENT_CONFIG_PATH)) + .map_err(WriteTomlConfigError::ClientWrite)?; client_toml .write( toml::to_string_pretty(&client_config) @@ -92,8 +94,9 @@ pub(crate) enum WriteTomlConfigError { Mkdir(#[source] io::Error), } -fn read_default_yml() -> Result, ReadYamlConfigError> { - let content = YamlLoader::load_from_str(&read_file_as_string(".config/default.yml")?)?; +fn read_default_yml(base_dir: &Path) -> Result, ReadYamlConfigError> { + let content = + YamlLoader::load_from_str(&read_file_as_string(&base_dir.join(".config/default.yml"))?)?; if content.is_empty() { return Err(ReadYamlConfigError::InvalidConfig( @@ -234,8 +237,8 @@ async fn read_meta_table( Ok(meta) } -async fn read_old_config() -> Result<(HashMap, Meta), Error> { - let default_yml = read_default_yml()?; +async fn read_old_config(base_dir: &Path) -> Result<(HashMap, Meta), Error> { + let default_yml = read_default_yml(base_dir)?; let db = default_yml .get("db") .and_then(|db| db.as_hash()) diff --git a/fishctl/src/command/config/validate.rs b/fishctl/src/command/config/validate.rs index b76e149..c0f5742 100644 --- a/fishctl/src/command/config/validate.rs +++ b/fishctl/src/command/config/validate.rs @@ -5,6 +5,7 @@ use crate::config::{client, server, CLIENT_CONFIG_PATH, SERVER_CONFIG_PATH}; use color_print::cprintln; use enum_iterator::Sequence; use sqlx::{postgres::PgConnectOptions, query, ConnectOptions}; +use std::path::Path; use validator::Validate; /// Errors that can happen in `config validate` subcommand @@ -26,13 +27,16 @@ pub(crate) enum ValidationError { CacheServer, } -pub(super) async fn run(bypass_connection_checks: bool) -> Result<(), ValidationError> { - if current_revision()?.next().is_some() { +pub(super) async fn run( + bypass_connection_checks: bool, + base_dir: &Path, +) -> Result<(), ValidationError> { + if current_revision(base_dir)?.next().is_some() { cprintln!("Please first run `fishctl config update` to update your config files."); return Err(ValidationError::OutOfDate); } - let server_validation_result = match read_server_config_as::() { + let server_validation_result = match read_server_config_as::(base_dir) { Ok(config) => config.validate().map_err(|err| { cprintln!("config/server.toml is invalid.\n{}", err); ValidationError::InvalidConfig @@ -44,7 +48,7 @@ pub(super) async fn run(bypass_connection_checks: bool) -> Result<(), Validation Err(ReadError::ReadFile(err)) => Err(ValidationError::ReadFile(err)), }; - let client_validation_result = match read_client_config_as::() { + let client_validation_result = match read_client_config_as::(base_dir) { Ok(config) => config.validate().map_err(|err| { cprintln!( "{} is invalid.\n{}", @@ -77,7 +81,7 @@ pub(super) async fn run(bypass_connection_checks: bool) -> Result<(), Validation return Err(err); } - let server_config = read_server_config_as::() + let server_config = read_server_config_as::(base_dir) .expect("server config should be formally valid at this point"); match bypass_connection_checks { diff --git a/fishctl/src/command/mod.rs b/fishctl/src/command/mod.rs index 0c4ecdc..b1dc3dd 100644 --- a/fishctl/src/command/mod.rs +++ b/fishctl/src/command/mod.rs @@ -4,6 +4,7 @@ mod config; mod generate; use clap::{Parser, Subcommand}; +use std::path::Path; /// Errors that can happen in any commands #[derive(thiserror::Error, Debug)] @@ -19,6 +20,10 @@ pub(crate) enum Error { struct Args { #[command(subcommand)] command: Commands, + + /// parent of the config directory (default: current directory) + #[arg(short, long, global = true)] + firefish_dir: Option, } #[derive(Subcommand)] @@ -26,6 +31,7 @@ enum Commands { /// Modify or validate the config files #[command(subcommand)] Config(config::Commands), + /// Generate keys #[command(subcommand)] Generate(generate::Commands), @@ -34,8 +40,11 @@ enum Commands { pub(super) async fn run() -> Result<(), Error> { let args = Args::parse(); + let dirname = args.firefish_dir.unwrap_or("".to_string()); + let base_dir = Path::new(&dirname); + match args.command { - Commands::Config(subcommand) => config::run(subcommand).await?, + Commands::Config(subcommand) => config::run(subcommand, base_dir).await?, Commands::Generate(subcommand) => generate::run(subcommand)?, } diff --git a/fishctl/src/config/template/client.toml b/fishctl/src/config/template/client.toml new file mode 100644 index 0000000..7bfc6dc --- /dev/null +++ b/fishctl/src/config/template/client.toml @@ -0,0 +1,5 @@ +# Firefish client configuration (config/client.toml) +# +# DO NOT WRITE ANY SENSITIVE INFORMATION TO THIS FILE, EVEN IN COMMENTS! +# +# This file is read from the web client (browser), so the content will be exposed. diff --git a/fishctl/src/config/template/server.toml b/fishctl/src/config/template/server.toml new file mode 100644 index 0000000..6b7a54a --- /dev/null +++ b/fishctl/src/config/template/server.toml @@ -0,0 +1 @@ +# Firefish server configuration (config/server.toml)