1039 lines
35 KiB
Rust
1039 lines
35 KiB
Rust
use nix::fcntl::FdFlag;
|
|
use nix::unistd::{close, pipe};
|
|
use nix::Error as NixError;
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::io;
|
|
use std::io::{BufWriter, Read, Write};
|
|
use std::os::fd::FromRawFd;
|
|
use std::path::PathBuf;
|
|
use std::process::Stdio; // For Stdio::null, Stdio::piped
|
|
use std::str::FromStr;
|
|
|
|
use anyhow::{anyhow, Context, Error, Result};
|
|
use clap::error::ErrorKind;
|
|
use clap::*;
|
|
use gethostname::gethostname;
|
|
use log::*;
|
|
use rusqlite::Connection;
|
|
mod modes;
|
|
use crate::modes::common::*;
|
|
|
|
extern crate directories;
|
|
use directories::ProjectDirs;
|
|
|
|
extern crate prettytable;
|
|
use prettytable::color;
|
|
use prettytable::format;
|
|
use prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR;
|
|
use prettytable::format::{Alignment, TableFormat};
|
|
use prettytable::row;
|
|
use prettytable::{Attr, Cell, Row, Table};
|
|
|
|
use chrono::prelude::*;
|
|
|
|
#[macro_use]
|
|
extern crate lazy_static;
|
|
|
|
pub mod compression;
|
|
pub mod db;
|
|
//pub mod item;
|
|
|
|
use compression::CompressionType;
|
|
|
|
use is_terminal::IsTerminal;
|
|
|
|
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())))
|
|
}
|
|
}
|
|
|
|
#[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 => {
|
|
crate::modes::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 => {
|
|
crate::modes::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 => {
|
|
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(())
|
|
}
|
|
|
|
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_diff(
|
|
cmd: &mut clap::Command,
|
|
_args: Args, // Mark as unused if not needed directly
|
|
ids: &mut Vec<i64>,
|
|
tags: &mut Vec<String>,
|
|
conn: &mut Connection,
|
|
data_path: PathBuf,
|
|
) -> Result<()> {
|
|
if !tags.is_empty() {
|
|
cmd.error(
|
|
ErrorKind::InvalidValue,
|
|
"Tags are not supported with --diff. Please provide exactly two IDs.",
|
|
)
|
|
.exit();
|
|
}
|
|
if ids.len() != 2 {
|
|
cmd.error(
|
|
ErrorKind::InvalidValue,
|
|
"You must supply exactly two IDs when using --diff.",
|
|
)
|
|
.exit();
|
|
}
|
|
|
|
// Fetch items, ensuring they exist.
|
|
let item_a = db::get_item(conn, ids[0])?
|
|
.ok_or_else(|| anyhow!("Unable to find first item (ID: {}) in database", ids[0]))?;
|
|
let item_b = db::get_item(conn, ids[1])?
|
|
.ok_or_else(|| anyhow!("Unable to find second item (ID: {}) in database", ids[1]))?;
|
|
|
|
debug!("MAIN: Found item A {:?}", item_a);
|
|
debug!("MAIN: Found item B {:?}", item_b);
|
|
|
|
let item_a_tags: Vec<String> = db::get_item_tags(conn, &item_a)?
|
|
.into_iter()
|
|
.map(|x| x.name)
|
|
.collect();
|
|
|
|
let item_b_tags: Vec<String> = db::get_item_tags(conn, &item_b)?
|
|
.into_iter()
|
|
.map(|x| x.name)
|
|
.collect();
|
|
|
|
let mut item_path_a = data_path.clone();
|
|
item_path_a.push(item_a.id.unwrap().to_string()); // id.unwrap() is safe due to ok_or_else
|
|
let compression_type_a = CompressionType::from_str(&item_a.compression)?;
|
|
debug!("MAIN: Item A has compression type {:?}", compression_type_a);
|
|
|
|
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);
|
|
|
|
// Create pipes for diff's input
|
|
let (fd_a_read, fd_a_write) =
|
|
pipe().map_err(|e: NixError| anyhow!("Failed to create pipe A: {}", e))?;
|
|
let (fd_b_read, fd_b_write) =
|
|
pipe().map_err(|e: NixError| anyhow!("Failed to create pipe B: {}", e))?;
|
|
|
|
// Set FD_CLOEXEC on write ends. While they are consumed by File::from_raw_fd,
|
|
// it's good practice if the raw FDs were to be handled further before that.
|
|
// For this specific code, since from_raw_fd takes ownership immediately, this is less critical
|
|
// but doesn't hurt.
|
|
nix::fcntl::fcntl(
|
|
fd_a_write,
|
|
nix::fcntl::FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC),
|
|
)
|
|
.map_err(|e| anyhow!("Failed to set FD_CLOEXEC on fd_a_write: {}", e))?;
|
|
nix::fcntl::fcntl(
|
|
fd_b_write,
|
|
nix::fcntl::FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC),
|
|
)
|
|
.map_err(|e| anyhow!("Failed to set FD_CLOEXEC on fd_b_write: {}", e))?;
|
|
|
|
debug!("MAIN: Creating child process for diff");
|
|
let mut diff_command = std::process::Command::new("diff");
|
|
diff_command
|
|
.arg("-u")
|
|
.arg("--label")
|
|
.arg(format!(
|
|
"Keep item A: {} {}",
|
|
item_a.id.unwrap(),
|
|
item_a_tags.join(" ")
|
|
))
|
|
.arg(format!("/dev/fd/{}", fd_a_read))
|
|
.arg("--label")
|
|
.arg(format!(
|
|
"Keep item B: {} {}",
|
|
item_b.id.unwrap(),
|
|
item_b_tags.join(" ")
|
|
))
|
|
.arg(format!("/dev/fd/{}", fd_b_read))
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let mut child_process = diff_command
|
|
.spawn()
|
|
.map_err(|e| anyhow!("Failed to execute diff command: {}", e))?;
|
|
|
|
close(fd_a_read).map_err(|e| anyhow!("Failed to close fd_a_read in parent: {}", e))?;
|
|
close(fd_b_read).map_err(|e| anyhow!("Failed to close fd_b_read in parent: {}", e))?;
|
|
|
|
let mut child_stdout_pipe = child_process
|
|
.stdout
|
|
.take()
|
|
.expect("BUG: Failed to capture diff stdout pipe");
|
|
let mut child_stderr_pipe = child_process
|
|
.stderr
|
|
.take()
|
|
.expect("BUG: Failed to capture diff stderr pipe");
|
|
|
|
debug!("MAIN: Creating threads for diff I/O");
|
|
|
|
// Thread to write Item A data to diff
|
|
let writer_thread_a = {
|
|
// `File::from_raw_fd` takes ownership of fd_a_write.
|
|
let pipe_writer_a_raw = unsafe { std::fs::File::from_raw_fd(fd_a_write) };
|
|
let item_path_a_clone = item_path_a.clone(); // Avoid lifetime issues if item_path_a is used later
|
|
let compression_type_a_clone = compression_type_a.clone();
|
|
std::thread::spawn(move || {
|
|
// Original code used .expect/.unwrap, implying panics on error.
|
|
// This matches that style. For more robust error handling, return Result from thread.
|
|
let mut buffered_pipe_writer_a = BufWriter::new(pipe_writer_a_raw);
|
|
let engine_a = compression::get_engine(compression_type_a_clone)
|
|
.expect("Unable to get compression engine for Item A");
|
|
debug!("THREAD_A: Sending item A to diff");
|
|
engine_a
|
|
.copy(item_path_a_clone, &mut buffered_pipe_writer_a)
|
|
.expect("Failed to copy/compress item A");
|
|
debug!("THREAD_A: Done Sending item A to diff");
|
|
// pipe_writer_a_raw (and buffered_pipe_writer_a) are dropped here, closing fd_a_write.
|
|
// This signals EOF to one of diff's inputs.
|
|
})
|
|
};
|
|
|
|
// Thread to write Item B data to diff
|
|
let writer_thread_b = {
|
|
let pipe_writer_b_raw = unsafe { std::fs::File::from_raw_fd(fd_b_write) };
|
|
let item_path_b_clone = item_path_b.clone();
|
|
let compression_type_b_clone = compression_type_b.clone();
|
|
std::thread::spawn(move || {
|
|
let mut buffered_pipe_writer_b = BufWriter::new(pipe_writer_b_raw);
|
|
let engine_b = compression::get_engine(compression_type_b_clone)
|
|
.expect("Unable to get compression engine for Item B");
|
|
debug!("THREAD_B: Sending item B to diff");
|
|
engine_b
|
|
.copy(item_path_b_clone, &mut buffered_pipe_writer_b)
|
|
.expect("Failed to copy/compress item B");
|
|
debug!("THREAD_B: Done Sending item B to diff");
|
|
})
|
|
};
|
|
|
|
// Thread to read diff's standard output
|
|
let stdout_reader_thread = std::thread::spawn(move || {
|
|
let mut output_buffer = Vec::new();
|
|
debug!("STDOUT_READER: Reading diff stdout");
|
|
// child_stdout_pipe is a ChildStdout, which implements std::io::Read
|
|
child_stdout_pipe
|
|
.read_to_end(&mut output_buffer)
|
|
.map_err(|e| anyhow!("Failed to read diff stdout: {}", e))
|
|
.map(|_| output_buffer) // Return the Vec<u8> on success
|
|
});
|
|
|
|
// Thread to read diff's standard error
|
|
let stderr_reader_thread = std::thread::spawn(move || {
|
|
let mut error_buffer = Vec::new();
|
|
debug!("STDERR_READER: Reading diff stderr");
|
|
child_stderr_pipe
|
|
.read_to_end(&mut error_buffer)
|
|
.map_err(|e| anyhow!("Failed to read diff stderr: {}", e))
|
|
.map(|_| error_buffer)
|
|
});
|
|
|
|
// Wait for writer threads to complete (meaning all input has been sent to diff)
|
|
debug!("MAIN: Waiting on writer thread for item A");
|
|
if let Err(panic_payload) = writer_thread_a.join() {
|
|
// Propagate panic from writer thread
|
|
return Err(anyhow!(
|
|
"Writer thread for item A (ID: {}) panicked: {:?}",
|
|
ids[0],
|
|
panic_payload
|
|
));
|
|
}
|
|
debug!("MAIN: Writer thread for item A completed.");
|
|
|
|
debug!("MAIN: Waiting on writer thread for item B");
|
|
if let Err(panic_payload) = writer_thread_b.join() {
|
|
return Err(anyhow!(
|
|
"Writer thread for item B (ID: {}) panicked: {:?}",
|
|
ids[1],
|
|
panic_payload
|
|
));
|
|
}
|
|
debug!("MAIN: Writer thread for item B completed.");
|
|
debug!("MAIN: Done waiting on input-writer threads.");
|
|
|
|
// Now that all input has been sent and input pipes will be closed by threads exiting,
|
|
// wait for the diff child process to terminate.
|
|
debug!("MAIN: Waiting for diff child process to finish...");
|
|
let diff_status = child_process
|
|
.wait()
|
|
.map_err(|e| anyhow!("Failed to wait on diff command: {}", e))?;
|
|
debug!(
|
|
"MAIN: Diff child process finished with status: {}",
|
|
diff_status
|
|
);
|
|
|
|
// Retrieve the captured output from the reader threads.
|
|
// .join().unwrap() here will panic if the reader thread itself panicked.
|
|
// The inner Result is from the read_to_end operation within the thread.
|
|
let stdout_capture_result = stdout_reader_thread
|
|
.join()
|
|
.unwrap_or_else(|panic_payload| {
|
|
Err(anyhow!(
|
|
"Stdout reader thread panicked: {:?}",
|
|
panic_payload
|
|
))
|
|
})?;
|
|
let stderr_capture_result = stderr_reader_thread
|
|
.join()
|
|
.unwrap_or_else(|panic_payload| {
|
|
Err(anyhow!(
|
|
"Stderr reader thread panicked: {:?}",
|
|
panic_payload
|
|
))
|
|
})?;
|
|
|
|
// Handle diff's exit status and output
|
|
match diff_status.code() {
|
|
Some(0) => {
|
|
// Exit code 0: No differences
|
|
debug!("MAIN: Diff successful, no differences found.");
|
|
// Typically, diff -u doesn't print to stdout if no differences.
|
|
// But if it did, it would be shown here.
|
|
if !stdout_capture_result.is_empty() {
|
|
println!("{}", String::from_utf8_lossy(&stdout_capture_result));
|
|
}
|
|
}
|
|
Some(1) => {
|
|
// Exit code 1: Differences found
|
|
debug!("MAIN: Diff successful, differences found.");
|
|
println!("{}", String::from_utf8_lossy(&stdout_capture_result));
|
|
}
|
|
Some(error_code) => {
|
|
// Exit code > 1: Error in diff utility
|
|
eprintln!("Diff command failed with exit code: {}", error_code);
|
|
if !stdout_capture_result.is_empty() {
|
|
eprintln!(
|
|
"Diff stdout before error:\n{}",
|
|
String::from_utf8_lossy(&stdout_capture_result)
|
|
);
|
|
}
|
|
if !stderr_capture_result.is_empty() {
|
|
eprintln!(
|
|
"Diff stderr:\n{}",
|
|
String::from_utf8_lossy(&stderr_capture_result)
|
|
);
|
|
}
|
|
return Err(anyhow!(
|
|
"Diff command reported an error (exit code {})",
|
|
error_code
|
|
));
|
|
}
|
|
None => {
|
|
// Process terminated by a signal
|
|
eprintln!("Diff command terminated by signal.");
|
|
if !stderr_capture_result.is_empty() {
|
|
eprintln!(
|
|
"Diff stderr before signal termination:\n{}",
|
|
String::from_utf8_lossy(&stderr_capture_result)
|
|
);
|
|
}
|
|
return Err(anyhow!("Diff command terminated by signal"));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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_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"))
|
|
}
|
|
}
|