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 {
/// Convert old config files to the new format
Update { revision: Option<Revision> },
/// 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<String, ReadError> {
fn read_file_as_string(path: &Path) -> Result<String, ReadError> {
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<String, ReadError> {
Ok(result)
}
fn read_server_config_as<T>() -> Result<T, ReadError>
fn read_server_config_as<T>(base_dir: &Path) -> Result<T, ReadError>
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<T>() -> Result<T, ReadError>
fn read_client_config_as<T>(base_dir: &Path) -> Result<T, ReadError>
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<Revision, RevisionCheckError> {
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<Revision, RevisionCheckError> {
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<Revision, RevisionCheckError> {
config_revision: Revision,
}
let server_config_revision = read_server_config_as::<Config>()?.config_revision;
let client_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_client_config_as::<Config>(base_dir)?.config_revision;
if server_config_revision != client_config_revision {
return Err(RevisionCheckError::UnknownRevision(

View file

@ -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<Revision>) -> Result<(), UpdateError> {
let current = current_revision()?;
pub(super) async fn run(revision: Option<Revision>, 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 <bold>{}</>...",
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 <bold>{}</>...",
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(())
}

View file

@ -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!("<bold>{}</> 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<HashMap<String, Yaml>, ReadYamlConfigError> {
let content = YamlLoader::load_from_str(&read_file_as_string(".config/default.yml")?)?;
fn read_default_yml(base_dir: &Path) -> Result<HashMap<String, Yaml>, 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<String, Yaml>, Meta), Error> {
let default_yml = read_default_yml()?;
async fn read_old_config(base_dir: &Path) -> Result<(HashMap<String, Yaml>, Meta), Error> {
let default_yml = read_default_yml(base_dir)?;
let db = default_yml
.get("db")
.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 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 `<bold>fishctl config update</>` to update your config files.");
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| {
cprintln!("<r!><bold>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::<client::Config>() {
let client_validation_result = match read_client_config_as::<client::Config>(base_dir) {
Ok(config) => config.validate().map_err(|err| {
cprintln!(
"<r!><bold>{} 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::<server::Config>()
let server_config = read_server_config_as::<server::Config>(base_dir)
.expect("server config should be formally valid at this point");
match bypass_connection_checks {

View file

@ -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<String>,
}
#[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)?,
}

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)