diff --git a/src/main.rs b/src/main.rs index 333672b..cd2576f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use directories::ProjectDirs; extern crate prettytable; use std::str::FromStr; +use serde::{Deserialize, Serialize}; extern crate lazy_static; @@ -21,6 +22,9 @@ pub mod meta_plugin; //pub mod item; extern crate term; +extern crate serde_json; +extern crate serde_yaml; +extern crate serde; /** * Main struct for command-line arguments. @@ -105,6 +109,7 @@ struct ItemArgs { meta_plugins: Vec, } + /** * Struct for general options. */ @@ -133,6 +138,10 @@ struct OptionsArgs { #[arg(short, long)] #[arg(help("Do not show any messages"))] quiet: bool, + + #[arg(long, value_enum, default_value("table"))] + #[arg(help("Output format (only works with --info, --status, --list)"))] + output_format: Option, } /** @@ -252,6 +261,16 @@ fn main() -> Result<(), Error> { } } + // Validate output format usage + if let Some(output_format_str) = &args.options.output_format { + if output_format_str != "table" && mode != KeepModes::Info && mode != KeepModes::Status && mode != KeepModes::List { + cmd.error( + ErrorKind::InvalidValue, + "--output-format can only be used with --info, --status, or --list modes" + ).exit(); + } + } + debug!("MAIN: args: {:?}", args); debug!("MAIN: ids: {:?}", ids); debug!("MAIN: tags: {:?}", tags); diff --git a/src/modes/common.rs b/src/modes/common.rs index 87862c2..442c458 100644 --- a/src/modes/common.rs +++ b/src/modes/common.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use std::env; use std::str::FromStr; use strum::IntoEnumIterator; +use serde::{Deserialize, Serialize}; pub fn get_meta_from_env() -> HashMap { debug!("COMMON: Getting meta from KEEP_META_*"); @@ -162,6 +163,33 @@ pub fn cmd_args_compression_type(cmd: &mut Command, args: &Args) -> CompressionT compression_type_opt.unwrap() } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum OutputFormat { + Table, + Json, + Yaml, +} + +impl FromStr for OutputFormat { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "table" => Ok(OutputFormat::Table), + "json" => Ok(OutputFormat::Json), + "yaml" => Ok(OutputFormat::Yaml), + _ => Err(anyhow::anyhow!("Invalid output format. Supported formats: table, json, yaml")), + } + } +} + +pub fn get_output_format(args: &Args) -> OutputFormat { + args.options.output_format + .as_ref() + .and_then(|s| OutputFormat::from_str(s).ok()) + .unwrap_or(OutputFormat::Table) +} + pub fn cmd_args_meta_plugin_types(cmd: &mut Command, args: &Args) -> Vec { let mut meta_plugin_types = Vec::new(); diff --git a/src/modes/info.rs b/src/modes/info.rs index 0ec0c35..ef8bab7 100644 --- a/src/modes/info.rs +++ b/src/modes/info.rs @@ -1,6 +1,9 @@ use crate::db::Item; -use crate::modes::common::format_size; +use crate::modes::common::{format_size, get_output_format, OutputFormat}; use anyhow::anyhow; +use serde_json; +use serde_yaml; +use serde::{Deserialize, Serialize}; use clap::Command; use clap::error::ErrorKind; use std::path::PathBuf; @@ -48,6 +51,20 @@ pub fn mode_info( } } +#[derive(Serialize, Deserialize)] +struct ItemInfo { + id: i64, + timestamp: String, + path: String, + stream_size: Option, + stream_size_formatted: String, + compression: String, + file_size: Option, + file_size_formatted: String, + tags: Vec, + meta: std::collections::HashMap, +} + fn show_item( item: Item, // Using the provided struct definition args: &crate::Args, @@ -61,6 +78,12 @@ fn show_item( .map(|x| x.name) .collect(); + let output_format = get_output_format(args); + + if output_format != OutputFormat::Table { + return show_item_structured(item, args, conn, data_path, output_format); + } + let mut table = Table::new(); if std::io::stdout().is_terminal() { table.set_format(get_format_box_chars_no_border_line_separator()); @@ -136,3 +159,61 @@ fn show_item( table.printstd(); Ok(()) } + +fn show_item_structured( + item: Item, + args: &crate::Args, + conn: &mut rusqlite::Connection, + data_path: PathBuf, + output_format: OutputFormat, +) -> anyhow::Result<()> { + let item_id = item.id.unwrap(); + let item_tags: Vec = crate::db::get_item_tags(conn, &item)? + .into_iter() + .map(|x| x.name) + .collect(); + + let mut item_path_buf = data_path.clone(); + item_path_buf.push(item_id.to_string()); + + let file_size = item_path_buf.metadata().map(|m| m.len()).ok(); + let file_size_formatted = match file_size { + Some(size) => format_size(size, args.options.human_readable), + None => "Missing".to_string(), + }; + + let stream_size_formatted = match item.size { + Some(size) => format_size(size as u64, args.options.human_readable), + None => "Missing".to_string(), + }; + + let mut meta_map = std::collections::HashMap::new(); + for meta in crate::db::get_item_meta(conn, &item)? { + meta_map.insert(meta.name, meta.value); + } + + let item_info = ItemInfo { + id: item_id, + timestamp: item.ts.with_timezone(&chrono::Local).format("%F %T %Z").to_string(), + path: item_path_buf.to_str().unwrap_or("").to_string(), + stream_size: item.size.map(|s| s as u64), + stream_size_formatted, + compression: item.compression, + file_size, + file_size_formatted, + tags: item_tags, + meta: meta_map, + }; + + match output_format { + OutputFormat::Json => { + println!("{}", serde_json::to_string_pretty(&item_info)?); + } + OutputFormat::Yaml => { + println!("{}", serde_yaml::to_string(&item_info)?); + } + OutputFormat::Table => unreachable!(), + } + + Ok(()) +} diff --git a/src/modes/list.rs b/src/modes/list.rs index 2550ba7..5668fe5 100644 --- a/src/modes/list.rs +++ b/src/modes/list.rs @@ -1,6 +1,9 @@ use crate::db::{get_items, get_items_matching}; use crate::modes::common::ColumnType; -use crate::modes::common::{size_column, string_column}; +use crate::modes::common::{size_column, string_column, get_output_format, OutputFormat}; +use serde::{Deserialize, Serialize}; +use serde_json; +use serde_yaml; use anyhow::anyhow; use log::debug; use prettytable::color; @@ -8,6 +11,20 @@ use prettytable::row; use prettytable::format::Alignment; use prettytable::{Attr, Cell, Row, Table}; +#[derive(Serialize, Deserialize)] +struct ListItem { + id: Option, + time: String, + size: Option, + size_formatted: String, + compression: String, + file_size: Option, + file_size_formatted: String, + file_path: String, + tags: Vec, + meta: std::collections::HashMap, +} + pub fn mode_list( cmd: &mut clap::Command, args: &crate::Args, @@ -54,6 +71,12 @@ pub fn mode_list( // Fetch all metadata for all items in a single query let meta_by_item = crate::db::get_meta_for_items(conn, &item_ids)?; + let output_format = get_output_format(args); + + if output_format != OutputFormat::Table { + return show_list_structured(items, tags_by_item, meta_by_item, data_path, args, output_format); + } + let mut table = Table::new(); table.set_format(*prettytable::format::consts::FORMAT_CLEAN); @@ -166,3 +189,61 @@ pub fn mode_list( Ok(()) } + +fn show_list_structured( + items: Vec, + tags_by_item: std::collections::HashMap>, + meta_by_item: std::collections::HashMap>, + data_path: std::path::PathBuf, + args: &crate::Args, + output_format: OutputFormat, +) -> anyhow::Result<()> { + let mut list_items = Vec::new(); + + for item in items { + let item_id = item.id.unwrap(); + let tags = tags_by_item.get(&item_id).cloned().unwrap_or_default(); + let meta = meta_by_item.get(&item_id).cloned().unwrap_or_default(); + + let mut item_path = data_path.clone(); + item_path.push(item_id.to_string()); + + let file_size = item_path.metadata().map(|m| m.len()).ok(); + let file_size_formatted = match file_size { + Some(size) => crate::modes::common::format_size(size, args.options.human_readable), + None => "Missing".to_string(), + }; + + let size_formatted = match item.size { + Some(size) => crate::modes::common::format_size(size as u64, args.options.human_readable), + None => "Unknown".to_string(), + }; + + let list_item = ListItem { + id: item.id, + time: item.ts.with_timezone(&chrono::Local).format("%F %T").to_string(), + size: item.size.map(|s| s as u64), + size_formatted, + compression: item.compression, + file_size, + file_size_formatted, + file_path: item_path.into_os_string().into_string().unwrap_or_default(), + tags, + meta, + }; + + list_items.push(list_item); + } + + match output_format { + OutputFormat::Json => { + println!("{}", serde_json::to_string_pretty(&list_items)?); + } + OutputFormat::Yaml => { + println!("{}", serde_yaml::to_string(&list_items)?); + } + OutputFormat::Table => unreachable!(), + } + + Ok(()) +} diff --git a/src/modes/status.rs b/src/modes/status.rs index 8c21b4c..8d088fb 100644 --- a/src/modes/status.rs +++ b/src/modes/status.rs @@ -8,8 +8,11 @@ use crate::compression_engine::COMPRESSION_PROGRAMS; use crate::compression_engine::CompressionType; use crate::compression_engine::program::CompressionEngineProgram; -use crate::modes::common::get_format_box_chars_no_border_line_separator; +use crate::modes::common::{get_format_box_chars_no_border_line_separator, get_output_format, OutputFormat}; use prettytable::color; +use serde::{Deserialize, Serialize}; +use serde_json; +use serde_yaml; use prettytable::row; use prettytable::{Attr, Cell, Row, Table}; use prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR; @@ -178,6 +181,138 @@ fn build_meta_plugin_table(enabled_meta_plugins: &Vec) -> Table meta_plugin_table } +fn show_status_structured( + data_path: PathBuf, + db_path: PathBuf, + enabled_meta_plugins: &Vec, + output_format: OutputFormat, +) -> Result<(), anyhow::Error> { + let path_info = PathInfo { + data: data_path.into_os_string().into_string().expect("Unable to convert data path to string"), + database: db_path.into_os_string().into_string().expect("Unable to convert DB path to string"), + }; + + let default_type = compression_engine::default_compression_type(); + let mut compression_info = Vec::new(); + + for compression_type in CompressionType::iter() { + let compression_program: CompressionEngineProgram = + match &COMPRESSION_PROGRAMS[compression_type.clone()] { + Some(compression_program) => compression_program.clone(), + None => CompressionEngineProgram { + program: "".to_string(), + compress: Vec::new(), + decompress: Vec::new(), + supported: true, + }, + }; + + let is_default = compression_type == default_type; + let binary = if compression_program.program.is_empty() { + "".to_string() + } else { + compression_program.program + }; + + compression_info.push(CompressionInfo { + compression_type: compression_type.to_string(), + found: compression_program.supported, + default: is_default, + binary, + compress: compression_program.compress.join(" "), + decompress: compression_program.decompress.join(" "), + }); + } + + let mut meta_plugin_info = Vec::new(); + + for meta_plugin_type in MetaPluginType::iter() { + let mut meta_plugin = meta_plugin::get_meta_plugin(meta_plugin_type.clone()); + let is_supported = meta_plugin.is_supported(); + let is_enabled = enabled_meta_plugins.contains(&meta_plugin_type); + + let (binary_display, args_display) = if !is_supported { + ("".to_string(), "".to_string()) + } else { + match meta_plugin_type { + MetaPluginType::DigestSha256 | MetaPluginType::ReadTime | MetaPluginType::ReadRate | + MetaPluginType::Cwd | MetaPluginType::Uid | MetaPluginType::User | + MetaPluginType::Gid | MetaPluginType::Group | MetaPluginType::Shell | + MetaPluginType::ShellPid | MetaPluginType::KeepPid | MetaPluginType::Hostname | + MetaPluginType::FullHostname => { + ("".to_string(), "".to_string()) + }, + _ => { + if let Some((program, args)) = meta_plugin.program_info() { + (program.to_string(), args.join(" ")) + } else { + ("".to_string(), "".to_string()) + } + } + } + }; + + meta_plugin_info.push(MetaPluginInfo { + meta_name: meta_plugin.meta_name(), + found: is_supported, + enabled: is_enabled, + binary: binary_display, + args: args_display, + }); + } + + let status_info = StatusInfo { + paths: path_info, + compression: compression_info, + meta_plugins: meta_plugin_info, + }; + + match output_format { + OutputFormat::Json => { + println!("{}", serde_json::to_string_pretty(&status_info)?); + } + OutputFormat::Yaml => { + println!("{}", serde_yaml::to_string(&status_info)?); + } + OutputFormat::Table => unreachable!(), + } + + Ok(()) +} + +#[derive(Serialize, Deserialize)] +struct StatusInfo { + paths: PathInfo, + compression: Vec, + meta_plugins: Vec, +} + +#[derive(Serialize, Deserialize)] +struct PathInfo { + data: String, + database: String, +} + +#[derive(Serialize, Deserialize)] +struct CompressionInfo { + #[serde(rename = "type")] + compression_type: String, + found: bool, + default: bool, + binary: String, + compress: String, + decompress: String, +} + +#[derive(Serialize, Deserialize)] +struct MetaPluginInfo { + meta_name: String, + found: bool, + enabled: bool, + binary: String, + args: String, +} + pub fn mode_status( _cmd: &mut Command, args: &crate::Args, @@ -201,6 +336,12 @@ pub fn mode_status( } } + let output_format = get_output_format(args); + + if output_format != OutputFormat::Table { + return show_status_structured(data_path, db_path, &meta_plugin_types, output_format); + } + println!("PATHS:"); build_path_table(data_path, db_path).printstd(); println!();