mirror of
https://gitlab.steamos.cloud/holo/steamos-manager.git
synced 2025-07-13 01:41:59 -04:00
config: Allow config fragment loading code to be used generically
This commit is contained in:
parent
2d4647a918
commit
6c485684b8
2 changed files with 98 additions and 81 deletions
|
@ -6,52 +6,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
|
||||||
use config::builder::AsyncState;
|
use config::builder::AsyncState;
|
||||||
use config::{AsyncSource, ConfigBuilder, ConfigError, FileFormat, Format, Map, Value};
|
use config::{ConfigBuilder, FileFormat, FileStoredFormat};
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::Path;
|
use tokio::fs::{create_dir_all, read_to_string, write};
|
||||||
use tokio::fs::{create_dir_all, read_dir, read_to_string, write};
|
use tracing::{error, info};
|
||||||
use tracing::{debug, error, info};
|
|
||||||
|
|
||||||
use crate::daemon::DaemonContext;
|
use crate::daemon::DaemonContext;
|
||||||
|
use crate::{read_config_directory, AsyncFileSource};
|
||||||
#[derive(Debug)]
|
|
||||||
struct AsyncFileSource<F: Format, P: AsRef<Path> + Sized + Send + Sync> {
|
|
||||||
path: P,
|
|
||||||
format: F,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F: Format, P: AsRef<Path> + Sized + Send + Sync + Debug> AsyncFileSource<F, P> {
|
|
||||||
fn from(path: P, format: F) -> AsyncFileSource<F, P> {
|
|
||||||
AsyncFileSource { path, format }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<F: Format + Send + Sync + Debug, P: AsRef<Path> + Sized + Send + Sync + Debug> AsyncSource
|
|
||||||
for AsyncFileSource<F, P>
|
|
||||||
{
|
|
||||||
async fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
|
|
||||||
let path = self.path.as_ref();
|
|
||||||
let text = match read_to_string(&path).await {
|
|
||||||
Ok(text) => text,
|
|
||||||
Err(e) => {
|
|
||||||
if e.kind() == ErrorKind::NotFound {
|
|
||||||
info!("No config file {} found", path.to_string_lossy());
|
|
||||||
return Ok(Map::new());
|
|
||||||
}
|
|
||||||
return Err(ConfigError::Foreign(Box::new(e)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let path = path.to_string_lossy().to_string();
|
|
||||||
self.format
|
|
||||||
.parse(Some(&path), &text)
|
|
||||||
.map_err(ConfigError::Foreign)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(in crate::daemon) async fn read_state<C: DaemonContext>(context: &C) -> Result<C::State> {
|
pub(in crate::daemon) async fn read_state<C: DaemonContext>(context: &C) -> Result<C::State> {
|
||||||
let path = context.state_path()?;
|
let path = context.state_path()?;
|
||||||
|
@ -89,52 +51,29 @@ pub(in crate::daemon) async fn read_config<C: DaemonContext>(context: &C) -> Res
|
||||||
system_config_path.join("config.toml"),
|
system_config_path.join("config.toml"),
|
||||||
FileFormat::Toml,
|
FileFormat::Toml,
|
||||||
));
|
));
|
||||||
let builder = read_config_directory(builder, system_config_path.join("config.toml.d")).await?;
|
let builder = read_config_directory(
|
||||||
|
builder,
|
||||||
|
system_config_path.join("config.toml.d"),
|
||||||
|
FileFormat::Toml.file_extensions(),
|
||||||
|
FileFormat::Toml,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let builder = builder.add_async_source(AsyncFileSource::from(
|
let builder = builder.add_async_source(AsyncFileSource::from(
|
||||||
user_config_path.join("config.toml"),
|
user_config_path.join("config.toml"),
|
||||||
FileFormat::Toml,
|
FileFormat::Toml,
|
||||||
));
|
));
|
||||||
let builder = read_config_directory(builder, user_config_path.join("config.toml.d")).await?;
|
let builder = read_config_directory(
|
||||||
|
builder,
|
||||||
|
user_config_path.join("config.toml.d"),
|
||||||
|
FileFormat::Toml.file_extensions(),
|
||||||
|
FileFormat::Toml,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let config = builder.build().await?;
|
let config = builder.build().await?;
|
||||||
Ok(config.try_deserialize()?)
|
Ok(config.try_deserialize()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_config_directory<P: AsRef<Path> + Sync + Send>(
|
|
||||||
builder: ConfigBuilder<AsyncState>,
|
|
||||||
path: P,
|
|
||||||
) -> Result<ConfigBuilder<AsyncState>> {
|
|
||||||
let mut dir = match read_dir(&path).await {
|
|
||||||
Ok(dir) => dir,
|
|
||||||
Err(e) => {
|
|
||||||
if e.kind() == ErrorKind::NotFound {
|
|
||||||
debug!(
|
|
||||||
"No config fragment directory {} found",
|
|
||||||
path.as_ref().to_string_lossy()
|
|
||||||
);
|
|
||||||
return Ok(builder);
|
|
||||||
}
|
|
||||||
error!(
|
|
||||||
"Error reading config fragment directory {}: {e}",
|
|
||||||
path.as_ref().to_string_lossy()
|
|
||||||
);
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
while let Some(entry) = dir.next_entry().await? {
|
|
||||||
let path = entry.path();
|
|
||||||
match path.extension() {
|
|
||||||
Some(ext) if ext == OsStr::new("toml") => entries.push(path),
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries.sort();
|
|
||||||
Ok(entries.into_iter().fold(builder, |builder, path| {
|
|
||||||
builder.add_async_source(AsyncFileSource::from(path, FileFormat::Toml))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
82
src/lib.rs
82
src/lib.rs
|
@ -6,12 +6,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use config::builder::AsyncState;
|
||||||
|
use config::{AsyncSource, ConfigBuilder, ConfigError, FileFormat, Format, Map, Value};
|
||||||
|
use std::fmt::Debug;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
use std::io::ErrorKind;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tokio::fs::File;
|
use tokio::fs::{read_dir, read_to_string, File};
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
mod ds_inhibit;
|
mod ds_inhibit;
|
||||||
mod error;
|
mod error;
|
||||||
|
@ -69,6 +74,41 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct AsyncFileSource<F: Format, P: AsRef<Path> + Sized + Send + Sync> {
|
||||||
|
path: P,
|
||||||
|
format: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Format, P: AsRef<Path> + Sized + Send + Sync + Debug> AsyncFileSource<F, P> {
|
||||||
|
fn from(path: P, format: F) -> AsyncFileSource<F, P> {
|
||||||
|
AsyncFileSource { path, format }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<F: Format + Send + Sync + Debug, P: AsRef<Path> + Sized + Send + Sync + Debug> AsyncSource
|
||||||
|
for AsyncFileSource<F, P>
|
||||||
|
{
|
||||||
|
async fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
|
||||||
|
let path = self.path.as_ref();
|
||||||
|
let text = match read_to_string(&path).await {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => {
|
||||||
|
if e.kind() == ErrorKind::NotFound {
|
||||||
|
info!("No config file {} found", path.to_string_lossy());
|
||||||
|
return Ok(Map::new());
|
||||||
|
}
|
||||||
|
return Err(ConfigError::Foreign(Box::new(e)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let path = path.to_string_lossy().to_string();
|
||||||
|
self.format
|
||||||
|
.parse(Some(&path), &text)
|
||||||
|
.map_err(ConfigError::Foreign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub fn path<S: AsRef<str>>(path: S) -> PathBuf {
|
pub fn path<S: AsRef<str>>(path: S) -> PathBuf {
|
||||||
PathBuf::from(path.as_ref())
|
PathBuf::from(path.as_ref())
|
||||||
|
@ -126,6 +166,44 @@ pub(crate) fn get_appid(pid: u32) -> Result<Option<u64>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn read_config_directory<P: AsRef<Path> + Sync + Send>(
|
||||||
|
builder: ConfigBuilder<AsyncState>,
|
||||||
|
path: P,
|
||||||
|
extensions: &[&str],
|
||||||
|
format: FileFormat,
|
||||||
|
) -> Result<ConfigBuilder<AsyncState>> {
|
||||||
|
let mut dir = match read_dir(&path).await {
|
||||||
|
Ok(dir) => dir,
|
||||||
|
Err(e) => {
|
||||||
|
if e.kind() == ErrorKind::NotFound {
|
||||||
|
debug!(
|
||||||
|
"No config fragment directory {} found",
|
||||||
|
path.as_ref().to_string_lossy()
|
||||||
|
);
|
||||||
|
return Ok(builder);
|
||||||
|
}
|
||||||
|
error!(
|
||||||
|
"Error reading config fragment directory {}: {e}",
|
||||||
|
path.as_ref().to_string_lossy()
|
||||||
|
);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
while let Some(entry) = dir.next_entry().await? {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) {
|
||||||
|
if extensions.contains(&ext) {
|
||||||
|
entries.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.sort();
|
||||||
|
Ok(entries.into_iter().fold(builder, |builder, path| {
|
||||||
|
builder.add_async_source(AsyncFileSource::from(path, format))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::testing;
|
use crate::testing;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue