use std::path::PathBuf; use std::str::FromStr; use clap::*; use clap_complete::Shell; /// Main struct for command-line arguments, parsed via Clap. #[derive(Parser, Debug, Clone)] #[command(author, version, about, long_about = None)] pub struct Args { #[command(flatten)] pub mode: ModeArgs, #[command(flatten)] pub item: ItemArgs, #[command(flatten)] pub options: OptionsArgs, #[arg(help("A list of either item IDs or tags"))] #[arg(value_parser = clap::value_parser!(NumberOrString))] #[arg(required = false)] pub ids_or_tags: Vec, } /// Struct for mode-specific arguments, defining CLI flags for different operations. #[derive(Parser, Debug, Clone)] pub struct ModeArgs { #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["get", "diff", "list", "delete", "info", "update", "status"]))] #[arg(help("Save an item using any tags or metadata provided"))] pub save: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "diff", "list", "delete", "info", "update", "status"]))] #[arg(help( "Get an item either by it's ID or by a combination of matching tags and metatdata" ))] pub get: bool, #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "list", "delete", "info", "update", "status"]))] #[arg(help("Show a diff between two items by ID"))] pub diff: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "delete", "info", "update", "status"]))] #[arg(help("List items, filtering on tags or metadata if given"))] pub list: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "info", "update", "status"]))] #[arg(help("Delete items either by ID or by matching tags"))] #[arg(requires = "ids_or_tags")] pub delete: bool, #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "delete", "update", "status"]))] #[arg(help( "Get an item either by it's ID or by a combination of matching tags and metatdata" ))] pub info: bool, #[arg(group("mode"), help_heading("Mode Options"), short('u'), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "status"]))] #[arg(help("Update an item's tags and metadata by ID"))] pub update: bool, #[arg(group("mode"), help_heading("Mode Options"), short('S'), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "server", "status_plugins"]))] #[arg(help("Show status of directories and supported compression algorithms"))] pub status: bool, #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server"]))] #[arg(help("Show available plugins and their configurations"))] pub status_plugins: bool, #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status"]))] #[arg(help("Start REST HTTP server"))] pub server: bool, #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server"]))] #[arg(help("Generate default configuration and output to stdout"))] pub generate_config: bool, #[arg(help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server", "generate_config"]))] #[arg(help("Generate shell completion script (bash, zsh, fish, elvish, powershell)"))] pub generate_completion: Option, #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_ADDRESS"))] #[arg(help("Server address to bind to"))] pub server_address: Option, #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_PORT"))] #[arg(help("Server port to bind to"))] pub server_port: Option, #[cfg(feature = "tls")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_CERT"))] #[arg(help("Path to TLS certificate file (PEM) for HTTPS"))] pub server_cert: Option, #[cfg(feature = "tls")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_KEY"))] #[arg(help("Path to TLS private key file (PEM) for HTTPS"))] pub server_key: Option, } /// Represents a meta plugin argument with optional JSON config. /// /// Parsed from `name` or `name:{"options":{...},"outputs":{...}}` syntax. #[derive(Debug, Clone)] pub struct MetaPluginArg { pub name: String, pub options: Option, } impl FromStr for MetaPluginArg { type Err = anyhow::Error; fn from_str(s: &str) -> Result { if let Some((name, json_str)) = s.split_once(':') { let value: serde_json::Value = serde_json::from_str(json_str) .map_err(|e| anyhow::anyhow!("Invalid JSON for meta plugin '{}': {}", name, e))?; Ok(MetaPluginArg { name: name.to_string(), options: Some(value), }) } else { Ok(MetaPluginArg { name: s.to_string(), options: None, }) } } } /// Represents a metadata key-value argument. /// /// Parsed from `key=value` (set) or `key` (delete/filter by existence). #[derive(Debug, Clone)] pub enum MetaArg { /// Set metadata with a value. Set { key: String, value: String }, /// Bare key without a value (delete in update mode, filter by existence otherwise). Key(String), } impl MetaArg { /// Returns the key. pub fn key(&self) -> &str { match self { MetaArg::Set { key, .. } | MetaArg::Key(key) => key, } } /// Returns the value if this is a Set variant. pub fn value(&self) -> Option<&str> { match self { MetaArg::Set { value, .. } => Some(value), MetaArg::Key(_) => None, } } } impl FromStr for MetaArg { type Err = anyhow::Error; fn from_str(s: &str) -> Result { if let Some((key, value)) = s.split_once('=') { Ok(MetaArg::Set { key: key.to_string(), value: value.to_string(), }) } else { Ok(MetaArg::Key(s.to_string())) } } } /// Struct for item-specific arguments, such as compression and plugins. #[derive(Parser, Debug, Clone)] pub struct ItemArgs { #[arg(help_heading("Item Options"), short, long, env("KEEP_COMPRESSION"))] #[arg(help("Compression algorithm to use when saving items"))] pub compression: Option, #[arg( help_heading("Item Options"), short('M'), long = "meta-plugin", value_parser = clap::value_parser!(MetaPluginArg), env("KEEP_META_PLUGINS") )] #[arg(help("Meta plugin to use (repeatable): name or name:{json}"))] pub meta_plugins: Vec, #[arg(help_heading("Item Options"), long)] #[arg(help("Metadata key=value to set (or key to delete in --update)"))] pub meta: Vec, #[arg(help_heading("Item Options"), long, env("KEEP_FILTERS"))] #[arg(help("Filter string to apply to content when getting items"))] pub filters: Option, } /// Struct for general options, including verbosity, paths, and output settings. #[derive(Parser, Debug, Default, Clone)] pub struct OptionsArgs { #[arg(long, env("KEEP_CONFIG"))] #[arg(help("Specify the configuration file to use"))] pub config: Option, #[arg(long, env("KEEP_DIR"))] #[arg(help("Specify the directory to use for storage"))] pub 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"))] pub list_format: String, #[arg(short('H'), long)] #[arg(help("Display file sizes with units"))] pub human_readable: bool, #[arg(short, long, action = clap::ArgAction::Count, conflicts_with("quiet"))] #[arg(help("Increase message verbosity, can be given more than once"))] pub verbose: u8, #[arg(short, long)] #[arg(help("Do not show any messages"))] pub quiet: bool, #[arg(long, value_enum, default_value("table"))] #[arg(help("Output format (only works with --info, --status, --list)"))] pub output_format: Option, #[arg(long, env("KEEP_SERVER_PASSWORD"))] #[arg(help("Password for server authentication (requires --server)"))] pub server_password: Option, #[arg(long, env("KEEP_SERVER_PASSWORD_HASH"))] #[arg(help("Password hash for server authentication (requires --server)"))] pub server_password_hash: Option, #[arg(long, env("KEEP_SERVER_USERNAME"))] #[arg(help( "Username for server Basic authentication (requires --server, defaults to 'keep')" ))] pub server_username: Option, #[arg(long, env("KEEP_SERVER_JWT_SECRET"))] #[arg(help("JWT secret for token-based authentication (requires --server)"))] pub server_jwt_secret: Option, #[arg(long, env("KEEP_SERVER_JWT_SECRET_FILE"))] #[arg(help("Path to file containing JWT secret (requires --server)"))] pub server_jwt_secret_file: Option, #[arg(long, env("KEEP_SERVER_MAX_BODY_SIZE"))] #[arg(help("Maximum request body size in bytes (requires --server, default: unlimited)"))] pub server_max_body_size: Option, #[cfg(feature = "client")] #[arg(long, env("KEEP_CLIENT_URL"), help_heading("Client Options"))] #[arg(help("Remote keep server URL for client mode"))] pub client_url: Option, #[cfg(feature = "client")] #[arg(long, env("KEEP_CLIENT_PASSWORD"), help_heading("Client Options"))] #[arg(help("Password for remote keep server authentication"))] pub client_password: Option, #[cfg(feature = "client")] #[arg(long, env("KEEP_CLIENT_USERNAME"), help_heading("Client Options"))] #[arg(help("Username for remote keep server authentication (defaults to 'keep')"))] pub client_username: Option, #[cfg(feature = "client")] #[arg(long, env("KEEP_CLIENT_JWT"), help_heading("Client Options"))] #[arg(help("JWT token for remote keep server authentication"))] pub client_jwt: Option, #[arg( long, help("Force output even when binary data would be sent to a TTY") )] pub force: bool, } /// Enum for representing either a number (item ID) or a string (tag). #[derive(Debug, Clone)] pub 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()))) } } /// Validates the parsed arguments based on mode constraints. /// /// # Returns /// /// `Result<(), String>` - Ok if valid, or an error message string. impl Args { /// Validate the arguments based on the selected mode pub fn validate(&self) -> Result<(), String> { // Check if --delete is used and ids_or_tags is empty if self.mode.delete && self.ids_or_tags.is_empty() { return Err("At least one ID is required when using --delete".to_string()); } // Check if --delete is used and any of the ids_or_tags are tags (strings) if self.mode.delete { for item in &self.ids_or_tags { if let NumberOrString::Str(_) = item { return Err("Tags are not supported for --delete, only IDs".to_string()); } } } Ok(()) } }