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 {
|
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(
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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