You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
distant/src/config.rs

127 lines
4.6 KiB
Rust

use crate::paths;
use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::{
io,
path::{Path, PathBuf},
};
use toml_edit::Document;
mod client;
mod common;
mod generate;
mod manager;
mod network;
mod server;
pub use client::*;
pub use common::*;
pub use generate::*;
pub use manager::*;
pub use network::*;
pub use server::*;
/// Represents configuration settings for all of distant
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config {
pub client: ClientConfig,
pub generate: GenerateConfig,
pub manager: ManagerConfig,
pub server: ServerConfig,
}
impl Config {
/// Loads the configuration from multiple sources in a blocking fashion
///
/// 1. If `custom` is provided, it is used by itself as the source for configuration
/// 2. Otherwise, if `custom` is not provided, will attempt to load from global and user
/// config files, merging together if they both exist
/// 3. Otherwise if no `custom` path and none of the standard configuration paths exist,
/// then the default configuration is returned instead
pub fn load_multi(custom: Option<PathBuf>) -> anyhow::Result<Self> {
match custom {
Some(path) => {
toml_edit::de::from_slice(&std::fs::read(path)?).context("Failed to parse config")
}
None => {
let paths = vec![
paths::global::CONFIG_FILE_PATH.as_path(),
paths::user::CONFIG_FILE_PATH.as_path(),
];
match (paths[0].exists(), paths[1].exists()) {
// At least one standard path exists, so load it
(exists_1, exists_2) if exists_1 || exists_2 => {
use config::{Config, File};
let config = Config::builder()
.add_source(File::from(paths[0]).required(exists_1))
.add_source(File::from(paths[1]).required(exists_2))
.build()
.context("Failed to build config from paths")?;
config.try_deserialize().context("Failed to parse config")
}
// None of our standard paths exist, so use the default value instead
_ => Ok(Self::default()),
}
}
}
}
/// Loads the specified `path` as a [`Config`]
pub async fn load(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let bytes = tokio::fs::read(path.as_ref())
.await
.with_context(|| format!("Failed to read config file {:?}", path.as_ref()))?;
toml_edit::de::from_slice(&bytes).context("Failed to parse config")
}
/// Like `edit` but will succeed without invoking `f` if the path is not found
pub async fn edit_if_exists(
path: impl AsRef<Path>,
f: impl FnOnce(&mut Document) -> io::Result<()>,
) -> io::Result<()> {
Self::edit(path, f).await.or_else(|x| {
if x.kind() == io::ErrorKind::NotFound {
Ok(())
} else {
Err(x)
}
})
}
/// Loads the specified `path` as a [`Document`], performs changes to the document using `f`,
/// and overwrites the `path` with the updated [`Document`]
pub async fn edit(
path: impl AsRef<Path>,
f: impl FnOnce(&mut Document) -> io::Result<()>,
) -> io::Result<()> {
let mut document = tokio::fs::read_to_string(path.as_ref())
.await?
.parse::<Document>()
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
f(&mut document)?;
tokio::fs::write(path, document.to_string()).await
}
/// Saves the [`Config`] to the specified `path` only if the path points to no file
pub async fn save_if_not_found(&self, path: impl AsRef<Path>) -> io::Result<()> {
use tokio::io::AsyncWriteExt;
let text = toml_edit::ser::to_string_pretty(self)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
tokio::fs::OpenOptions::new()
.create_new(true)
.open(path)
.await?
.write_all(text.as_bytes())
.await
}
/// Saves the [`Config`] to the specified `path`, overwriting the file if it exists
pub async fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
let text = toml_edit::ser::to_string_pretty(self)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
tokio::fs::write(path, text).await
}
}