add ability to specify base directory
This commit is contained in:
parent
d2f8bc44ff
commit
38b76dcd8d
7 changed files with 78 additions and 45 deletions
|
@ -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(
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)?,
|
||||
}
|
||||
|
||||
|
|
5
fishctl/src/config/template/client.toml
Normal file
5
fishctl/src/config/template/client.toml
Normal 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.
|
1
fishctl/src/config/template/server.toml
Normal file
1
fishctl/src/config/template/server.toml
Normal file
|
@ -0,0 +1 @@
|
|||
# Firefish server configuration (config/server.toml)
|
Loading…
Reference in a new issue