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;
#[derive(Parser)]
pub struct RestoreOptions {
#[clap(long = "trash-dir", value_parser, value_hint(ValueHint::DirPath))]
trash_dir: Option<PathBuf>,
#[clap(short = 'a', long = "all")]
all: bool,
}
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());
}
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(¤t_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")
.arg("-m")
.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;
}
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]; 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(())
}