add ability to specify base directory

This commit is contained in:
naskya 2024-07-01 00:15:23 +09:00
parent d2f8bc44ff
commit 38b76dcd8d
Signed by: naskya
GPG key ID: 712D413B3A9FED5C
7 changed files with 78 additions and 45 deletions

View file

@ -16,6 +16,7 @@ use std::{
pub(crate) enum Commands { pub(crate) enum Commands {
/// Convert old config files to the new format /// Convert old config files to the new format
Update { revision: Option<Revision> }, Update { revision: Option<Revision> },
/// Validate the config files /// Validate the config files
Validate { Validate {
#[arg(short, long)] #[arg(short, long)]
@ -33,10 +34,10 @@ pub(crate) enum ConfigError {
Validate(#[from] validate::ValidationError), 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 { match command {
Commands::Update { revision } => update::run(revision).await?, Commands::Update { revision } => update::run(revision, base_dir).await?,
Commands::Validate { offline } => validate::run(offline).await?, Commands::Validate { offline } => validate::run(offline, base_dir).await?,
} }
Ok(()) Ok(())
} }
@ -57,7 +58,7 @@ pub(crate) enum ReadError {
InvalidFormat(#[from] toml::de::Error), InvalidFormat(#[from] toml::de::Error),
} }
fn read_file_as_string(path: &str) -> Result<String, ReadError> { fn read_file_as_string(path: &Path) -> Result<String, ReadError> {
let mut file = fs::File::open(path)?; let mut file = fs::File::open(path)?;
let mut result = String::new(); let mut result = String::new();
file.read_to_string(&mut result)?; file.read_to_string(&mut result)?;
@ -65,24 +66,30 @@ fn read_file_as_string(path: &str) -> Result<String, ReadError> {
Ok(result) Ok(result)
} }
fn read_server_config_as<T>() -> Result<T, ReadError> fn read_server_config_as<T>(base_dir: &Path) -> Result<T, ReadError>
where where
T: serde::de::DeserializeOwned, 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<T>() -> Result<T, ReadError> fn read_client_config_as<T>(base_dir: &Path) -> Result<T, ReadError>
where where
T: serde::de::DeserializeOwned, 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<Revision, RevisionCheckError> { fn current_revision(base_dir: &Path) -> Result<Revision, RevisionCheckError> {
let old_config_exists = Path::new(".config/default.yml").is_file(); let old_config_exists = Path::new(&base_dir.join(".config/default.yml")).is_file();
let server_config_exists = Path::new(SERVER_CONFIG_PATH).is_file(); let server_config_exists = Path::new(&base_dir.join(SERVER_CONFIG_PATH)).is_file();
let client_config_exists = Path::new(CLIENT_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 { if server_config_exists && !client_config_exists {
return Err(RevisionCheckError::UnknownRevision( return Err(RevisionCheckError::UnknownRevision(
@ -109,8 +116,8 @@ fn current_revision() -> Result<Revision, RevisionCheckError> {
config_revision: Revision, config_revision: Revision,
} }
let server_config_revision = read_server_config_as::<Config>()?.config_revision; let server_config_revision = read_server_config_as::<Config>(base_dir)?.config_revision;
let client_config_revision = read_server_config_as::<Config>()?.config_revision; let client_config_revision = read_client_config_as::<Config>(base_dir)?.config_revision;
if server_config_revision != client_config_revision { if server_config_revision != client_config_revision {
return Err(RevisionCheckError::UnknownRevision( return Err(RevisionCheckError::UnknownRevision(

View file

@ -9,7 +9,7 @@ use crate::{
use chrono::Local; use chrono::Local;
use color_print::cprintln; use color_print::cprintln;
use enum_iterator::Sequence; use enum_iterator::Sequence;
use std::{fs, io}; use std::{fs, io, path::Path};
/// Errors that happen in `config update` subcommand /// Errors that happen in `config update` subcommand
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -24,24 +24,24 @@ pub(crate) enum UpdateError {
V1(#[from] v1::Error), V1(#[from] v1::Error),
} }
pub(super) async fn run(revision: Option<Revision>) -> Result<(), UpdateError> { pub(super) async fn run(revision: Option<Revision>, base_dir: &Path) -> Result<(), UpdateError> {
let current = current_revision()?; let current = current_revision(base_dir)?;
if current.next().is_none() { if current.next().is_none() {
println!("Your config files are already up-to-date! (as of this fishctl release)"); println!("Your config files are already up-to-date! (as of this fishctl release)");
return Ok(()); return Ok(());
} }
if current != Revision::V0 { if current != Revision::V0 {
take_backup()?; take_backup(base_dir)?;
} }
match revision { match revision {
Some(target) => update(current, target).await, Some(target) => update(current, target, base_dir).await,
None => update(current, Revision::last().unwrap()).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 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 server_backup_filename = format!("{}-backup-{}", SERVER_CONFIG_PATH, current_time);
let client_backup_filename = format!("{}-backup-{}", CLIENT_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 <bold>{}</>...", "Saving server config backup as <bold>{}</>...",
server_backup_filename server_backup_filename
); );
fs::copy(SERVER_CONFIG_PATH, server_backup_filename)?; fs::copy(&base_dir.join(SERVER_CONFIG_PATH), server_backup_filename)?;
cprintln!( cprintln!(
"Saving client config backup as <bold>{}</>...", "Saving client config backup as <bold>{}</>...",
client_backup_filename client_backup_filename
); );
fs::copy(CLIENT_CONFIG_PATH, client_backup_filename)?; fs::copy(&base_dir.join(CLIENT_CONFIG_PATH), client_backup_filename)?;
Ok(()) Ok(())
} }
/// Updates config files to the specified revision. /// 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 { if current > target {
return Err(UpdateError::Downgrade); return Err(UpdateError::Downgrade);
} }
while current < target { while current < target {
let next = current.next().unwrap(); let next = current.next().unwrap();
update_one_revision(next.clone()).await?; update_one_revision(next.clone(), base_dir).await?;
current = next; current = next;
} }
Ok(()) Ok(())
} }
/// Updates config file revision from `target - 1` to `target`. /// 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!( println!(
"Updating the config to revision {}...", "Updating the config to revision {}...",
target.clone() as u8 target.clone() as u8
); );
match target { match target {
Revision::V0 => unreachable!(), Revision::V0 => unreachable!(),
Revision::V1 => v1::run().await?, Revision::V1 => v1::run(base_dir).await?,
} }
Ok(()) Ok(())
} }

View file

@ -31,18 +31,20 @@ pub(crate) enum Error {
InvalidUrl(#[from] url::ParseError), InvalidUrl(#[from] url::ParseError),
} }
pub(super) async fn run() -> Result<(), Error> { pub(super) async fn run(base_dir: &Path) -> Result<(), Error> {
let (default_yml, meta) = read_old_config().await?; let (default_yml, meta) = read_old_config(base_dir).await?;
let server_config = create_new_server_config(default_yml, &meta)?; let server_config = create_new_server_config(default_yml, &meta)?;
let client_config = create_new_client_config(meta)?; let client_config = create_new_client_config(meta)?;
if !Path::new("config").exists() { let config_dir = base_dir.join("config");
fs::create_dir("config").map_err(WriteTomlConfigError::Mkdir)?;
if !config_dir.exists() {
fs::create_dir(&config_dir).map_err(WriteTomlConfigError::Mkdir)?;
} }
let mut server_toml = let mut server_toml = fs::File::create(&config_dir.join(SERVER_CONFIG_PATH))
fs::File::create(SERVER_CONFIG_PATH).map_err(WriteTomlConfigError::ServerWrite)?; .map_err(WriteTomlConfigError::ServerWrite)?;
server_toml server_toml
.write( .write(
toml::to_string_pretty(&server_config) toml::to_string_pretty(&server_config)
@ -52,8 +54,8 @@ pub(super) async fn run() -> Result<(), Error> {
.map_err(WriteTomlConfigError::ServerWrite)?; .map_err(WriteTomlConfigError::ServerWrite)?;
cprintln!("<bold>{}</> has been created!", SERVER_CONFIG_PATH); cprintln!("<bold>{}</> has been created!", SERVER_CONFIG_PATH);
let mut client_toml = let mut client_toml = fs::File::create(&config_dir.join(CLIENT_CONFIG_PATH))
fs::File::create(CLIENT_CONFIG_PATH).map_err(WriteTomlConfigError::ClientWrite)?; .map_err(WriteTomlConfigError::ClientWrite)?;
client_toml client_toml
.write( .write(
toml::to_string_pretty(&client_config) toml::to_string_pretty(&client_config)
@ -92,8 +94,9 @@ pub(crate) enum WriteTomlConfigError {
Mkdir(#[source] io::Error), Mkdir(#[source] io::Error),
} }
fn read_default_yml() -> Result<HashMap<String, Yaml>, ReadYamlConfigError> { fn read_default_yml(base_dir: &Path) -> Result<HashMap<String, Yaml>, ReadYamlConfigError> {
let content = YamlLoader::load_from_str(&read_file_as_string(".config/default.yml")?)?; let content =
YamlLoader::load_from_str(&read_file_as_string(&base_dir.join(".config/default.yml"))?)?;
if content.is_empty() { if content.is_empty() {
return Err(ReadYamlConfigError::InvalidConfig( return Err(ReadYamlConfigError::InvalidConfig(
@ -234,8 +237,8 @@ async fn read_meta_table(
Ok(meta) Ok(meta)
} }
async fn read_old_config() -> Result<(HashMap<String, Yaml>, Meta), Error> { async fn read_old_config(base_dir: &Path) -> Result<(HashMap<String, Yaml>, Meta), Error> {
let default_yml = read_default_yml()?; let default_yml = read_default_yml(base_dir)?;
let db = default_yml let db = default_yml
.get("db") .get("db")
.and_then(|db| db.as_hash()) .and_then(|db| db.as_hash())

View file

@ -5,6 +5,7 @@ use crate::config::{client, server, CLIENT_CONFIG_PATH, SERVER_CONFIG_PATH};
use color_print::cprintln; use color_print::cprintln;
use enum_iterator::Sequence; use enum_iterator::Sequence;
use sqlx::{postgres::PgConnectOptions, query, ConnectOptions}; use sqlx::{postgres::PgConnectOptions, query, ConnectOptions};
use std::path::Path;
use validator::Validate; use validator::Validate;
/// Errors that can happen in `config validate` subcommand /// Errors that can happen in `config validate` subcommand
@ -26,13 +27,16 @@ pub(crate) enum ValidationError {
CacheServer, CacheServer,
} }
pub(super) async fn run(bypass_connection_checks: bool) -> Result<(), ValidationError> { pub(super) async fn run(
if current_revision()?.next().is_some() { bypass_connection_checks: bool,
base_dir: &Path,
) -> Result<(), ValidationError> {
if current_revision(base_dir)?.next().is_some() {
cprintln!("Please first run `<bold>fishctl config update</>` to update your config files."); cprintln!("Please first run `<bold>fishctl config update</>` to update your config files.");
return Err(ValidationError::OutOfDate); return Err(ValidationError::OutOfDate);
} }
let server_validation_result = match read_server_config_as::<server::Config>() { let server_validation_result = match read_server_config_as::<server::Config>(base_dir) {
Ok(config) => config.validate().map_err(|err| { Ok(config) => config.validate().map_err(|err| {
cprintln!("<r!><bold>config/server.toml is invalid.</></>\n{}", err); cprintln!("<r!><bold>config/server.toml is invalid.</></>\n{}", err);
ValidationError::InvalidConfig 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)), Err(ReadError::ReadFile(err)) => Err(ValidationError::ReadFile(err)),
}; };
let client_validation_result = match read_client_config_as::<client::Config>() { let client_validation_result = match read_client_config_as::<client::Config>(base_dir) {
Ok(config) => config.validate().map_err(|err| { Ok(config) => config.validate().map_err(|err| {
cprintln!( cprintln!(
"<r!><bold>{} is invalid.</></>\n{}", "<r!><bold>{} is invalid.</></>\n{}",
@ -77,7 +81,7 @@ pub(super) async fn run(bypass_connection_checks: bool) -> Result<(), Validation
return Err(err); return Err(err);
} }
let server_config = read_server_config_as::<server::Config>() let server_config = read_server_config_as::<server::Config>(base_dir)
.expect("server config should be formally valid at this point"); .expect("server config should be formally valid at this point");
match bypass_connection_checks { match bypass_connection_checks {

View file

@ -4,6 +4,7 @@ mod config;
mod generate; mod generate;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::path::Path;
/// Errors that can happen in any commands /// Errors that can happen in any commands
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -19,6 +20,10 @@ pub(crate) enum Error {
struct Args { struct Args {
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Commands,
/// parent of the config directory (default: current directory)
#[arg(short, long, global = true)]
firefish_dir: Option<String>,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@ -26,6 +31,7 @@ enum Commands {
/// Modify or validate the config files /// Modify or validate the config files
#[command(subcommand)] #[command(subcommand)]
Config(config::Commands), Config(config::Commands),
/// Generate keys /// Generate keys
#[command(subcommand)] #[command(subcommand)]
Generate(generate::Commands), Generate(generate::Commands),
@ -34,8 +40,11 @@ enum Commands {
pub(super) async fn run() -> Result<(), Error> { pub(super) async fn run() -> Result<(), Error> {
let args = Args::parse(); let args = Args::parse();
let dirname = args.firefish_dir.unwrap_or("".to_string());
let base_dir = Path::new(&dirname);
match args.command { 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)?, Commands::Generate(subcommand) => generate::run(subcommand)?,
} }

View file

@ -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.

View file

@ -0,0 +1 @@
# Firefish server configuration (config/server.toml)