garbage 0.4.3

CLI tool for interacting with the freedesktop trashcan
Documentation
use std::env;
use std::fs;
use std::io::{self, BufRead, BufReader, Write};
use std::mem;
use std::path::PathBuf;
use std::process::{Command, Stdio};

use anyhow::Result;
use chrono_humanize::HumanTime;
use clap::ValueHint;
use garbage_fs::Filesystem;

use crate::list;
use crate::TrashDir;
use crate::TrashInfo;

/// Options to pass to restore
#[derive(Parser)]
pub struct RestoreOptions {
  /// The path to the trash directory to restore from.
  /// By default, this is your home directory's trash ($XDG_DATA_HOME/Trash)
  #[clap(long = "trash-dir", value_parser, value_hint(ValueHint::DirPath))]
  trash_dir: Option<PathBuf>,

  /// List all files in the trash (by default, only files in the current
  /// directory are listed)
  #[clap(short = 'a', long = "all")]
  all: bool,
}

/// Restore files from a trash directory
pub fn restore(options: RestoreOptions, fs: &impl Filesystem) -> Result<()> {
  let trash_dir = TrashDir::from_opt(options.trash_dir.as_ref());

  if trash_dir.check_info_dir(fs)?.is_none() {
    bail!("Trash directory {:?} doesn't exist.", trash_dir.path());
  }

  // get list of files sorted by deletion date
  // TODO: possible to get this to be streaming?
  let current_dir = env::current_dir()?;
  let files = {
    let mut files = trash_dir
      .iter(fs)?
      .filter_map(|entry| match entry {
        Ok(info) => {
          if !options.all && !info.path.starts_with(&current_dir) {
            return None;
          }
          Some(info)
        }
        Err(err) => {
          eprintln!("failed to get file info: {:?}", err);
          None
        }
      })
      .collect::<Vec<_>>();

    files.sort_unstable_by_key(|info| info.deletion_date);
    files
  };

  if files.len() == 0 {
    bail!("No files in trash directory {:?}", trash_dir.path());
  }

  if !fzf_interactive(&files)? {
    plain_interactive(&files)?;
  }

  Ok(())
}

fn fzf_interactive(files: &[TrashInfo]) -> Result<bool> {
  use std::io::ErrorKind;

  let child_result = Command::new("fzf")
    // multi-search
    .arg("-m")
    // hide the line numbers
    .arg("--with-nth=2..")
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .spawn();

  let mut child = match child_result {
    Ok(child) => child,
    Err(e) if matches!(e.kind(), ErrorKind::NotFound) => return Ok(false),
    Err(e) => return Err(e.into()),
  };

  let mut stdin = child.stdin.take().expect("should be piped");
  for (i, trash_info) in files.iter().enumerate() {
    let line = format!(
      "{} {}\t{}\n",
      i,
      trash_info.path.display(),
      HumanTime::from(trash_info.deletion_date)
    );
    stdin.write(line.as_bytes())?;
  }
  mem::drop(stdin);

  let stdout = child.stdout.take().expect("should be piped");
  let mut stdout_buf = BufReader::new(stdout);

  loop {
    let mut string = String::new();
    let bytes = stdout_buf.read_line(&mut string)?;
    if bytes == 0 {
      break;
    }

    // TODO: lol
    let index = string.split(" ").next().unwrap().parse::<usize>().unwrap();
    let info = &files[index];
    restore_from_info(info)?;
  }

  Ok(true)
}

fn plain_interactive(files: &[TrashInfo]) -> Result<()> {
  list::print_files_list(files.iter(), true)?;

  let stdin = io::stdin();
  let mut s = String::new();
  eprintln!("which file to restore? [0..{}]", files.len() - 1);
  stdin.read_line(&mut s).unwrap();

  match s.trim_end().parse::<usize>() {
    Ok(i) if i < files.len() => {
      let info = &files[i]; // should never fail since we just checked
      eprintln!("moving {:?} to {:?}", &info.deleted_path, &info.path);
      restore_from_info(info)?;
    }
    _ => eprintln!("Invalid number."),
  }
  Ok(())
}

fn restore_from_info(trash_info: &TrashInfo) -> Result<()> {
  fs::remove_file(&trash_info.info_path)?;
  fs::rename(&trash_info.deleted_path, &trash_info.path)?;
  Ok(())
}