use std::io::{self, Write};
use std::os::unix::prelude::PermissionsExt;
use std::path::Path;
use anyhow::{Context, Result};
use chrono::Local;
use garbage_fs::Filesystem;
use crate::{utils, TrashDir, TrashInfo};
use crate::{HOME_MOUNT, MOUNTS};
use super::PutOptions;
#[derive(Debug)]
pub enum DeletionStrategy {
Fixed(TrashDir),
MoveTo(TrashDir),
CopyTo(TrashDir),
}
impl DeletionStrategy {
pub fn pick_strategy(
target: impl AsRef<Path>,
fs: &impl Filesystem,
) -> Result<DeletionStrategy> {
let target = target.as_ref();
let target_mount = MOUNTS
.get_mount_point(target)
.context("Could not find mount point.")?;
if target_mount == *HOME_MOUNT {
debug!(
?target,
"Picking strategy MoveTo because target was on the home mount.",
);
return Ok(DeletionStrategy::MoveTo(TrashDir::get_home_trash()));
}
debug!("Considering $mount/.Trash");
'topdir: while should_use_topdir_trash(&target_mount, fs)? {
let topdir_trash_dir = target_mount
.join(".Trash")
.join(utils::get_uid().to_string());
let trash_dir = TrashDir::from(&topdir_trash_dir);
match trash_dir.mkdir(fs) {
Ok(_) => (),
Err(err) => {
debug!(
file = ?target,
mount_trash_dir = ?topdir_trash_dir,
?err,
"Not deleting file in mount's trash-dir because of IO error"
);
break 'topdir;
}
}
return Ok(DeletionStrategy::MoveTo(trash_dir));
}
debug!("Considering $mount/.Trash-$uid");
'cond: while should_use_topdir_trash_uid(&target_mount, fs) {
let topdir_trash_uid =
target_mount.join(format!(".Trash-{}", utils::get_uid()));
let trash_dir = TrashDir::from(topdir_trash_uid);
debug!(
trash_dir = ?trash_dir,
"Trying to create trash directory."
);
if let Err(err) = trash_dir.mkdir(fs) {
debug!(
err = err.to_string(),
trash_dir = ?trash_dir,
"Could not use the $mount/.Trash-$uid directory, falling back instead."
);
break 'cond;
}
return Ok(DeletionStrategy::MoveTo(trash_dir));
}
Ok(DeletionStrategy::CopyTo(TrashDir::get_home_trash()))
}
fn get_target_trash(&self) -> (&TrashDir, bool) {
match self {
DeletionStrategy::Fixed(trash) => {
(trash, true)
}
DeletionStrategy::MoveTo(trash) => (trash, false),
DeletionStrategy::CopyTo(trash) => (trash, true),
}
}
pub fn delete(
&self,
target: impl AsRef<Path>,
options: &PutOptions,
fs: &impl Filesystem,
) -> Result<()> {
let target = target.as_ref();
let _link_info = target.read_link().ok();
let (trash_dir, requires_copy) = self.get_target_trash();
let _guard = debug_span!("trash_dir", "{trash_dir:?}").entered();
if !options.force && (requires_copy || options.prompt) {
if requires_copy {
eprint!("Removing file '{}' requires potentially expensive copying. Continue? [y/n] ", target.to_str().unwrap());
} else if options.prompt {
eprint!("Remove file '{}'? [y/n] ", target.to_str().unwrap());
}
io::stderr().flush()?;
let should_continue = loop {
let stdin = io::stdin();
let mut s = String::new();
stdin.read_line(&mut s).unwrap();
match s.trim().to_lowercase().as_str() {
"yes" | "y" => break true,
"no" | "n" => break false,
_ => {
eprint!("Invalid response. Please type yes or no: ");
}
}
};
if !should_continue {
bail!("Cancelled by user.");
}
}
let now = Local::now();
let elapsed = now.timestamp_millis();
let elapsed_str = elapsed.to_string();
let target_file = match target.file_name() {
Some(file) => file.to_os_string(),
None => bail!("Invalid filename found"),
};
let file_name = concat_os_str!(elapsed_str, ".", target_file);
let trash_file_path = trash_dir
.files_dir(fs)
.context("Failed to get trash dir files")?
.join(&file_name);
let trash_info_path = trash_dir
.info_dir(fs)?
.join(concat_os_str!(file_name, ".trashinfo"));
debug!(path_to_trashed_file = trash_file_path.display().to_string());
debug!(path_to_trash_info = trash_info_path.display().to_string());
let trash_info = TrashInfo {
path: utils::into_absolute(target)?,
deletion_date: now,
deleted_path: trash_file_path.clone(),
info_path: trash_info_path.clone(),
};
{
let mut trash_info_file = fs.create_file(trash_info_path)?;
trash_info.write(&mut trash_info_file)?;
}
debug!("Trash info: {:?}", trash_info);
let is_symlink = {
let meta = fs.symlink_metadata(target)?;
let file_type = meta.file_type();
file_type.is_symlink()
};
if requires_copy {
utils::recursive_copy(&target, &trash_file_path)?;
if target.is_dir() {
fs.remove_dir_all(target)?;
} else if target.is_file() {
fs.remove_file(target)?;
}
} else {
debug!(
from = ?target,
to = ?trash_file_path,
is_symlink,
"Renaming file into trash directory.",
);
if is_symlink {
let real_target = fs.read_link(target)?;
debug!("Real target: {:?}", real_target);
std::os::unix::fs::symlink(&real_target, &trash_file_path)?;
fs.remove_file(target)?;
} else {
fs.rename(target, trash_file_path)?;
}
info!("Removed {}", target.display());
}
Ok(())
}
}
fn should_use_topdir_trash(
mount: impl AsRef<Path>,
fs: &impl Filesystem,
) -> Result<bool> {
let mount = mount.as_ref();
let trash_dir = mount.join(".Trash");
if !fs.path_exists(&trash_dir)? {
return Ok(false);
}
let dir = fs.open_file(&trash_dir).with_context(|| {
format!("Error opening top directory: {}", trash_dir.display())
})?;
let meta = fs
.file_metadata(&dir)
.context("Could not retrieve file metadata.")?;
if meta.file_type().is_symlink() {
debug!("Trashdir {:?} is a symlink.", trash_dir);
return Ok(false);
}
let perms = meta.permissions();
Ok(perms.mode() & 0o1000 > 0)
}
fn should_use_topdir_trash_uid(
path: impl AsRef<Path>,
fs: &impl Filesystem,
) -> bool {
let path = path.as_ref();
if !path.exists() {
match fs.create_dir(path) {
Ok(_) => (),
Err(_) => return false,
};
}
return true;
}