Files
dkl/src/bin/dkl.rs

163 lines
4.1 KiB
Rust
Raw Normal View History

2025-07-20 18:48:47 +02:00
use clap::{CommandFactory, Parser, Subcommand};
use eyre::{format_err, Result};
use log::{debug, error};
use tokio::fs;
#[derive(Parser)]
#[command()]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
ApplyConfig {
/// config file to use
#[arg(default_value = "config.yaml")]
config: String,
/// glob filters to select files to apply
#[arg(short = 'F', long)]
filters: Vec<String>,
/// path prefix (aka chroot)
#[arg(short = 'P', long, default_value = "/")]
prefix: String,
},
Logger {
#[arg(long, short = 'p', default_value = "/var/log")]
log_path: String,
#[arg(long, short = 'n')]
log_name: Option<String>,
#[arg(long)]
with_prefix: bool,
command: String,
args: Vec<String>,
},
Log {
#[arg(long, short = 'p', default_value = "/var/log")]
log_path: String,
log_name: String,
since: Option<String>,
until: Option<String>,
},
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
clap_complete::CompleteEnv::with_factory(Cli::command).complete();
let cli = Cli::parse();
env_logger::builder()
.parse_filters("info")
.parse_default_env()
.init();
use Command as C;
match cli.command {
C::ApplyConfig {
config,
filters,
prefix,
} => {
let filters = parse_globs(&filters)?;
apply_config(&config, &filters, &prefix).await
}
C::Logger {
ref log_path,
ref log_name,
with_prefix,
command,
args,
} => {
let command = command.as_str();
let log_name = log_name.as_deref().unwrap_or_else(|| basename(command));
dkl::logger::Logger {
log_path,
log_name,
with_prefix,
}
.run(command, &args)
.await
}
C::Log {
log_path,
log_name,
since,
until,
} => {
let since = parse_ts_arg(since)?;
let until = parse_ts_arg(until)?;
let mut files = dkl::logger::log_files(&log_path, &log_name).await?;
files.sort();
let mut out = tokio::io::stdout();
for f in files {
if !since.is_none_or(|since| f.timestamp >= since) {
continue;
}
if !until.is_none_or(|until| f.timestamp <= until) {
continue;
}
debug!("{f:?}");
f.copy_to(&mut out).await?;
}
Ok(())
}
}
}
fn parse_ts_arg(ts: Option<String>) -> Result<Option<dkl::logger::Timestamp>> {
match ts {
None => Ok(None),
Some(ts) => {
let ts = dkl::logger::parse_ts(&ts)
.map_err(|e| format_err!("invalid timestamp: {ts}: {e}"))?;
Ok(Some(ts))
}
}
}
fn basename(path: &str) -> &str {
path.rsplit_once('/').map_or(path, |split| split.1)
}
async fn apply_config(config_file: &str, filters: &[glob::Pattern], chroot: &str) -> Result<()> {
let config = fs::read_to_string(config_file).await?;
let config: dkl::Config = serde_yaml::from_str(&config)?;
let files = if filters.is_empty() {
config.files
} else {
(config.files.into_iter())
.filter(|f| filters.iter().any(|filter| filter.matches(&f.path)))
.collect()
};
dkl::apply::files(&files, chroot).await
}
fn parse_globs(filters: &[String]) -> Result<Vec<glob::Pattern>> {
let mut errors = false;
let filters = (filters.iter())
.filter_map(|s| {
glob::Pattern::new(s)
.inspect_err(|e| {
error!("invalid filter: {s:?}: {e}");
errors = true;
})
.ok()
})
.collect();
if errors {
return Err(format_err!("invalid filters"));
}
Ok(filters)
}