Files
keep/src/main.rs
2025-05-06 13:29:52 -03:00

1018 lines
35 KiB
Rust

use std::io;
use std::io::{Read, Write, BufWriter};
use std::fs;
use std::str::FromStr;
use std::path::PathBuf;
use std::collections::HashMap;
use std::env;
use std::os::fd::FromRawFd;
use regex::Regex;
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;
pub mod compression;
pub mod db;
//pub mod item;
use compression::CompressionType;
use compression::program::CompressionEngineProgram;
use humansize::BINARY;
use is_terminal::IsTerminal;
extern crate term;
use nix::unistd;
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 {
#[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())))
}
}
#[derive(Debug, Eq, PartialEq, Clone, strum::EnumIter, strum::Display, strum::EnumString)]
#[strum(ascii_case_insensitive)]
pub enum ColumnType {
Id,
Time,
Size,
Compression,
FileSize,
FilePath,
Tags,
Meta
}
fn main() -> Result<(), Error> {
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 => mode_save(&mut cmd, args, ids, tags, conn, data_path)?,
KeepModes::Get => mode_get(&mut cmd, args, ids, tags, &mut conn, data_path)?,
KeepModes::Diff => mode_diff(&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, data_path)?,
KeepModes::Info => mode_info(&mut cmd, args, ids, tags, &mut conn, data_path)?,
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<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();
}
if tags.is_empty() {
tags.push("none".to_string());
}
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> = get_meta_from_env();
if let Ok(hostname) = gethostname().into_string() {
if ! item_meta.contains_key("hostname") {
item_meta.insert("hostname".to_string(), hostname);
}
}
for item in args.item.meta.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; libc::BUFSIZ as usize];
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[..libc::BUFSIZ as usize])?;
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<i64>, tags: &mut Vec<String>, conn: &mut Connection, data_path: PathBuf) -> Result<()> {
if ! ids.is_empty() && ! tags.is_empty() {
cmd.error(ErrorKind::InvalidValue, "Both ID and tags given, you must supply exactly 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 exactly one ID or atleast one tag when using --get").exit();
}
let mut meta: HashMap<String, String> = HashMap::new();
for item in args.item.meta.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 => db::get_item_last(conn)?
},
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())?;
Ok(())
} else {
Err(anyhow!("Unable to find matching item in database"))
}
}
fn mode_diff(cmd: &mut Command, args: Args, ids: &mut Vec<i64>, tags: &mut Vec<String>, conn: &mut Connection, data_path: PathBuf) -> Result<()> {
if ! tags.is_empty() {
cmd.error(ErrorKind::InvalidValue, "Tags given, you must supply exactly two IDs when using --diff").exit();
} else if ids.len() > 2 || ids.len() < 1 {
cmd.error(ErrorKind::InvalidValue, "You must supply exactly one or two IDs when using --diff").exit();
}
let item_a: Option<db::Item> = db::get_item(conn, ids[0])?;
let mut item_b: Option<db::Item> = None;
if ids.len() == 2 {
item_b = db::get_item(conn, ids[1])?;
}
if let Some(item_a) = item_a {
debug!("MAIN: Found item A {:?}", item_a);
let mut item_path_a = data_path.clone();
item_path_a.push(item_a.id.unwrap().to_string());
let compression_type_a = CompressionType::from_str(&item_a.compression)?;
debug!("MAIN: Item A has compression type {:?}", compression_type_a.clone());
let compression_engine_a = compression::get_engine(compression_type_a).expect("Unable to get compression engine");
if let Some(item_b) = item_b {
debug!("MAIN: Found item B {:?}", item_b);
let mut item_path_b = data_path.clone();
item_path_b.push(item_b.id.unwrap().to_string());
let compression_type_b = CompressionType::from_str(&item_b.compression)?;
debug!("MAIN: Item B has compression type {:?}", compression_type_b.clone());
let compression_engine_b = compression::get_engine(compression_type_b).expect("Unable to get compression engine");
let (fd_a_read, fd_a_write) = unistd::pipe().unwrap();
let (fd_b_read, fd_b_write) = unistd::pipe().unwrap();
let child = std::process::Command::new("diff")
.arg("-u")
.arg(format!("/dev/fd/{}", fd_a_read))
.arg(format!("/dev/fd/{}", fd_b_read))
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("Failed to execute diff command");
let stdout_a_raw = unsafe { std::fs::File::from_raw_fd(fd_a_write) };
let stdout_b_raw = unsafe { std::fs::File::from_raw_fd(fd_b_write) };
let mut stdout_a = BufWriter::new(stdout_a_raw);
let mut stdout_b = BufWriter::new(stdout_b_raw);
compression_engine_a.copy(item_path_a.clone(), &mut stdout_a)?;
compression_engine_b.copy(item_path_b.clone(), &mut stdout_b)?;
let output = child.wait_with_output().expect("Failed to wait on diff command");
if output.status.success() {
println!("{}", String::from_utf8_lossy(&output.stdout));
} else {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
Ok(())
} else {
Err(anyhow!("Unable to find second item in database"))
}
} else {
Err(anyhow!("Unable to find first item in database"))
}
}
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();
for item in args.item.meta.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();
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)? {
item_meta.insert(meta.name.clone(), meta.value);
}
meta_by_item.insert(item_id, item_meta);
};
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_CLEAN);
let list_format = args.options.list_format.split(",");
let mut title_row = row!();
for column in list_format.clone() {
let mut column_format = column.split(":").into_iter();
let column_name = column_format.next().expect("Unable to parse column name");
let column_type = ColumnType::from_str(column_name).expect(format!("Unknown column {:?}", column_name).as_str());
if column_type == ColumnType::Meta {
let meta_name = column_format.next().expect("Unable to parse metadata name for meta column");
title_row.add_cell(Cell::new(meta_name).with_style(Attr::Bold));
} else {
title_row.add_cell(Cell::new(&column_type.to_string()).with_style(Attr::Bold));
}
}
table.set_titles(title_row);
for item in items {
let item_id = item.id.unwrap();
let tags = tags_by_item.get(&item_id).unwrap();
let meta = meta_by_item.get(&item_id).unwrap();
let mut item_path = data_path.clone();
item_path.push(item.id.unwrap().to_string());
let mut table_row = Row::new(vec![]);
for column in list_format.clone() {
let mut column_format = column.split(":").into_iter();
let column_name = column_format.next().expect("Unable to parse column name");
let column_type = ColumnType::from_str(column_name).expect(format!("Unknown column {:?}", column_name).as_str());
let mut meta_name: Option<&str> = None;
if column_type == ColumnType::Meta {
meta_name = column_format.next();
}
let column_width: usize = match column_format.next() {
Some(len) => len.parse().unwrap_or(0),
None => 0
};
let cell = match column_type {
ColumnType::Id => Cell::new_align(
&string_column(item.id.unwrap_or(0).to_string(), column_width),
Alignment::RIGHT),
ColumnType::Time => Cell::new(
&string_column(item.ts.with_timezone(&Local).format("%F %T").to_string(), column_width)),
ColumnType::Size => match item.size {
Some(size) => Cell::new_align(
&size_column(size as u64, args.options.human_readable, column_width),
Alignment::RIGHT),
None => match item_path.metadata() {
Ok(_) => Cell::new_align("Unknown", Alignment::RIGHT)
.with_style(Attr::ForegroundColor(color::YELLOW))
.with_style(Attr::Bold),
Err(_) => Cell::new_align("Missing", Alignment::RIGHT)
.with_style(Attr::ForegroundColor(color::RED))
.with_style(Attr::Bold)
}
},
ColumnType::Compression => Cell::new(&string_column(item.compression.to_string(), column_width)),
ColumnType::FileSize => match item_path.metadata() {
Ok(metadata) => Cell::new_align(
&size_column(metadata.len() as u64, args.options.human_readable, column_width),
Alignment::RIGHT),
Err(_) => Cell::new_align("Missing", Alignment::RIGHT)
.with_style(Attr::ForegroundColor(color::RED)).with_style(Attr::Bold)
},
ColumnType::FilePath => Cell::new(&string_column(item_path.clone().into_os_string().into_string().unwrap(), column_width)),
ColumnType::Tags => Cell::new(&string_column(tags.join(" "), column_width)),
ColumnType::Meta => match meta_name {
Some(meta_name) => match meta.get(meta_name) {
Some(meta_value) => Cell::new(&string_column(meta_value.to_string(), column_width)),
None => Cell::new("")
},
None => Cell::new("")
}
};
table_row.add_cell(cell);
}
table.add_row(table_row);
}
table.printstd();
Ok(())
}
fn mode_update(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 exactly one ID when using --update").exit();
} else if ids.len() > 1 {
cmd.error(ErrorKind::InvalidValue, "More than one ID given, you must supply exactly one ID or atleast one tag 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 mut 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 item.size.is_none() {
info!("Updating unknown stream size");
let mut item_path = data_path.clone();
item_path.push(item.id.unwrap().to_string());
let item_file_metadata = item_path.metadata();
if item_file_metadata.is_ok() {
debug!("MAIN: Updating stream size of {:?}", item_path);
let compression_type = CompressionType::from_str(&item.compression)?;
let compression_engine = compression::get_engine(compression_type).expect("Unable to get compression engine");
let size = compression_engine.size(item_path)? as i64;
item.size = Some(size);
db::update_item(&conn, item.clone())?;
} else {
debug!("MAIN: Unable to update size of item due to missing file {:?}", item_path);
}
}
if args.item.meta.len() > 0 {
debug!("MAIN: Updating item meta");
for kv in args.item.meta.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_info(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, "Both ID and tags given, you must supply exactly one ID or atleast one tag when using --info").exit();
} else if ids.len() > 1 {
cmd.error(ErrorKind::InvalidValue, "More than one ID given, you must supply exactly one ID or atleast one tag when using --info").exit();
}
let mut meta: HashMap<String, String> = HashMap::new();
for item in args.item.meta.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 => db::get_item_last(conn)?
},
false => db::get_item_matching(conn, tags, &meta)?
};
if let Some(item) = item_maybe {
debug!("MAIN: Found item {:?}", item);
let item_id = item.id.unwrap();
let item_tags: Vec<String> = db::get_item_tags(conn, &item)?
.into_iter()
.map(|x| {x.name})
.collect();
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);
}
table.add_row(Row::new(vec![
Cell::new("ID").with_style(Attr::Bold),
Cell::new(&item_id.to_string())
]));
let ts_cell = Cell::new(&item.ts.with_timezone(&Local).format("%F %T %Z").to_string());
table.add_row(Row::new(vec![
Cell::new("Timestamp").with_style(Attr::Bold),
ts_cell
]));
let mut item_path = data_path.clone();
item_path.push(item.id.unwrap().to_string());
table.add_row(Row::new(vec![
Cell::new("Path").with_style(Attr::Bold),
Cell::new(item_path.to_str().expect("Unable to get item path"))
]));
let size_cell = match item.size {
Some(size) => Cell::new(format_size(size as u64, args.options.human_readable).as_str()),
None => Cell::new("Missing").with_style(Attr::ForegroundColor(color::RED)).with_style(Attr::Bold)
};
table.add_row(Row::new(vec![
Cell::new("Stream Size").with_style(Attr::Bold),
size_cell
]));
let compression_type = CompressionType::from_str(&item.compression)?;
table.add_row(Row::new(vec![
Cell::new("Compression").with_style(Attr::Bold),
Cell::new(&compression_type.to_string())
]));
let file_size_cell = match item_path.metadata() {
Ok(metadata) => Cell::new(format_size(metadata.len(), args.options.human_readable).as_str()),
Err(_) => Cell::new("Missing").with_style(Attr::ForegroundColor(color::RED)).with_style(Attr::Bold)
};
table.add_row(Row::new(vec![
Cell::new("File Size").with_style(Attr::Bold),
file_size_cell
]));
let compression_engine = compression::get_engine(compression_type).expect("Unable to get compression engine");
let magic = compression_engine.magic(item_path.clone());
let file_magic_cell = match magic {
Ok(magic) => Cell::new(magic.as_str()),
Err(e) => Cell::new(&e.to_string()).with_style(Attr::ForegroundColor(color::RED)).with_style(Attr::Bold)
};
table.add_row(Row::new(vec![
Cell::new("File Magic").with_style(Attr::Bold),
file_magic_cell
]));
table.add_row(Row::new(vec![
Cell::new("Tags").with_style(Attr::Bold),
Cell::new(&item_tags.join(" "))
]));
for meta in db::get_item_meta(conn, &item)? {
let meta_name = format!("Meta: {}", &meta.name);
table.add_row(Row::new(vec![
Cell::new(meta_name.as_str()).with_style(Attr::Bold),
Cell::new(&meta.value)
]));
}
table.printstd();
Ok(())
} else {
Err(anyhow!("Unable to find matching item in database"))
}
}
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 atleast one ID when using --delete").exit();
} else if ! tags.is_empty() {
cmd.error(ErrorKind::InvalidValue, "Tags given but not supported, you must supply atleast one ID when using --delete").exit();
}
for item_id in ids.iter() {
if let Some(item) = db::get_item(conn, *item_id)? {
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))?;
} else {
warn!("Unable to find item {item_id} in database");
}
}
Ok(())
}
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"))
]));
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: CompressionEngineProgram = match &compression::COMPRESSION_PROGRAMS[compression_type.clone()] {
Some(compression_program) => compression_program.clone(),
None => CompressionEngineProgram {
program: "".to_string(),
compress: Vec::new(),
decompress: Vec::new(),
supported: true
}
};
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")
},
match compression_program.program.eq("") {
true => Cell::new("<INTERNAL>").with_style(Attr::ForegroundColor(color::BRIGHT_BLACK)),
false => 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(())
}
fn get_meta_from_env() -> HashMap<String,String> {
debug!("MAIN: Getting meta from KEEP_META_*");
let re = Regex::new(r"^KEEP_META_(.+)$").unwrap();
let mut meta_env: HashMap<String,String> = HashMap::new();
for (key, value) in env::vars() {
if let Some(meta_name_caps) = re.captures(key.as_str()) {
let name = String::from(meta_name_caps.get(1).unwrap().as_str());
debug!("MAIN: Found meta: {}={}", name.clone(), value.clone());
meta_env.insert(name, value.clone());
}
}
meta_env
}
fn format_size_human_readable(size: u64) -> String {
let options = humansize::FormatSizeOptions::from(BINARY)
.decimal_places(1);
humansize::format_size(size, options)
}
fn format_size(size: u64, human_readable: bool) -> String {
match human_readable {
true => format_size_human_readable(size),
false => size.to_string()
}
}
fn string_column(s: String, column_width: usize) -> String {
if column_width > 0 {
match s.char_indices().nth(column_width) {
None => s.to_string(),
Some((idx, _)) => s[..idx].to_string(),
}
} else {
s.to_string()
}
}
fn size_column(size: u64, human_readable: bool, column_width: usize) -> String {
string_column(format_size(size, human_readable), column_width)
}