Most of basic functionality implemented
This commit is contained in:
584
src/main.rs
584
src/main.rs
@@ -1,12 +1,69 @@
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::fs;
|
||||
use std::str::FromStr;
|
||||
use std::path::PathBuf;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::{Context, Result, Error, anyhow};
|
||||
use rusqlite::Connection;
|
||||
use gethostname::gethostname;
|
||||
use strum::IntoEnumIterator;
|
||||
use clap::error::ErrorKind;
|
||||
use clap::*;
|
||||
use log::*;
|
||||
|
||||
extern crate directories;
|
||||
use directories::ProjectDirs;
|
||||
|
||||
extern crate prettytable;
|
||||
use prettytable::{Table, Row, Cell, Attr};
|
||||
use prettytable::format;
|
||||
use prettytable::format::{TableFormat, Alignment};
|
||||
use prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR;
|
||||
use prettytable::row;
|
||||
use prettytable::color;
|
||||
|
||||
use chrono::prelude::*;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
use crate::compression::CompressionType;
|
||||
pub mod compression;
|
||||
pub mod db;
|
||||
|
||||
use humansize::{format_size, BINARY};
|
||||
|
||||
use is_terminal::IsTerminal;
|
||||
|
||||
extern crate term;
|
||||
|
||||
const BUFSIZ: usize = 8192;
|
||||
|
||||
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)]
|
||||
struct Args {
|
||||
@@ -17,60 +74,66 @@ struct Args {
|
||||
#[command(flatten)]
|
||||
options: OptionsArgs,
|
||||
|
||||
#[arg()]
|
||||
#[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"), short, long, conflicts_with_all(["get", "list", "update", "delete", "status"]))]
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["get", "list", "update", "delete", "status"]))]
|
||||
#[arg(help("Save an item using any tags or metadata provided"))]
|
||||
save: bool,
|
||||
|
||||
#[arg(group("mode"), help_heading("Mode"), short, long, conflicts_with_all(["save", "list", "update", "delete", "status"]))]
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "list", "update", "delete", "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"), short, long, conflicts_with_all(["save", "get", "update", "delete", "status"]))]
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "update", "delete", "status"]))]
|
||||
#[arg(help("List items, filtering on tags or metadata if given"))]
|
||||
list: bool,
|
||||
|
||||
#[arg(group("mode"), help_heading("Mode"), short, long, conflicts_with_all(["save", "get", "list", "delete", "status"]), requires("ids_or_tags"))]
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "list", "delete", "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"), short, long, conflicts_with_all(["save", "get", "list", "update", "status"]), requires("ids_or_tags"))]
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "list", "update", "status"]), requires("ids_or_tags"))]
|
||||
#[arg(help("Delete items either by ID or by matching tags"))]
|
||||
delete: bool,
|
||||
|
||||
#[arg(group("mode"), help_heading("Mode"), short('S'), long, conflicts_with_all(["save", "get", "list", "update", "delete"]))]
|
||||
#[arg(group("mode"), help_heading("Mode Options"), short('S'), long, conflicts_with_all(["save", "get", "list", "update", "delete"]))]
|
||||
#[arg(help("Show status of directories and supported compression algorithms"))]
|
||||
status: bool
|
||||
}
|
||||
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct ItemArgs {
|
||||
#[arg(help_heading("Item"), short, long, conflicts_with("get"), conflicts_with("list"))]
|
||||
comment: Option<String>,
|
||||
#[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: Option<Vec<KeyValue>>,
|
||||
|
||||
#[arg(help_heading("Item"), short('C'), long, conflicts_with("get"), conflicts_with("list"), env("KEEP_COMPRESS"))]
|
||||
compress: Option<String>,
|
||||
#[arg(help_heading("Item Options"), short('C'), long, conflicts_with("get"), conflicts_with("list"), env("KEEP_COMPRESSION"), )]
|
||||
#[arg(help("Compression algorithm to use when saving items"))]
|
||||
compression: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct OptionsArgs {
|
||||
#[arg(help_heading("Options"), long, env("KEEP_DIR"))]
|
||||
#[arg(long, env("KEEP_DIR"))]
|
||||
#[arg(help("Specify the directory to use for storage"))]
|
||||
dir: Option<PathBuf>,
|
||||
|
||||
#[arg(help_heading("Options"), short, long)]
|
||||
force: bool,
|
||||
|
||||
#[arg(help_heading("Options"), short, long, action = clap::ArgAction::Count, conflicts_with("quiet"))]
|
||||
#[arg(short, long, action = clap::ArgAction::Count, conflicts_with("quiet"))]
|
||||
#[arg(help("Increase message verbosity, can be given more than once"))]
|
||||
verbose: u8,
|
||||
|
||||
#[arg(help_heading("Options"), short, long)]
|
||||
#[arg(short, long)]
|
||||
#[arg(help("Do show any messages"))]
|
||||
quiet: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
enum NumberOrString {
|
||||
Number(u32),
|
||||
Str(String),
|
||||
}
|
||||
|
||||
#[derive(Debug,PartialEq)]
|
||||
enum KeepModes {
|
||||
@@ -83,35 +146,60 @@ enum KeepModes {
|
||||
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 = &'static str; // The actual type doesn't matter since we never error, but it must implement `Display`
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err>
|
||||
{
|
||||
Ok (s.parse::<u32>()
|
||||
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> {
|
||||
let proj_dirs = ProjectDirs::from("gt0.ca", "Andrew Phillips", "Keep");
|
||||
|
||||
fn main() {
|
||||
let mut cmd = Args::command();
|
||||
let args = Args::parse();
|
||||
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)
|
||||
//.timestamp(stderrlog::Timestamp::Second)
|
||||
.init()
|
||||
.unwrap();
|
||||
|
||||
debug!("Start");
|
||||
|
||||
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),
|
||||
@@ -119,7 +207,11 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
tags.sort();
|
||||
tags.dedup();
|
||||
|
||||
let mut mode: KeepModes = KeepModes::Unknown;
|
||||
|
||||
if args.mode.save {
|
||||
mode = KeepModes::Save;
|
||||
} else if args.mode.get {
|
||||
@@ -133,7 +225,7 @@ fn main() {
|
||||
} else if args.mode.status {
|
||||
mode = KeepModes::Status;
|
||||
}
|
||||
|
||||
|
||||
if mode == KeepModes::Unknown {
|
||||
if ! ids.is_empty() {
|
||||
mode = KeepModes::Get;
|
||||
@@ -142,24 +234,53 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
debug!("args: {:?}", args);
|
||||
debug!("ids: {:?}", ids);
|
||||
debug!("tags: {:?}", tags);
|
||||
debug!("mode: {:?}", mode);
|
||||
|
||||
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 => mode_save(&mut cmd, args, ids, tags),
|
||||
KeepModes::Get => mode_get(&mut cmd, args, ids, tags),
|
||||
KeepModes::List => mode_list(&mut cmd, args, ids, tags),
|
||||
KeepModes::Update => mode_update(&mut cmd, args, ids, tags),
|
||||
KeepModes::Delete => mode_delete(&mut cmd, args, ids, tags),
|
||||
KeepModes::Status => mode_status(&mut cmd, args),
|
||||
KeepModes::Save => mode_save(&mut cmd, args, ids, tags, conn, data_path)?,
|
||||
KeepModes::Get => mode_get(&mut cmd, args, ids, tags, &mut conn, data_path)?,
|
||||
KeepModes::List => mode_list(&mut cmd, args, ids, tags, &mut conn, data_path)?,
|
||||
KeepModes::Update => mode_update(&mut cmd, args, ids, tags, &mut conn)?,
|
||||
KeepModes::Delete => mode_delete(&mut cmd, args, ids, tags, &mut conn, data_path)?,
|
||||
KeepModes::Status => mode_status(&mut cmd, args, data_path, db_path)?,
|
||||
_ => todo!()
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn mode_save(cmd: &mut Command, args: Args, ids: &mut Vec<u32>, tags: &mut Vec<String>) {
|
||||
|
||||
fn mode_save(cmd: &mut Command, args: Args, ids: &mut Vec<i64>, tags: &mut Vec<String>, conn: Connection, data_path: PathBuf) -> Result<()> {
|
||||
if ! ids.is_empty() {
|
||||
cmd.error(ErrorKind::InvalidValue, "ID given, you cannot supply IDs when using --save").exit();
|
||||
}
|
||||
@@ -168,37 +289,294 @@ fn mode_save(cmd: &mut Command, args: Args, ids: &mut Vec<u32>, tags: &mut Vec<S
|
||||
tags.push("none".to_string());
|
||||
}
|
||||
|
||||
let compression_type = compression::get_compression(args.item.compress);
|
||||
let compression_name = args.item.compression.unwrap_or(compression::default_type().to_string());
|
||||
|
||||
let compression_type_opt = CompressionType::from_str(&compression_name);
|
||||
if compression_type_opt.is_err() {
|
||||
cmd.error(ErrorKind::InvalidValue, format!("Unknown compression type: {}", compression_name)).exit();
|
||||
}
|
||||
|
||||
let compression_type = compression_type_opt.unwrap();
|
||||
debug!("MAIN: Compression type: {}", compression_type);
|
||||
|
||||
let mut item = db::Item {
|
||||
id: None,
|
||||
ts: Utc::now(),
|
||||
size: None,
|
||||
compression: compression_type.to_string()
|
||||
};
|
||||
|
||||
let id = db::insert_item(&conn, item.clone())?;
|
||||
item.id = Some(id);
|
||||
debug!("MAIN: Added item {:?}", item.clone());
|
||||
|
||||
if ! args.options.quiet {
|
||||
if std::io::stderr().is_terminal() {
|
||||
let mut t = term::stderr().unwrap();
|
||||
t.reset().unwrap_or(());
|
||||
t.attr(term::Attr::Bold).unwrap_or(());
|
||||
write!(t, "KEEP:").unwrap_or(());
|
||||
t.reset().unwrap_or(());
|
||||
write!(t, " New item ").unwrap_or(());
|
||||
t.attr(term::Attr::Bold).unwrap_or(());
|
||||
write!(t, "{id}")?;
|
||||
t.reset().unwrap_or(());
|
||||
write!(t, " tags: ")?;
|
||||
t.attr(term::Attr::Bold).unwrap_or(());
|
||||
write!(t, "{}", tags.join(" "))?;
|
||||
t.reset().unwrap_or(());
|
||||
writeln!(t, "")?;
|
||||
std::io::stderr().flush()?;
|
||||
} else {
|
||||
let mut t = std::io::stderr();
|
||||
writeln!(t, "KEEP: New item: {} tags: {:?}", id, tags)?;
|
||||
}
|
||||
}
|
||||
|
||||
db::set_item_tags(&conn, item.clone(), tags)?;
|
||||
|
||||
let mut item_meta: HashMap<String, String> = HashMap::new();
|
||||
|
||||
if let Ok(hostname) = gethostname().into_string() {
|
||||
item_meta.insert("hostname".to_string(), hostname);
|
||||
}
|
||||
|
||||
if args.item.meta.is_some() {
|
||||
for item in args.item.meta.unwrap().iter() {
|
||||
let item = item.clone();
|
||||
item_meta.insert(item.key, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
for kv in item_meta.iter() {
|
||||
let meta = db::Meta {
|
||||
id: item.id.unwrap(),
|
||||
name: kv.0.to_string(),
|
||||
value: kv.1.to_string()
|
||||
};
|
||||
db::store_meta(&conn, meta)?;
|
||||
}
|
||||
|
||||
let mut item_path = data_path.clone();
|
||||
item_path.push(id.to_string());
|
||||
|
||||
let mut stdin = io::stdin().lock();
|
||||
let mut stdout = io::stdout().lock();
|
||||
let mut buffer = [0; BUFSIZ];
|
||||
|
||||
let compression_engine = compression::get_engine(compression_type.clone()).expect("Unable to get compression engine");
|
||||
let mut item_out: Box<dyn Write> = compression_engine.create(item_path.clone())
|
||||
.context(anyhow!("Unable to write file {:?} using compression {:?}", item_path, compression_type))?;
|
||||
|
||||
debug!("MAIN: Starting IO loop");
|
||||
loop {
|
||||
let n = stdin.read(&mut buffer[..BUFSIZ])?;
|
||||
|
||||
if n == 0 {
|
||||
debug!("MAIN: EOF on STDIN");
|
||||
break;
|
||||
}
|
||||
|
||||
stdout.write_all(&buffer[..n])?;
|
||||
item_out.write_all(&buffer[..n])?;
|
||||
item.size = match item.size {
|
||||
None => Some(n as i64),
|
||||
Some(prev_n) => Some(prev_n + n as i64)
|
||||
};
|
||||
}
|
||||
debug!("MAIN: Ending IO loop");
|
||||
|
||||
stdout.flush()?;
|
||||
item_out.flush()?;
|
||||
|
||||
db::update_item(&conn, item.clone())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn mode_get(cmd: &mut Command, args: Args, ids: &mut Vec<u32>, tags: &mut Vec<String>) {
|
||||
fn mode_get(cmd: &mut Command, args: Args, ids: &mut Vec<i64>, tags: &mut Vec<String>, conn: &mut Connection, data_path: PathBuf) -> Result<()> {
|
||||
if ids.is_empty() && tags.is_empty() {
|
||||
cmd.error(ErrorKind::InvalidValue, "No ID or tags given, ou must supply one ID or atleast one tag when using --get").exit();
|
||||
cmd.error(ErrorKind::InvalidValue, "No ID or tags given, you must supply one ID or atleast one tag when using --get").exit();
|
||||
} else if ! ids.is_empty() && ! tags.is_empty() {
|
||||
cmd.error(ErrorKind::InvalidValue, "Both ID and tags given, you must supply one ID or atleast one tag when using --get").exit();
|
||||
} else if ids.len() > 1 {
|
||||
cmd.error(ErrorKind::InvalidValue, "More than one ID given, you must supply one ID or atleast one tag when using --get").exit();
|
||||
}
|
||||
|
||||
let mut meta: HashMap<String, String> = HashMap::new();
|
||||
if args.item.meta.is_some() {
|
||||
for item in args.item.meta.unwrap().iter() {
|
||||
let item = item.clone();
|
||||
meta.insert(item.key, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
let item_maybe = match tags.is_empty() && meta.is_empty() {
|
||||
true => match ids.iter().next() {
|
||||
Some(item_id) => db::get_item(conn, *item_id)?,
|
||||
None => None
|
||||
},
|
||||
false => db::get_item_matching(conn, tags, &meta)?
|
||||
};
|
||||
|
||||
if let Some(item) = item_maybe {
|
||||
debug!("MAIN: Found item {:?}", item);
|
||||
|
||||
let mut item_path = data_path.clone();
|
||||
item_path.push(item.id.unwrap().to_string());
|
||||
|
||||
let compression_type = CompressionType::from_str(&item.compression)?;
|
||||
debug!("MAIN: Item has compression type {:?}", compression_type.clone());
|
||||
|
||||
let compression_engine = compression::get_engine(compression_type).expect("Unable to get compression engine");
|
||||
compression_engine.cat(item_path.clone())
|
||||
} else {
|
||||
Err(anyhow!("Unable to find matching item in database"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn mode_list(cmd: &mut Command, args: Args, ids: &mut Vec<u32>, tags: &mut Vec<String>) {
|
||||
fn mode_list(cmd: &mut Command, args: Args, ids: &mut Vec<i64>, tags: &Vec<String>, conn: &mut Connection, data_path: PathBuf) -> Result<()> {
|
||||
if ! ids.is_empty() {
|
||||
cmd.error(ErrorKind::InvalidValue, "ID given, you can only supply tags when using --list").exit();
|
||||
}
|
||||
|
||||
let mut meta: HashMap<String, String> = HashMap::new();
|
||||
if args.item.meta.is_some() {
|
||||
for item in args.item.meta.unwrap().iter() {
|
||||
let item = item.clone();
|
||||
meta.insert(item.key, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let items = match tags.is_empty() && meta.is_empty() {
|
||||
true => db::get_items(conn)?,
|
||||
false => db::get_items_matching(conn, tags, &meta)?
|
||||
};
|
||||
|
||||
debug!("MAIN: Items: {:?}", items);
|
||||
|
||||
let mut tags_by_item: HashMap<i64, Vec<String>> = HashMap::new();
|
||||
let mut meta_by_item: HashMap<i64, HashMap<String, String>> = HashMap::new();
|
||||
let mut meta_columns = HashSet::new();
|
||||
|
||||
for item in items.iter() {
|
||||
let item_id = item.id.unwrap();
|
||||
|
||||
let item_tags: Vec<String> = db::get_item_tags(conn, item)?
|
||||
.into_iter()
|
||||
.map(|x| {x.name})
|
||||
.collect();
|
||||
tags_by_item.insert(item_id, item_tags);
|
||||
|
||||
let mut item_meta: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for meta in db::get_item_meta(conn, item)? {
|
||||
meta_columns.insert(meta.name.clone());
|
||||
item_meta.insert(meta.name.clone(), meta.value);
|
||||
}
|
||||
meta_by_item.insert(item_id, item_meta);
|
||||
};
|
||||
|
||||
let mut meta_columns_sorted = Vec::from_iter(meta_columns);
|
||||
meta_columns_sorted.sort();
|
||||
|
||||
let mut table = Table::new();
|
||||
if std::io::stdout().is_terminal() {
|
||||
table.set_format(*FORMAT_BOX_CHARS_NO_BORDER_LINE_SEPARATOR);
|
||||
} else {
|
||||
table.set_format(*FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
}
|
||||
|
||||
let mut title_row = row!(
|
||||
b->"ID",
|
||||
b->"Time",
|
||||
b->"Stream Size",
|
||||
b->"Comp",
|
||||
b->"File Size",
|
||||
b->"Tags",
|
||||
);
|
||||
|
||||
for name in &meta_columns_sorted {
|
||||
title_row.add_cell(Cell::new(name).with_style(Attr::Bold));
|
||||
}
|
||||
|
||||
table.set_titles(title_row);
|
||||
|
||||
for item in items {
|
||||
let item_id = item.id.unwrap();
|
||||
let mut item_path = data_path.clone();
|
||||
item_path.push(item.id.unwrap().to_string());
|
||||
|
||||
let id_cell = Cell::new_align(&item.id.unwrap_or(0).to_string(), Alignment::RIGHT);
|
||||
let ts_cell = Cell::new(&item.ts.with_timezone(&Local).format("%F %T").to_string());
|
||||
let size_cell = match item.size {
|
||||
Some(size) => Cell::new_align(format_size(size as u64, BINARY).as_str(), Alignment::RIGHT),
|
||||
None => Cell::new_align("Missing", Alignment::RIGHT).with_style(Attr::ForegroundColor(color::RED)).with_style(Attr::Bold)
|
||||
};
|
||||
let compression_cell = Cell::new(&item.compression);
|
||||
let file_size_cell = match item_path.metadata() {
|
||||
Ok(metadata) => Cell::new_align(format_size(metadata.len(), BINARY).as_str(), Alignment::RIGHT),
|
||||
Err(_) => Cell::new_align("Missing", Alignment::RIGHT).with_style(Attr::ForegroundColor(color::RED)).with_style(Attr::Bold)
|
||||
};
|
||||
|
||||
let item_tags = tags_by_item.get(&item_id).unwrap();
|
||||
let tags_cell = Cell::new(&item_tags.join(" "));
|
||||
|
||||
let mut table_row = Row::new(vec![id_cell,ts_cell,size_cell, compression_cell, file_size_cell, tags_cell]);
|
||||
|
||||
let item_meta = meta_by_item.get(&item_id).unwrap();
|
||||
for name in &meta_columns_sorted {
|
||||
match item_meta.get(name) {
|
||||
Some(value) => table_row.add_cell(Cell::new(value)),
|
||||
None => table_row.add_cell(Cell::new(""))
|
||||
};
|
||||
}
|
||||
|
||||
table.add_row(table_row);
|
||||
}
|
||||
|
||||
table.printstd();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn mode_update(cmd: &mut Command, args: Args, ids: &mut Vec<u32>, tags: &mut Vec<String>) {
|
||||
fn mode_update(cmd: &mut Command, args: Args, ids: &mut Vec<i64>, tags: &mut Vec<String>, conn: &mut Connection) -> Result<()> {
|
||||
if ids.is_empty() {
|
||||
cmd.error(ErrorKind::InvalidValue, "No ID given, you must supply one ID when using --update").exit();
|
||||
}
|
||||
|
||||
let item_id = ids.iter().next().expect("Unable to determine item id");
|
||||
let item_maybe = db::get_item(conn, *item_id)?;
|
||||
|
||||
let item = item_maybe.expect("Unable to find item in database");
|
||||
debug!("MAIN: Found item {:?}", item);
|
||||
|
||||
if ! tags.is_empty() {
|
||||
debug!("MAIN: Updating item tags");
|
||||
db::set_item_tags(conn, item.clone(), tags)?;
|
||||
}
|
||||
|
||||
if args.item.meta.is_some() {
|
||||
debug!("MAIN: Updating item meta");
|
||||
for kv in args.item.meta.unwrap().iter() {
|
||||
let meta = db::Meta {
|
||||
id: item.id.unwrap(),
|
||||
name: kv.key.to_string(),
|
||||
value: kv.value.to_string()
|
||||
};
|
||||
db::store_meta(conn, meta)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn mode_delete(cmd: &mut Command, args: Args, ids: &mut Vec<u32>, tags: &mut Vec<String>) {
|
||||
fn mode_delete(cmd: &mut Command, _args: Args, ids: &mut Vec<i64>, tags: &mut Vec<String>, conn: &mut Connection, data_path: PathBuf) -> Result<()> {
|
||||
if ids.is_empty() {
|
||||
cmd.error(ErrorKind::InvalidValue, "No ID given, you must supply one ID when using --delete").exit();
|
||||
} else if ! tags.is_empty() {
|
||||
@@ -206,19 +584,105 @@ fn mode_delete(cmd: &mut Command, args: Args, ids: &mut Vec<u32>, tags: &mut Vec
|
||||
} else if ids.len() > 1 {
|
||||
cmd.error(ErrorKind::InvalidValue, "More than one ID given, you must supply one ID when using --delete").exit();
|
||||
}
|
||||
|
||||
let item_id = ids.iter().next().expect("Unable to determine item id");
|
||||
let item_maybe = db::get_item(conn, *item_id)?;
|
||||
|
||||
let item = item_maybe.expect("Unable to find item in database");
|
||||
debug!("MAIN: Found item {:?}", item);
|
||||
|
||||
db::delete_item(conn, item)?;
|
||||
|
||||
let mut item_path = data_path.clone();
|
||||
item_path.push(item_id.to_string());
|
||||
|
||||
fs::remove_file(&item_path).context(anyhow!("Unable to remove item file {:?}", item_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn mode_status_show_compression() {
|
||||
let compression_types = compression::supported_compression_types();
|
||||
|
||||
println!("compression_types:");
|
||||
for compression_type in compression_types.into_iter() {
|
||||
println!(" {}", compression_type);
|
||||
fn mode_status(_cmd: &mut Command, args: Args, data_path: PathBuf, db_path: PathBuf) -> Result<()> {
|
||||
let mut path_table = Table::new();
|
||||
|
||||
if std::io::stdout().is_terminal() {
|
||||
path_table.set_format(*FORMAT_BOX_CHARS_NO_BORDER_LINE_SEPARATOR);
|
||||
} else {
|
||||
path_table.set_format(*FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
}
|
||||
}
|
||||
|
||||
path_table.set_titles(Row::new(vec![
|
||||
Cell::new("Type").with_style(Attr::Bold),
|
||||
Cell::new("Path").with_style(Attr::Bold),
|
||||
]));
|
||||
|
||||
path_table.add_row(Row::new(vec![
|
||||
Cell::new("Data"),
|
||||
Cell::new(&data_path.into_os_string().into_string().expect("Unable to convert data path to string"))
|
||||
]));
|
||||
|
||||
path_table.add_row(Row::new(vec![
|
||||
Cell::new("Database"),
|
||||
Cell::new(&db_path.into_os_string().into_string().expect("Unable to convert DB path to string"))
|
||||
]));
|
||||
|
||||
|
||||
fn mode_status(cmd: &mut Command, args: Args) {
|
||||
mode_status_show_compression();
|
||||
let mut compression_table = Table::new();
|
||||
if std::io::stdout().is_terminal() {
|
||||
compression_table.set_format(*FORMAT_BOX_CHARS_NO_BORDER_LINE_SEPARATOR);
|
||||
} else {
|
||||
compression_table.set_format(*FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
}
|
||||
|
||||
compression_table.set_titles(row!(
|
||||
b->"Type",
|
||||
b->"Found",
|
||||
b->"Default",
|
||||
b->"Binary",
|
||||
b->"Compress",
|
||||
b->"Decompress"));
|
||||
|
||||
|
||||
let default_type = match args.item.compression {
|
||||
Some(compression_name) => CompressionType::from_str(&compression_name)
|
||||
.context(anyhow!("Invalid compression type {}", compression_name))?,
|
||||
None => compression::default_type()
|
||||
};
|
||||
|
||||
for compression_type in CompressionType::iter() {
|
||||
let compression_program = match compression_type {
|
||||
CompressionType::None => compression::CompressionProgram {
|
||||
program: "".to_string(),
|
||||
compress: Vec::new(),
|
||||
decompress: Vec::new(),
|
||||
supported: true
|
||||
},
|
||||
_ => compression::get_program(compression_type.clone())?
|
||||
};
|
||||
|
||||
let is_default = compression_type == default_type;
|
||||
|
||||
compression_table.add_row(Row::new(vec![
|
||||
Cell::new(&compression_type.to_string()),
|
||||
match compression_program.supported {
|
||||
true => Cell::new("Yes").with_style(Attr::ForegroundColor(color::GREEN)),
|
||||
false => Cell::new("No").with_style(Attr::ForegroundColor(color::RED))
|
||||
},
|
||||
match is_default {
|
||||
true => Cell::new("Yes").with_style(Attr::ForegroundColor(color::GREEN)),
|
||||
false => Cell::new("No")
|
||||
},
|
||||
Cell::new(&compression_program.program),
|
||||
Cell::new(&compression_program.compress.join(" ")),
|
||||
Cell::new(&compression_program.decompress.join(" ")),
|
||||
]));
|
||||
}
|
||||
|
||||
println!("PATHS:");
|
||||
path_table.printstd();
|
||||
println!();
|
||||
println!("COMPRESSION:");
|
||||
compression_table.printstd();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user