use std::path::PathBuf; use anyhow::{Context, Error, Result, anyhow}; use clap::*; use clap::error::ErrorKind; use log::*; mod modes; extern crate directories; use directories::ProjectDirs; extern crate prettytable; use std::str::FromStr; extern crate lazy_static; pub mod compression_engine; pub mod db; pub mod plugins; 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. */ #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { #[command(flatten)] mode: ModeArgs, #[command(flatten)] item: ItemArgs, #[command(flatten)] options: OptionsArgs, #[arg(help("A list of either item IDs or tags"))] ids_or_tags: Vec, } /** * Struct for mode-specific arguments. */ #[derive(Parser, Debug)] struct ModeArgs { #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["get", "diff", "list", "update", "delete", "info", "status"]))] #[arg(help("Save an item using any tags or metadata provided"))] save: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "diff", "list", "update", "delete", "info", "status"]))] #[arg(help( "Get an item either by it's ID or by a combination of matching tags and metatdata" ))] get: bool, #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "list", "update", "delete", "info", "status"]))] #[arg(help("Show a diff between two items by ID"))] diff: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "update", "delete", "info", "status"]))] #[arg(help("List items, filtering on tags or metadata if given"))] list: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "status"]), requires("ids_or_tags"))] #[arg(help("Update a specified item ID's tags and/or metadata"))] update: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "update", "info", "status"]), requires("ids_or_tags"))] #[arg(help("Delete items either by ID or by matching tags"))] delete: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "update", "delete", "status"]), requires("ids_or_tags"))] #[arg(help( "Get an item either by it's ID or by a combination of matching tags and metatdata" ))] info: bool, #[arg(group("mode"), help_heading("Mode Options"), short('S'), long, conflicts_with_all(["save", "get", "diff", "list", "update", "delete", "info"]))] #[arg(help("Show status of directories and supported compression algorithms"))] status: bool, } /** * Struct for item-specific arguments. */ #[derive(Parser, Debug)] struct ItemArgs { #[arg(help_heading("Item Options"), short, long, conflicts_with_all(["get", "delete", "status"]))] #[arg(help( "Set metadata for the item using the format KEY=[VALUE], the metadata will be removed if VALUE is not provided" ))] meta: Vec, #[arg(help_heading("Item Options"), long, env("KEEP_DIGEST"))] #[arg(help("Digest algorithm to use when saving items"))] digest: Option, #[arg(help_heading("Item Options"), short, long, env("KEEP_COMPRESSION"))] #[arg(help("Compression algorithm to use when saving items"))] compression: Option, #[arg(help_heading("Item Options"), short('M'), long, env("KEEP_META_PLUGINS"))] #[arg(help("Meta plugins to use when saving items"))] meta_plugins: Vec, } /** * Struct for general options. */ #[derive(Parser, Debug)] struct OptionsArgs { #[arg(long, env("KEEP_DIR"))] #[arg(help("Specify the directory to use for storage"))] dir: Option, #[arg( long, env("KEEP_LIST_FORMAT"), default_value("id,time,size,tags,meta:hostname") )] #[arg(help("A comma separated list of columns to display with --list"))] list_format: String, #[arg(short('H'), long)] #[arg(help("Display file sizes with units"))] human_readable: bool, #[arg(short, long, action = clap::ArgAction::Count, conflicts_with("quiet"))] #[arg(help("Increase message verbosity, can be given more than once"))] verbose: u8, #[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, } /** * Enum representing the different modes of operation. */ #[derive(Debug, PartialEq)] enum KeepModes { Unknown, Save, Get, Diff, List, Update, Delete, Info, Status, } /** * Struct for key-value pairs. */ #[derive(Debug, Clone)] struct KeyValue { key: String, value: String, } impl FromStr for KeyValue { type Err = Error; fn from_str(s: &str) -> Result { match s.split_once('=') { Some(kv) => Ok(KeyValue { key: kv.0.to_string(), value: kv.1.to_string(), }), None => Err(anyhow!("Unable to parse key=value pair")), } } } /** * Enum for representing either a number or a string. */ #[derive(Debug, Clone)] enum NumberOrString { Number(i64), Str(String), } impl FromStr for NumberOrString { type Err = Error; fn from_str(s: &str) -> Result { Ok(s.parse::() .map(NumberOrString::Number) .unwrap_or_else(|_| NumberOrString::Str(s.to_string()))) } } /** * Main function to handle command-line arguments and execute the appropriate mode. */ fn main() -> Result<(), Error> { use std::fs; let proj_dirs = ProjectDirs::from("gt0.ca", "Andrew Phillips", "Keep"); let mut cmd = Args::command(); let mut args = Args::parse(); stderrlog::new() .module(module_path!()) .quiet(args.options.quiet) .verbosity(usize::from(args.options.verbose + 2)) //.timestamp(stderrlog::Timestamp::Second) .init() .unwrap(); debug!("MAIN: Start"); let ids = &mut Vec::new(); let tags = &mut Vec::new(); for v in args.ids_or_tags.iter() { match v.clone() { NumberOrString::Number(num) => ids.push(num), NumberOrString::Str(str) => tags.push(str), } } tags.sort(); tags.dedup(); let mut mode: KeepModes = KeepModes::Unknown; if args.mode.save { mode = KeepModes::Save; } else if args.mode.get { mode = KeepModes::Get; } else if args.mode.diff { mode = KeepModes::Diff; } else if args.mode.list { mode = KeepModes::List; } else if args.mode.delete { mode = KeepModes::Delete; } else if args.mode.update { mode = KeepModes::Update; } else if args.mode.info { mode = KeepModes::Info; } else if args.mode.status { mode = KeepModes::Status; } if mode == KeepModes::Unknown { if !ids.is_empty() { mode = KeepModes::Get; } else { mode = KeepModes::Save; } } // 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); debug!("MAIN: mode: {:?}", mode); if args.options.dir.is_none() { match proj_dirs { Some(proj_dirs) => args.options.dir = Some(proj_dirs.data_dir().to_path_buf()), None => return Err(anyhow!("Unable to determine data directory")), } } unsafe { libc::umask(0o077); } let data_path = args.options.dir.clone().unwrap(); let mut db_path = data_path.clone(); db_path.push("keep-1.db"); debug!("MAIN: Data directory: {:?}", data_path); debug!("MAIN: DB file: {:?}", db_path); fs::create_dir_all(data_path.clone()).context("Problem creating data directory")?; debug!("MAIN: Data directory created or already exists"); let mut conn = db::open(db_path.clone()).context("Problem opening database")?; debug!("MAIN: DB opened successfully"); match mode { KeepModes::Save => { crate::modes::save::mode_save(&mut cmd, &args, ids, tags, &mut conn, data_path)? } KeepModes::Get => { crate::modes::get::mode_get(&mut cmd, &args, ids, tags, &mut conn, data_path)? } KeepModes::Diff => { crate::modes::diff::mode_diff(&mut cmd, &args, ids, tags, &mut conn, data_path)? } KeepModes::List => { crate::modes::list::mode_list(&mut cmd, &args, ids, tags, &mut conn, data_path)? } KeepModes::Update => { crate::modes::update::mode_update(&mut cmd, &args, ids, tags, &mut conn, data_path)? } KeepModes::Info => { crate::modes::info::mode_info(&mut cmd, &args, ids, tags, &mut conn, data_path)? } KeepModes::Delete => { crate::modes::delete::mode_delete(&mut cmd, &args, ids, tags, &mut conn, data_path)? } KeepModes::Status => { crate::modes::status::mode_status(&mut cmd, &args, data_path, db_path)? } _ => todo!(), } Ok(()) }