use std::io; use std::io::{Read, Write}; 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 } #[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, #[arg(help_heading("Item Options"), short, long, env("KEEP_COMPRESSION"), )] #[arg(help("Compression algorithm to use when saving items"))] compression: Option, } #[derive(Parser, Debug)] struct OptionsArgs { #[arg(long, env("KEEP_DIR"))] #[arg(help("Specify the directory to use for storage"))] 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"))] 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 { 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 { Ok(s.parse::() .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, tags: &mut Vec, 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 = 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 = 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, tags: &mut Vec, 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 = 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, tags: &mut Vec, 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::get_item(conn, ids[0])?; let mut item_b: Option = 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 mut child = std::process::Command::new("diff") .arg("-u") .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .expect("Failed to execute diff command"); let stdout_a = unsafe { std::fs::File::from_raw_fd(fd_a_write) }; let stdout_b = unsafe { std::fs::File::from_raw_fd(fd_b_write) }; let mut stdout_a = BufWriter::new(stdout_a); let mut stdout_b = BufWriter::new(stdout_b); let mut stdout_a = BufWriter::new(stdout_a); let mut stdout_b = BufWriter::new(stdout_b); let mut child_stdin = child.stdin.unwrap(); 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)); } if output.status.success() { println!("{}", String::from_utf8_lossy(&output.stdout)); } else { eprintln!("{}", String::from_utf8_lossy(&output.stderr)); } if output.status.success() { println!("{}", String::from_utf8_lossy(&output.stdout)); } else { eprintln!("{}", String::from_utf8_lossy(&output.stderr)); } 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, tags: &Vec, 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 = 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> = HashMap::new(); let mut meta_by_item: HashMap> = HashMap::new(); for item in items.iter() { let item_id = item.id.unwrap(); let item_tags: Vec = 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 = 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, tags: &mut Vec, 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, tags: &mut Vec, 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 = 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 = 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, tags: &mut Vec, 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("").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 { debug!("MAIN: Getting meta from KEEP_META_*"); let re = Regex::new(r"^KEEP_META_(.+)$").unwrap(); let mut meta_env: HashMap = 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) }