feat: add --update mode, --meta/--meta-plugin flags, streaming diff
- Add --update mode to modify tags and metadata for existing items by ID
- Add --meta key=value flag to set metadata during save/update
- Add --meta key (bare) to delete metadata keys or filter by existence
- Add --meta-plugin/-M name:{json} flag for plugin options via CLI
- Env meta plugin now uses options from --meta-plugin instead of only env vars
- Stream decompressed content to diff via /dev/fd pipes (no temp files)
- Wire --list-format CLI arg to settings (was parsed but ignored)
- Allow --info to accept tags (was restricted to numeric IDs only)
- Change DB meta filtering to HashMap<String, Option<String>> for exact match + key existence
- Fix fcntl error checking in diff pre_exec
- Fix README inaccuracies (delete by tag, nonexistent --digest flag, meta plugin key names)
This commit is contained in:
120
src/config.rs
120
src/config.rs
@@ -209,6 +209,9 @@ pub struct Settings {
|
||||
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>)>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -330,19 +333,8 @@ impl Settings {
|
||||
config_builder.set_override("compression_plugin.name", compression.as_str())?;
|
||||
}
|
||||
|
||||
if !args.item.meta_plugins.is_empty() {
|
||||
let meta_plugins: Vec<std::collections::HashMap<String, String>> = args
|
||||
.item
|
||||
.meta_plugins
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert("name".to_string(), name.clone());
|
||||
map
|
||||
})
|
||||
.collect();
|
||||
config_builder = config_builder.set_override("meta_plugins", meta_plugins)?;
|
||||
}
|
||||
// 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");
|
||||
@@ -438,6 +430,57 @@ impl Settings {
|
||||
}]);
|
||||
}
|
||||
|
||||
// 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:?}");
|
||||
@@ -469,6 +512,20 @@ impl Settings {
|
||||
.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();
|
||||
|
||||
debug!("CONFIG: Final settings: {settings:?}");
|
||||
Ok(settings)
|
||||
}
|
||||
@@ -642,4 +699,41 @@ impl Settings {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user