Outputs one ID per line with no header. Errors if used with any mode other than --list. Works with both local and client (remote) list.
756 lines
28 KiB
Rust
756 lines
28 KiB
Rust
use crate::args::Args;
|
|
use anyhow::{Context, Result};
|
|
use dirs;
|
|
use log::{debug, error};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ColumnAlignment {
|
|
#[default]
|
|
Left,
|
|
Right,
|
|
Center,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ContentArrangement {
|
|
#[default]
|
|
Dynamic,
|
|
DynamicFullWidth,
|
|
Disabled,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum TableStyle {
|
|
Ascii,
|
|
Utf8,
|
|
Utf8Full,
|
|
#[default]
|
|
Nothing,
|
|
Custom(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum TableColor {
|
|
Black,
|
|
Red,
|
|
Green,
|
|
Yellow,
|
|
Blue,
|
|
Magenta,
|
|
Cyan,
|
|
White,
|
|
Gray,
|
|
DarkRed,
|
|
DarkGreen,
|
|
DarkYellow,
|
|
DarkBlue,
|
|
DarkMagenta,
|
|
DarkCyan,
|
|
Rgb(u8, u8, u8),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum TableAttribute {
|
|
Bold,
|
|
Dim,
|
|
Italic,
|
|
Underlined,
|
|
SlowBlink,
|
|
RapidBlink,
|
|
Reverse,
|
|
Hidden,
|
|
CrossedOut,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct TableConfig {
|
|
#[serde(default)]
|
|
pub style: TableStyle,
|
|
#[serde(default)]
|
|
pub modifiers: Vec<String>,
|
|
#[serde(default)]
|
|
pub content_arrangement: ContentArrangement,
|
|
#[serde(default)]
|
|
pub truncation_indicator: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Default)]
|
|
pub struct ColumnConfig {
|
|
pub name: String,
|
|
pub label: String,
|
|
#[serde(default)]
|
|
pub align: ColumnAlignment,
|
|
#[serde(default)]
|
|
pub max_len: Option<String>,
|
|
#[serde(default)]
|
|
pub fg_color: Option<TableColor>,
|
|
#[serde(default)]
|
|
pub bg_color: Option<TableColor>,
|
|
#[serde(default)]
|
|
pub attributes: Vec<TableAttribute>,
|
|
#[serde(default)]
|
|
pub padding: Option<(u16, u16)>,
|
|
}
|
|
|
|
impl<'de> serde::Deserialize<'de> for ColumnConfig {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
#[derive(Deserialize)]
|
|
struct Helper {
|
|
name: String,
|
|
label: Option<String>,
|
|
#[serde(default)]
|
|
align: ColumnAlignment,
|
|
#[serde(default)]
|
|
max_len: Option<String>,
|
|
#[serde(default)]
|
|
fg_color: Option<TableColor>,
|
|
#[serde(default)]
|
|
bg_color: Option<TableColor>,
|
|
#[serde(default)]
|
|
attributes: Vec<TableAttribute>,
|
|
#[serde(default)]
|
|
padding: Option<(u16, u16)>,
|
|
}
|
|
|
|
let helper = Helper::deserialize(deserializer)?;
|
|
let label = helper.label.unwrap_or_else(|| helper.name.clone());
|
|
|
|
Ok(ColumnConfig {
|
|
name: helper.name,
|
|
label,
|
|
align: helper.align,
|
|
max_len: helper.max_len,
|
|
fg_color: helper.fg_color,
|
|
bg_color: helper.bg_color,
|
|
attributes: helper.attributes,
|
|
padding: helper.padding,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct ServerConfig {
|
|
pub address: Option<String>,
|
|
pub port: Option<u16>,
|
|
pub username: Option<String>,
|
|
pub password_file: Option<PathBuf>,
|
|
pub password: Option<String>,
|
|
pub password_hash: Option<String>,
|
|
pub jwt_secret: Option<String>,
|
|
pub jwt_secret_file: Option<PathBuf>,
|
|
pub cert_file: Option<PathBuf>,
|
|
pub key_file: Option<PathBuf>,
|
|
pub cors_origin: Option<String>,
|
|
pub max_body_size: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct CompressionPluginConfig {
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct ClientConfig {
|
|
pub url: Option<String>,
|
|
pub username: Option<String>,
|
|
pub password: Option<String>,
|
|
pub jwt: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
|
|
pub struct MetaPluginConfig {
|
|
pub name: String,
|
|
#[serde(default)]
|
|
#[cfg_attr(feature = "server", schema(value_type = Object))]
|
|
pub options: std::collections::HashMap<String, serde_yaml::Value>,
|
|
#[serde(default)]
|
|
#[cfg_attr(feature = "server", schema(value_type = Object))]
|
|
pub outputs: std::collections::HashMap<String, String>,
|
|
}
|
|
|
|
/// Unified settings that merges config file and CLI arguments
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct Settings {
|
|
#[serde(default)]
|
|
pub dir: PathBuf,
|
|
#[serde(default)]
|
|
pub list_format: Vec<ColumnConfig>,
|
|
#[serde(default)]
|
|
pub table_config: TableConfig,
|
|
#[serde(default)]
|
|
pub human_readable: bool,
|
|
#[serde(default)]
|
|
pub ids_only: bool,
|
|
pub output_format: Option<String>,
|
|
#[serde(default)]
|
|
pub quiet: bool,
|
|
#[serde(default)]
|
|
pub force: bool,
|
|
pub server: Option<ServerConfig>,
|
|
pub compression_plugin: Option<CompressionPluginConfig>,
|
|
pub meta_plugins: Option<Vec<MetaPluginConfig>>,
|
|
pub client: Option<ClientConfig>,
|
|
// Non-serializable fields populated from CLI args
|
|
#[serde(skip)]
|
|
pub client_url: Option<String>,
|
|
#[serde(skip)]
|
|
pub client_username: Option<String>,
|
|
#[serde(skip)]
|
|
pub client_password: Option<String>,
|
|
#[serde(skip)]
|
|
pub client_jwt: Option<String>,
|
|
// Metadata key-value pairs from --meta CLI flag
|
|
#[serde(skip)]
|
|
pub meta: Vec<(String, Option<String>)>,
|
|
// Export filename format template (--export-filename-format)
|
|
#[serde(skip)]
|
|
pub export_filename_format: String,
|
|
// Import data file path (--import-data-file)
|
|
#[serde(skip)]
|
|
pub import_data_file: Option<std::path::PathBuf>,
|
|
}
|
|
|
|
impl Settings {
|
|
/// Create unified settings from config and args with proper priority
|
|
pub fn new(args: &Args, default_dir: PathBuf) -> Result<Self> {
|
|
debug!("CONFIG: Creating settings with default dir: {default_dir:?}");
|
|
|
|
let config_path = if let Some(config_path) = &args.options.config {
|
|
config_path.clone()
|
|
} else if let Ok(env_config) = std::env::var("KEEP_CONFIG") {
|
|
PathBuf::from(env_config)
|
|
} else {
|
|
let default_path = if let Ok(home_dir) = std::env::var("HOME") {
|
|
let mut path = PathBuf::from(home_dir);
|
|
path.push(".config");
|
|
path.push("keep");
|
|
path.push("config.yml");
|
|
path
|
|
} else {
|
|
PathBuf::from("~/.config/keep/config.yml")
|
|
};
|
|
debug!("CONFIG: Using default config path: {default_path:?}");
|
|
default_path
|
|
};
|
|
|
|
debug!("CONFIG: Using config path: {config_path:?}");
|
|
|
|
let mut config_builder = config::Config::builder();
|
|
|
|
// Load config file if it exists
|
|
if config_path.exists() {
|
|
debug!("CONFIG: Loading config file: {config_path:?}");
|
|
config_builder =
|
|
config_builder.add_source(config::File::from(config_path.clone()).required(false));
|
|
} else {
|
|
debug!("CONFIG: Config file does not exist: {config_path:?}");
|
|
}
|
|
|
|
// Add environment variables
|
|
debug!("CONFIG: Adding environment variables");
|
|
let env_source = config::Environment::with_prefix("KEEP")
|
|
.separator("__")
|
|
.ignore_empty(true);
|
|
config_builder = config_builder.add_source(env_source);
|
|
|
|
// Override with CLI args
|
|
if let Some(dir) = &args.options.dir {
|
|
debug!("CONFIG: Overriding dir with CLI arg: {dir:?}");
|
|
config_builder = config_builder.set_override(
|
|
"dir",
|
|
dir.to_str()
|
|
.ok_or_else(|| anyhow::anyhow!("non-UTF-8 directory path"))?,
|
|
)?;
|
|
}
|
|
|
|
if args.options.human_readable {
|
|
config_builder = config_builder.set_override("human_readable", true)?;
|
|
}
|
|
|
|
if args.options.ids_only {
|
|
config_builder = config_builder.set_override("ids_only", true)?;
|
|
}
|
|
|
|
if let Some(output_format) = &args.options.output_format {
|
|
config_builder =
|
|
config_builder.set_override("output_format", output_format.as_str())?;
|
|
}
|
|
|
|
if args.options.verbose > 0 {
|
|
config_builder = config_builder.set_override("verbose", args.options.verbose)?;
|
|
}
|
|
|
|
if args.options.quiet {
|
|
config_builder = config_builder.set_override("quiet", true)?;
|
|
}
|
|
|
|
if args.options.force {
|
|
config_builder = config_builder.set_override("force", true)?;
|
|
}
|
|
|
|
if let Some(server_password) = &args.options.server_password {
|
|
config_builder =
|
|
config_builder.set_override("server.password", server_password.as_str())?;
|
|
}
|
|
|
|
if let Some(server_password_hash) = &args.options.server_password_hash {
|
|
config_builder = config_builder
|
|
.set_override("server.password_hash", server_password_hash.as_str())?;
|
|
}
|
|
|
|
if let Some(server_username) = &args.options.server_username {
|
|
config_builder =
|
|
config_builder.set_override("server.username", server_username.as_str())?;
|
|
}
|
|
|
|
if let Some(server_address) = &args.mode.server_address {
|
|
config_builder =
|
|
config_builder.set_override("server.address", server_address.as_str())?;
|
|
}
|
|
|
|
if let Some(server_port) = args.mode.server_port {
|
|
config_builder = config_builder.set_override("server.port", server_port)?;
|
|
}
|
|
|
|
#[cfg(feature = "tls")]
|
|
if let Some(server_cert) = &args.mode.server_cert {
|
|
config_builder = config_builder
|
|
.set_override("server.cert_file", server_cert.to_string_lossy().as_ref())?;
|
|
}
|
|
|
|
#[cfg(feature = "tls")]
|
|
if let Some(server_key) = &args.mode.server_key {
|
|
config_builder = config_builder
|
|
.set_override("server.key_file", server_key.to_string_lossy().as_ref())?;
|
|
}
|
|
|
|
if let Some(max_body_size) = args.options.server_max_body_size {
|
|
config_builder = config_builder.set_override("server.max_body_size", max_body_size)?;
|
|
}
|
|
|
|
if let Some(compression) = &args.item.compression {
|
|
config_builder =
|
|
config_builder.set_override("compression_plugin.name", compression.as_str())?;
|
|
}
|
|
|
|
// Build MetaPluginConfig entries from --meta-plugin args (name[:json])
|
|
// These are handled after config deserialization (see below).
|
|
|
|
let config = config_builder.build()?;
|
|
debug!("CONFIG: Built config, attempting to deserialize");
|
|
|
|
match config.try_deserialize::<Settings>() {
|
|
Ok(mut settings) => {
|
|
debug!("CONFIG: Successfully deserialized settings: {settings:?}");
|
|
|
|
// Set defaults for list_format if not provided
|
|
if settings.list_format.is_empty() {
|
|
debug!("CONFIG: Setting default list_format");
|
|
settings.list_format = vec![
|
|
ColumnConfig {
|
|
name: "id".to_string(),
|
|
label: "Item".to_string(),
|
|
align: ColumnAlignment::Right,
|
|
max_len: None,
|
|
fg_color: None,
|
|
bg_color: None,
|
|
attributes: Vec::new(),
|
|
padding: None,
|
|
},
|
|
ColumnConfig {
|
|
name: "time".to_string(),
|
|
label: "Time".to_string(),
|
|
align: ColumnAlignment::Right,
|
|
max_len: None,
|
|
fg_color: None,
|
|
bg_color: None,
|
|
attributes: Vec::new(),
|
|
padding: None,
|
|
},
|
|
ColumnConfig {
|
|
name: "size".to_string(),
|
|
label: "Size".to_string(),
|
|
align: ColumnAlignment::Right,
|
|
max_len: None,
|
|
fg_color: None,
|
|
bg_color: None,
|
|
attributes: Vec::new(),
|
|
padding: None,
|
|
},
|
|
ColumnConfig {
|
|
name: "meta:text_line_count".to_string(),
|
|
label: "Lines".to_string(),
|
|
align: ColumnAlignment::Right,
|
|
max_len: None,
|
|
fg_color: None,
|
|
bg_color: None,
|
|
attributes: Vec::new(),
|
|
padding: None,
|
|
},
|
|
ColumnConfig {
|
|
name: "tags".to_string(),
|
|
label: "Tags".to_string(),
|
|
align: ColumnAlignment::Left,
|
|
max_len: None,
|
|
fg_color: None,
|
|
bg_color: None,
|
|
attributes: Vec::new(),
|
|
padding: None,
|
|
},
|
|
ColumnConfig {
|
|
name: "meta:hostname_short".to_string(),
|
|
label: "Host".to_string(),
|
|
align: ColumnAlignment::Left,
|
|
max_len: None,
|
|
fg_color: None,
|
|
bg_color: None,
|
|
attributes: Vec::new(),
|
|
padding: None,
|
|
},
|
|
ColumnConfig {
|
|
name: "meta:command".to_string(),
|
|
label: "Command".to_string(),
|
|
align: ColumnAlignment::Left,
|
|
max_len: None,
|
|
fg_color: None,
|
|
bg_color: None,
|
|
attributes: Vec::new(),
|
|
padding: None,
|
|
},
|
|
];
|
|
}
|
|
|
|
// Set default meta_plugins to include 'env' if not provided
|
|
if settings.meta_plugins.is_none() {
|
|
debug!("CONFIG: Setting default meta_plugins to include 'env'");
|
|
settings.meta_plugins = Some(vec![MetaPluginConfig {
|
|
name: "env".to_string(),
|
|
options: std::collections::HashMap::new(),
|
|
outputs: std::collections::HashMap::new(),
|
|
}]);
|
|
}
|
|
|
|
// Override meta_plugins from --meta-plugin CLI args
|
|
if !args.item.meta_plugins.is_empty() {
|
|
debug!("CONFIG: Overriding meta_plugins from --meta-plugin CLI args");
|
|
let cli_plugins: Vec<MetaPluginConfig> = args
|
|
.item
|
|
.meta_plugins
|
|
.iter()
|
|
.map(|arg| {
|
|
let mut options = std::collections::HashMap::new();
|
|
let mut outputs = std::collections::HashMap::new();
|
|
if let Some(serde_json::Value::Object(obj)) = &arg.options {
|
|
// Extract options and outputs from JSON value
|
|
if let Some(serde_json::Value::Object(opts_obj)) =
|
|
obj.get("options")
|
|
{
|
|
for (k, v) in opts_obj {
|
|
let yaml_str = serde_json::to_string(v).unwrap_or_default();
|
|
let yaml_val: serde_yaml::Value =
|
|
serde_yaml::from_str(&yaml_str)
|
|
.unwrap_or(serde_yaml::Value::Null);
|
|
options.insert(k.clone(), yaml_val);
|
|
}
|
|
}
|
|
if let Some(serde_json::Value::Object(outs_obj)) =
|
|
obj.get("outputs")
|
|
{
|
|
for (k, v) in outs_obj {
|
|
let val_str = match v {
|
|
serde_json::Value::String(s) => s.clone(),
|
|
_ => v.to_string(),
|
|
};
|
|
outputs.insert(k.clone(), val_str);
|
|
}
|
|
}
|
|
}
|
|
MetaPluginConfig {
|
|
name: arg.name.clone(),
|
|
options,
|
|
outputs,
|
|
}
|
|
})
|
|
.collect();
|
|
settings.meta_plugins = Some(cli_plugins);
|
|
}
|
|
|
|
// Override list_format from --list-format CLI arg
|
|
if args.options.list_format != "id,time,size,tags,meta:hostname" {
|
|
debug!("CONFIG: Overriding list_format from --list-format CLI arg");
|
|
settings.list_format = Settings::parse_list_format(&args.options.list_format);
|
|
}
|
|
|
|
// Set dir to default if not provided or is empty
|
|
if settings.dir == PathBuf::new() {
|
|
debug!("CONFIG: Setting default dir: {default_dir:?}");
|
|
settings.dir = default_dir;
|
|
}
|
|
|
|
// Populate client settings from CLI args and config
|
|
#[cfg(feature = "client")]
|
|
{
|
|
settings.client_url = args
|
|
.options
|
|
.client_url
|
|
.clone()
|
|
.or_else(|| settings.client.as_ref().and_then(|c| c.url.clone()));
|
|
settings.client_username = args
|
|
.options
|
|
.client_username
|
|
.clone()
|
|
.or_else(|| settings.client.as_ref().and_then(|c| c.username.clone()));
|
|
settings.client_password = args
|
|
.options
|
|
.client_password
|
|
.clone()
|
|
.or_else(|| settings.client.as_ref().and_then(|c| c.password.clone()));
|
|
settings.client_jwt = args
|
|
.options
|
|
.client_jwt
|
|
.clone()
|
|
.or_else(|| settings.client.as_ref().and_then(|c| c.jwt.clone()));
|
|
}
|
|
|
|
// Parse --meta key=value and bare key arguments
|
|
settings.meta = args
|
|
.item
|
|
.meta
|
|
.iter()
|
|
.map(|s| {
|
|
if let Some((key, value)) = s.split_once('=') {
|
|
(key.to_string(), Some(value.to_string()))
|
|
} else {
|
|
(s.to_string(), None)
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
// Set export filename format from CLI args
|
|
settings.export_filename_format = args.item.export_filename_format.clone();
|
|
settings.import_data_file = args.item.import_data_file.clone();
|
|
|
|
debug!("CONFIG: Final settings: {settings:?}");
|
|
Ok(settings)
|
|
}
|
|
Err(e) => {
|
|
error!("CONFIG: Failed to deserialize settings: {e}");
|
|
Err(e.into())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn default_dir() -> anyhow::Result<PathBuf> {
|
|
let mut path =
|
|
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("No home directory found"))?;
|
|
path.push(".keep");
|
|
if !path.exists() {
|
|
std::fs::create_dir_all(&path)?;
|
|
}
|
|
Ok(path)
|
|
}
|
|
|
|
/// Get server password from password_file or directly from config if configured
|
|
pub fn get_server_password(&self) -> Result<Option<String>> {
|
|
if let Some(server) = &self.server {
|
|
// First check for password_file
|
|
if let Some(password_file) = &server.password_file {
|
|
debug!("CONFIG: Reading password from file: {password_file:?}");
|
|
let password = fs::read(password_file)
|
|
.with_context(|| format!("Failed to read password file: {password_file:?}"))?;
|
|
let end = password.len().min(4096);
|
|
let password = String::from_utf8_lossy(&password[..end]).trim().to_string();
|
|
return Ok(Some(password));
|
|
}
|
|
|
|
// Fall back to direct password field
|
|
if let Some(password) = &server.password {
|
|
debug!("CONFIG: Using password from config");
|
|
return Ok(Some(password.clone()));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
// Helper methods to access configuration values
|
|
pub fn server_password(&self) -> Option<String> {
|
|
self.get_server_password().ok().flatten()
|
|
}
|
|
|
|
pub fn server_password_hash(&self) -> Option<String> {
|
|
self.server.as_ref().and_then(|s| s.password_hash.clone())
|
|
}
|
|
|
|
pub fn server_username(&self) -> Option<String> {
|
|
self.server.as_ref().and_then(|s| s.username.clone())
|
|
}
|
|
|
|
/// Get JWT secret from jwt_secret_file or directly from config if configured
|
|
pub fn get_server_jwt_secret(&self) -> Result<Option<String>> {
|
|
if let Some(server) = &self.server {
|
|
// First check for jwt_secret_file
|
|
if let Some(jwt_secret_file) = &server.jwt_secret_file {
|
|
debug!("CONFIG: Reading JWT secret from file: {jwt_secret_file:?}");
|
|
let secret = fs::read(jwt_secret_file).with_context(|| {
|
|
format!("Failed to read JWT secret file: {jwt_secret_file:?}")
|
|
})?;
|
|
let end = secret.len().min(4096);
|
|
let secret = String::from_utf8_lossy(&secret[..end]).trim().to_string();
|
|
return Ok(Some(secret));
|
|
}
|
|
|
|
// Fall back to direct jwt_secret field
|
|
if let Some(secret) = &server.jwt_secret {
|
|
debug!("CONFIG: Using JWT secret from config");
|
|
return Ok(Some(secret.clone()));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
pub fn server_jwt_secret(&self) -> Option<String> {
|
|
self.get_server_jwt_secret().ok().flatten()
|
|
}
|
|
|
|
pub fn server_address(&self) -> Option<String> {
|
|
self.server.as_ref().and_then(|s| s.address.clone())
|
|
}
|
|
|
|
pub fn server_port(&self) -> Option<u16> {
|
|
self.server.as_ref().and_then(|s| s.port)
|
|
}
|
|
|
|
pub fn server_cert_file(&self) -> Option<PathBuf> {
|
|
self.server.as_ref().and_then(|s| s.cert_file.clone())
|
|
}
|
|
|
|
pub fn server_key_file(&self) -> Option<PathBuf> {
|
|
self.server.as_ref().and_then(|s| s.key_file.clone())
|
|
}
|
|
|
|
pub fn server_cors_origin(&self) -> Option<String> {
|
|
self.server.as_ref().and_then(|s| s.cors_origin.clone())
|
|
}
|
|
|
|
pub fn compression(&self) -> Option<String> {
|
|
self.compression_plugin.as_ref().map(|c| c.name.clone())
|
|
}
|
|
|
|
pub fn meta_plugins_names(&self) -> Vec<String> {
|
|
self.meta_plugins
|
|
.as_ref()
|
|
.map(|plugins| plugins.iter().map(|p| p.name.clone()).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Validates the configuration against plugin schemas.
|
|
///
|
|
/// Checks that:
|
|
/// - All configured meta plugin names are valid and registered
|
|
/// - Required options are present for each meta plugin
|
|
/// - Compression plugin name (if set) is a valid compression type
|
|
///
|
|
/// Returns a list of warning strings. An empty list means the config is valid.
|
|
pub fn validate_config(&self) -> Vec<String> {
|
|
use crate::common::schema::gather_meta_plugin_schemas;
|
|
use crate::compression_engine::CompressionType;
|
|
use strum::IntoEnumIterator;
|
|
|
|
let mut warnings = Vec::new();
|
|
|
|
// Validate compression plugin
|
|
if let Some(ref comp) = self.compression_plugin {
|
|
let valid_types: Vec<String> =
|
|
CompressionType::iter().map(|ct| ct.to_string()).collect();
|
|
if !valid_types.contains(&comp.name) {
|
|
warnings.push(format!(
|
|
"Unknown compression_plugin.name: '{}'. Valid types: {}",
|
|
comp.name,
|
|
valid_types.join(", ")
|
|
));
|
|
}
|
|
}
|
|
|
|
// Validate meta plugins
|
|
if let Some(ref plugins) = self.meta_plugins {
|
|
let schemas = gather_meta_plugin_schemas();
|
|
let schema_map: std::collections::HashMap<&str, &crate::common::schema::PluginSchema> =
|
|
schemas.iter().map(|s| (s.name.as_str(), s)).collect();
|
|
|
|
for plugin in plugins {
|
|
match schema_map.get(plugin.name.as_str()) {
|
|
Some(schema) => {
|
|
// Check required options
|
|
for opt in &schema.options {
|
|
if opt.required && !plugin.options.contains_key(&opt.name) {
|
|
warnings.push(format!(
|
|
"Meta plugin '{}': missing required option '{}'",
|
|
plugin.name, opt.name
|
|
));
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
warnings.push(format!(
|
|
"Unknown meta plugin: '{}'. Available: {}",
|
|
plugin.name,
|
|
schema_map.keys().copied().collect::<Vec<_>>().join(", ")
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
warnings
|
|
}
|
|
|
|
/// Parse a comma-separated column list string into Vec<ColumnConfig>.
|
|
///
|
|
/// Maps known column names to their default labels and alignment.
|
|
/// For unknown names (including meta:* columns), uses the name as its own label.
|
|
fn parse_list_format(input: &str) -> Vec<ColumnConfig> {
|
|
input
|
|
.split(',')
|
|
.map(|s| s.trim())
|
|
.filter(|s| !s.is_empty())
|
|
.map(|name| {
|
|
let (label, align) = match name {
|
|
"id" => ("Item", ColumnAlignment::Right),
|
|
"time" => ("Time", ColumnAlignment::Right),
|
|
"size" => ("Size", ColumnAlignment::Right),
|
|
"meta:text_line_count" => ("Lines", ColumnAlignment::Right),
|
|
"meta:token_count" => ("Tokens", ColumnAlignment::Right),
|
|
"tags" => ("Tags", ColumnAlignment::Left),
|
|
"meta:hostname_short" => ("Host", ColumnAlignment::Left),
|
|
"meta:hostname" => ("Host", ColumnAlignment::Left),
|
|
"meta:command" => ("Command", ColumnAlignment::Left),
|
|
"compression" => ("Compression", ColumnAlignment::Left),
|
|
other if other.starts_with("meta:") => {
|
|
let sub = other.strip_prefix("meta:").unwrap_or(other);
|
|
(sub, ColumnAlignment::Left)
|
|
}
|
|
other => (other, ColumnAlignment::Left),
|
|
};
|
|
ColumnConfig {
|
|
name: name.to_string(),
|
|
label: label.to_string(),
|
|
align,
|
|
..Default::default()
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
}
|