Files
keep/src/main.rs
2025-05-12 22:11:56 -03:00

306 lines
9.3 KiB
Rust

use std::path::PathBuf;
use anyhow::{anyhow, Context, Error, Result};
use clap::*;
use log::*;
mod modes;
extern crate directories;
use directories::ProjectDirs;
extern crate prettytable;
use prettytable::format;
use prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR;
use prettytable::format::{Alignment, TableFormat};
use std::str::FromStr;
#[macro_use]
extern crate lazy_static;
pub mod compression_engine;
pub mod db;
pub mod digest_engine;
//pub mod item;
extern crate term;
lazy_static! {
static ref FORMAT_BOX_CHARS_NO_BORDER_LINE_SEPARATOR: TableFormat =
format::FormatBuilder::new()
.column_separator('│')
.borders('│')
.separators(
&[format::LinePosition::Top],
format::LineSeparator::new('─', '┬', '┌', '┐')
)
.separators(
&[format::LinePosition::Title],
format::LineSeparator::new('─', '┼', '├', '┤')
)
.separators(
&[format::LinePosition::Bottom],
format::LineSeparator::new('─', '┴', '└', '┘')
)
.padding(1, 1)
.build();
}
#[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<NumberOrString>,
}
#[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,
}
#[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<KeyValue>,
#[arg(help_heading("Item Options"), long, env("KEEP_DIGEST"))]
#[arg(help("Digest algorithm to use when saving items"))]
digest: Option<String>,
#[arg(help_heading("Item Options"), short, long, env("KEEP_COMPRESSION"))]
#[arg(help("Compression algorithm to use when saving items"))]
compression: Option<String>,
}
#[derive(Parser, Debug)]
struct OptionsArgs {
#[arg(long, env("KEEP_DIR"))]
#[arg(help("Specify the directory to use for storage"))]
dir: Option<PathBuf>,
#[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,
}
#[derive(Debug, PartialEq)]
enum KeepModes {
Unknown,
Save,
Get,
Diff,
List,
Update,
Delete,
Info,
Status,
}
#[derive(Debug, Clone)]
struct KeyValue {
key: String,
value: String,
}
impl FromStr for KeyValue {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
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")),
}
}
}
#[derive(Debug, Clone)]
enum NumberOrString {
Number(i64),
Str(String),
}
impl FromStr for NumberOrString {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.parse::<i64>()
.map(NumberOrString::Number)
.unwrap_or_else(|_| NumberOrString::Str(s.to_string())))
}
}
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;
}
}
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(())
}