303 lines
9.1 KiB
Rust
303 lines
9.1 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;
|
|
pub mod db;
|
|
//pub mod item;
|
|
|
|
use compression::CompressionType;
|
|
|
|
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"), 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(())
|
|
}
|