ok
This commit is contained in:
parent
a7bd15b0d9
commit
a64e391364
13 changed files with 388 additions and 269 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -88,7 +88,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garbage"
|
name = "garbage"
|
||||||
version = "0.1.4"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
"anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "garbage"
|
name = "garbage"
|
||||||
version = "0.1.4"
|
version = "0.2.0"
|
||||||
authors = ["Michael Zhang <iptq@protonmail.com>"]
|
authors = ["Michael Zhang <iptq@protonmail.com>"]
|
||||||
description = "cli tool for interacting with the freedesktop trashcan"
|
description = "cli tool for interacting with the freedesktop trashcan"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
IMAGE=$(docker build -q -f tests.Dockerfile .)
|
|
||||||
exec docker run --rm -it $IMAGE
|
|
21
src/dir.rs
21
src/dir.rs
|
@ -12,15 +12,32 @@ use crate::XDG;
|
||||||
pub struct TrashDir(pub PathBuf);
|
pub struct TrashDir(pub PathBuf);
|
||||||
|
|
||||||
impl TrashDir {
|
impl TrashDir {
|
||||||
|
pub fn from(path: impl AsRef<Path>) -> Self {
|
||||||
|
TrashDir(path.as_ref().to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets your user's "home" trash directory.
|
/// Gets your user's "home" trash directory.
|
||||||
///
|
///
|
||||||
/// According to Trash spec v1.0:
|
/// According to Trash spec v1.0:
|
||||||
///
|
///
|
||||||
/// > For every user2 a “home trash” directory MUST be available.
|
/// > For every user2 a “home trash” directory MUST be available.
|
||||||
/// > Its name and location are $XDG_DATA_HOME/Trash;
|
/// > Its name and location are $XDG_DATA_HOME/Trash
|
||||||
/// > $XDG_DATA_HOME is the base directory for user-specific data, as defined in the Desktop Base Directory Specification.
|
/// > $XDG_DATA_HOME is the base directory for user-specific data, as defined in the Desktop Base Directory Specification.
|
||||||
pub fn get_home_trash() -> Self {
|
pub fn get_home_trash() -> Self {
|
||||||
TrashDir(XDG.get_data_home().join("Trash"))
|
TrashDir::from(XDG.get_data_home().join("Trash"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_opt(opt: Option<impl AsRef<Path>>) -> Self {
|
||||||
|
opt.map(|path| TrashDir::from(path.as_ref().to_path_buf()))
|
||||||
|
.unwrap_or_else(|| TrashDir::get_home_trash())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(&self) -> Result<(), Error> {
|
||||||
|
let path = &self.0;
|
||||||
|
if !path.exists() {
|
||||||
|
fs::create_dir_all(&path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to this trash directory.
|
/// Returns the path to this trash directory.
|
||||||
|
|
|
@ -17,7 +17,6 @@ mod errors;
|
||||||
mod info;
|
mod info;
|
||||||
mod mounts;
|
mod mounts;
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
mod strategy;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
89
src/main.rs
89
src/main.rs
|
@ -2,101 +2,46 @@
|
||||||
|
|
||||||
extern crate anyhow;
|
extern crate anyhow;
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::io;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use garbage::{
|
use garbage::ops::{self, EmptyOptions, ListOptions, PutOptions, RestoreOptions};
|
||||||
ops::{self, EmptyOptions},
|
|
||||||
TrashDir,
|
|
||||||
};
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
enum Command {
|
enum Command {
|
||||||
|
/// Empty a trash directory.
|
||||||
#[structopt(name = "empty")]
|
#[structopt(name = "empty")]
|
||||||
Empty(EmptyOptions),
|
Empty(EmptyOptions),
|
||||||
|
|
||||||
|
/// List the contents of a trash directory.
|
||||||
#[structopt(name = "list")]
|
#[structopt(name = "list")]
|
||||||
List,
|
List(ListOptions),
|
||||||
|
|
||||||
|
/// Puts files into the trash.
|
||||||
|
///
|
||||||
|
/// If a trash directory isn't specified, the best strategy is picked
|
||||||
|
/// for each file that's deleted (after shell glob expansion). The algorithm
|
||||||
|
/// for deciding a strategy is specified in the FreeDesktop Trash spec.
|
||||||
#[structopt(name = "put")]
|
#[structopt(name = "put")]
|
||||||
Put {
|
Put(PutOptions),
|
||||||
/// The target path to be trashed
|
|
||||||
#[structopt(parse(from_os_str))]
|
|
||||||
paths: Vec<PathBuf>,
|
|
||||||
|
|
||||||
/// Trashes directories recursively
|
|
||||||
#[structopt(long = "recursive", short = "r")]
|
|
||||||
recursive: bool,
|
|
||||||
|
|
||||||
/// Suppress prompts/messages
|
|
||||||
#[structopt(long = "force", short = "f")]
|
|
||||||
force: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
|
/// Restores files from the trash.
|
||||||
#[structopt(name = "restore")]
|
#[structopt(name = "restore")]
|
||||||
Restore,
|
Restore(RestoreOptions),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
let cmd = Command::from_args();
|
let cmd = Command::from_args();
|
||||||
match cmd {
|
match cmd {
|
||||||
Command::Empty(options) => ops::empty(options),
|
Command::Empty(options) => ops::empty(options),
|
||||||
Command::List => {
|
Command::List(options) => ops::list(options),
|
||||||
ops::list();
|
Command::Put(options) => ops::put(options),
|
||||||
Ok(())
|
Command::Restore(options) => ops::restore(options),
|
||||||
}
|
|
||||||
Command::Put {
|
|
||||||
paths,
|
|
||||||
recursive,
|
|
||||||
force,
|
|
||||||
} => ops::put(paths, recursive, force),
|
|
||||||
Command::Restore => {
|
|
||||||
let home_trash = TrashDir::get_home_trash();
|
|
||||||
let mut files = home_trash
|
|
||||||
.iter()
|
|
||||||
.unwrap()
|
|
||||||
.filter_map(|entry| match entry {
|
|
||||||
Ok(info) => Some(info),
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("failed to get file info: {:?}", err);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
files.sort_unstable_by_key(|info| info.deletion_date);
|
|
||||||
for (i, info) in files.iter().enumerate() {
|
|
||||||
println!(
|
|
||||||
"[{}]\t{}\t{}",
|
|
||||||
i,
|
|
||||||
info.deletion_date,
|
|
||||||
info.path.to_str().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdin = io::stdin();
|
|
||||||
let mut s = String::new();
|
|
||||||
println!("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.get(i).unwrap();
|
|
||||||
println!("moving {:?} to {:?}", &info.deleted_path, &info.path);
|
|
||||||
fs::rename(&info.deleted_path, &info.path)?;
|
|
||||||
}
|
|
||||||
_ => println!("Invalid number."),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
match run() {
|
match run() {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
|
@ -11,22 +11,23 @@ use crate::TrashDir;
|
||||||
pub struct EmptyOptions {
|
pub struct EmptyOptions {
|
||||||
/// Only list the files that are to be deleted, without
|
/// Only list the files that are to be deleted, without
|
||||||
/// actually deleting anything.
|
/// actually deleting anything.
|
||||||
|
#[structopt(long = "dry")]
|
||||||
pub dry: bool,
|
pub dry: bool,
|
||||||
|
|
||||||
/// Delete all files older than (this number) of days.
|
/// Delete all files older than (this number) of integer days.
|
||||||
/// Removes everything if this option is not specified
|
/// Removes everything if this option is not specified
|
||||||
|
#[structopt(long = "days")]
|
||||||
days: Option<u32>,
|
days: Option<u32>,
|
||||||
|
|
||||||
/// The path to the trash directory to empty.
|
/// The path to the trash directory to empty.
|
||||||
|
/// By default, this is your home directory's trash ($XDG_DATA_HOME/Trash)
|
||||||
|
#[structopt(long = "trash-dir", parse(from_os_str))]
|
||||||
trash_dir: Option<PathBuf>,
|
trash_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actually delete files in the trash.
|
/// Actually delete files in the trash.
|
||||||
pub fn empty(options: EmptyOptions) -> Result<()> {
|
pub fn empty(options: EmptyOptions) -> Result<()> {
|
||||||
let trash_dir = options
|
let trash_dir = TrashDir::from_opt(options.trash_dir);
|
||||||
.trash_dir
|
|
||||||
.map(TrashDir)
|
|
||||||
.unwrap_or_else(|| TrashDir::get_home_trash());
|
|
||||||
|
|
||||||
// cutoff date
|
// cutoff date
|
||||||
let cutoff = if let Some(days) = options.days {
|
let cutoff = if let Some(days) = options.days {
|
||||||
|
|
|
@ -1,8 +1,21 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::TrashDir;
|
use crate::TrashDir;
|
||||||
|
|
||||||
pub fn list() {
|
#[derive(StructOpt)]
|
||||||
let home_trash = TrashDir::get_home_trash();
|
pub struct ListOptions {
|
||||||
let mut files = home_trash
|
/// The path to the trash directory to list.
|
||||||
|
/// By default, this is your home directory's trash ($XDG_DATA_HOME/Trash)
|
||||||
|
#[structopt(long = "trash-dir", parse(from_os_str))]
|
||||||
|
trash_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(options: ListOptions) -> Result<()> {
|
||||||
|
let trash_dir = TrashDir::from_opt(options.trash_dir);
|
||||||
|
|
||||||
|
let mut files = trash_dir
|
||||||
.iter()
|
.iter()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_map(|entry| match entry {
|
.filter_map(|entry| match entry {
|
||||||
|
@ -17,4 +30,6 @@ pub fn list() {
|
||||||
for info in files {
|
for info in files {
|
||||||
println!("{}\t{}", info.deletion_date, info.path.to_str().unwrap());
|
println!("{}\t{}", info.deletion_date, info.path.to_str().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,6 @@ mod put;
|
||||||
mod restore;
|
mod restore;
|
||||||
|
|
||||||
pub use self::empty::{empty, EmptyOptions};
|
pub use self::empty::{empty, EmptyOptions};
|
||||||
pub use self::list::list;
|
pub use self::list::{list, ListOptions};
|
||||||
pub use self::put::put;
|
pub use self::put::{put, PutOptions};
|
||||||
pub use self::restore::restore;
|
pub use self::restore::{restore, RestoreOptions};
|
||||||
|
|
272
src/ops/put.rs
272
src/ops/put.rs
|
@ -1,33 +1,289 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::fs::{self, File};
|
||||||
|
use std::io;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
use crate::strategy::DeletionStrategy;
|
use crate::utils;
|
||||||
use crate::HOME_TRASH;
|
use crate::{TrashDir, TrashInfo};
|
||||||
|
use crate::{HOME_MOUNT, HOME_TRASH, MOUNTS};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
#[error("File {0} doesn't exist.")]
|
||||||
|
FileDoesntExist(PathBuf),
|
||||||
|
|
||||||
#[error("Refusing to remove '.' or '..', skipping...")]
|
#[error("Refusing to remove '.' or '..', skipping...")]
|
||||||
CannotTrashDotDirs,
|
CannotTrashDotDirs,
|
||||||
|
|
||||||
|
#[error("Cancelled by user.")]
|
||||||
|
CancelledByUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt)]
|
||||||
|
pub struct PutOptions {
|
||||||
|
/// The target path to be trashed
|
||||||
|
#[structopt(parse(from_os_str))]
|
||||||
|
paths: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// Trashes directories recursively
|
||||||
|
#[structopt(long = "recursive", short = "r")]
|
||||||
|
recursive: bool,
|
||||||
|
|
||||||
|
/// Suppress prompts/messages
|
||||||
|
#[structopt(long = "force", short = "f")]
|
||||||
|
force: bool,
|
||||||
|
|
||||||
|
/// Put all the trashed files into this trash directory
|
||||||
|
/// regardless of what filesystem is on.
|
||||||
|
///
|
||||||
|
/// If a copy is required to copy the file, a prompt will be raised,
|
||||||
|
/// which can be bypassed by passing --force.
|
||||||
|
///
|
||||||
|
/// If this option is not passed, the best strategy will be chosen
|
||||||
|
/// automatically for each file.
|
||||||
|
#[structopt(long = "trash-dir", parse(from_os_str))]
|
||||||
|
trash_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Throw some files into the trash.
|
/// Throw some files into the trash.
|
||||||
pub fn put(paths: Vec<PathBuf>, _recursive: bool, _force: bool) -> Result<()> {
|
pub fn put(options: PutOptions) -> Result<()> {
|
||||||
let strategy = DeletionStrategy::pick_strategy(&HOME_TRASH);
|
for path in options.paths.iter() {
|
||||||
for path in paths {
|
|
||||||
// don't allow deleting '.' or '..'
|
// don't allow deleting '.' or '..'
|
||||||
let current_dir = env::current_dir()?;
|
let current_dir = env::current_dir()?;
|
||||||
ensure!(
|
ensure!(
|
||||||
!(path == current_dir
|
!(path.as_path() == current_dir.as_path()
|
||||||
|| (current_dir.parent().is_some() && path == current_dir.parent().unwrap())),
|
|| (current_dir.parent().is_some() && path == current_dir.parent().unwrap())),
|
||||||
Error::CannotTrashDotDirs
|
Error::CannotTrashDotDirs
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(err) = strategy.delete(path) {
|
// pick the best strategy for deleting this particular file
|
||||||
|
let strategy = if let Some(ref trash_dir) = options.trash_dir {
|
||||||
|
DeletionStrategy::Fixed(TrashDir::from(trash_dir))
|
||||||
|
} else {
|
||||||
|
DeletionStrategy::pick_strategy(path)?
|
||||||
|
};
|
||||||
|
println!("Strategy: {:?}", strategy);
|
||||||
|
|
||||||
|
if let Err(err) = strategy.delete(path, options.force) {
|
||||||
eprintln!("{}", err);
|
eprintln!("{}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// DeletionStrategy describes a strategy by which a file is deleted
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DeletionStrategy {
|
||||||
|
/// move or copy the file to this particular trash
|
||||||
|
Fixed(TrashDir),
|
||||||
|
|
||||||
|
/// move the candidate files/directories to the trash directory
|
||||||
|
/// (this requires that both the candidate and the trash directories be on the same filesystem)
|
||||||
|
MoveTo(TrashDir),
|
||||||
|
|
||||||
|
/// recursively copy the candidate files/directories to the trash directory
|
||||||
|
CopyTo(TrashDir),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeletionStrategy {
|
||||||
|
/// This method picks the ideal strategy
|
||||||
|
pub fn pick_strategy(target: impl AsRef<Path>) -> Result<DeletionStrategy> {
|
||||||
|
let target = target.as_ref();
|
||||||
|
let target_mount = MOUNTS
|
||||||
|
.get_mount_point(target)
|
||||||
|
.ok_or_else(|| anyhow!("couldn't get mount point"))?;
|
||||||
|
|
||||||
|
// first, are we on the home mount?
|
||||||
|
if target_mount == *HOME_MOUNT {
|
||||||
|
return Ok(DeletionStrategy::MoveTo(TrashDir::get_home_trash()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to use the $topdir/.Trash directory
|
||||||
|
if should_use_topdir_trash(&target_mount) {
|
||||||
|
let topdir_trash_dir = target_mount
|
||||||
|
.join(".Trash")
|
||||||
|
.join(utils::get_uid().to_string());
|
||||||
|
let trash_dir = TrashDir::from(topdir_trash_dir);
|
||||||
|
trash_dir.create()?;
|
||||||
|
return Ok(DeletionStrategy::MoveTo(trash_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to use the $topdir/.Trash-$uid directory
|
||||||
|
if should_use_topdir_trash_uid(&target_mount) {
|
||||||
|
let topdir_trash_uid = target_mount.join(format!(".Trash-{}", utils::get_uid()));
|
||||||
|
let trash_dir = TrashDir::from(topdir_trash_uid);
|
||||||
|
trash_dir.create()?;
|
||||||
|
return Ok(DeletionStrategy::MoveTo(trash_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's not on the home mount, but we'll copy into it anyway
|
||||||
|
Ok(DeletionStrategy::CopyTo(TrashDir::get_home_trash()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_target_trash(&self) -> (&TrashDir, bool) {
|
||||||
|
match self {
|
||||||
|
DeletionStrategy::Fixed(trash) => {
|
||||||
|
// TODO: finish
|
||||||
|
(trash, true)
|
||||||
|
}
|
||||||
|
DeletionStrategy::MoveTo(trash) => (trash, false),
|
||||||
|
DeletionStrategy::CopyTo(trash) => (trash, true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn get_target_trash(
|
||||||
|
// &self,
|
||||||
|
// mount: impl AsRef<Path>,
|
||||||
|
// path: impl AsRef<Path>,
|
||||||
|
// ) -> Option<(TrashDir, bool)> {
|
||||||
|
// let mount = mount.as_ref();
|
||||||
|
// let _path = path.as_ref();
|
||||||
|
|
||||||
|
// // first, are we on the home mount?
|
||||||
|
// if mount == *HOME_MOUNT {
|
||||||
|
// return Some((HOME_TRASH.clone(), false));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // are we just copying?
|
||||||
|
// if let DeletionStrategy::Copy = self {
|
||||||
|
// return Some((HOME_TRASH.clone(), true));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // try to use the $topdir/.Trash directory
|
||||||
|
// let topdir_trash = mount.join(".Trash");
|
||||||
|
// if self.should_use_topdir_trash(&topdir_trash) {
|
||||||
|
// return Some((
|
||||||
|
// TrashDir(topdir_trash.join(utils::get_uid().to_string())),
|
||||||
|
// false,
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // try to use the $topdir/.Trash-$uid directory
|
||||||
|
// let topdir_trash_uid = mount.join(format!(".Trash-{}", utils::get_uid()));
|
||||||
|
// if self.should_use_topdir_trash_uid(&topdir_trash_uid) {
|
||||||
|
// return Some((TrashDir(topdir_trash_uid), false));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // do we have the copy option
|
||||||
|
// if let DeletionStrategy::TopdirOrCopy = self {
|
||||||
|
// return Some((HOME_TRASH.clone(), true));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// The actual deletion happens here
|
||||||
|
pub fn delete(&self, target: impl AsRef<Path>, force: bool) -> Result<()> {
|
||||||
|
let target = target.as_ref();
|
||||||
|
if !target.exists() {
|
||||||
|
bail!(Error::FileDoesntExist(target.to_path_buf()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (trash_dir, requires_copy) = self.get_target_trash();
|
||||||
|
|
||||||
|
// prompt if not suppressed
|
||||||
|
if !force {
|
||||||
|
eprint!(
|
||||||
|
"Copy file '{}' to the trash? [Y/n] ",
|
||||||
|
target.to_str().unwrap()
|
||||||
|
);
|
||||||
|
let should_copy = 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_copy {
|
||||||
|
bail!(Error::CancelledByUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// preparing metadata
|
||||||
|
let now = Local::now();
|
||||||
|
let elapsed = now.timestamp_millis();
|
||||||
|
let file_name = format!(
|
||||||
|
"{}.{}",
|
||||||
|
elapsed,
|
||||||
|
target.file_name().unwrap().to_str().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let trash_file_path = trash_dir.files_dir()?.join(&file_name);
|
||||||
|
let trash_info_path = trash_dir.info_dir()?.join(file_name + ".trashinfo");
|
||||||
|
|
||||||
|
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 trash_info_file = File::create(trash_info_path)?;
|
||||||
|
trash_info.write(&trash_info_file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the file over
|
||||||
|
if requires_copy {
|
||||||
|
utils::recursive_copy(&target, &trash_file_path)?;
|
||||||
|
fs::remove_dir_all(&target)?;
|
||||||
|
} else {
|
||||||
|
fs::rename(&target, &trash_file_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Can we use $topdir/.Trash?
|
||||||
|
///
|
||||||
|
/// 1. If it doesn't exist, don't create it.
|
||||||
|
/// 2. All users should be able to write to it
|
||||||
|
/// 3. It must have sticky-bit permissions if the filesystem supports it.
|
||||||
|
/// 4. The directory must not be a symbolic link.
|
||||||
|
fn should_use_topdir_trash(mount: impl AsRef<Path>) -> bool {
|
||||||
|
let mount = mount.as_ref();
|
||||||
|
let trash_dir = mount.join(".Trash");
|
||||||
|
|
||||||
|
if !trash_dir.exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir = match File::open(trash_dir) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
let meta = match dir.metadata() {
|
||||||
|
Ok(meta) => meta,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
if meta.file_type().is_symlink() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let perms = meta.permissions();
|
||||||
|
|
||||||
|
perms.mode() & 0o1000 > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Can we use $topdir/.Trash-uid?
|
||||||
|
|
||||||
|
fn should_use_topdir_trash_uid(path: impl AsRef<Path>) -> bool {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if !path.exists() {
|
||||||
|
match fs::create_dir(path) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,60 @@
|
||||||
pub fn restore() {
|
use std::fs;
|
||||||
// let trash = select_trash();
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::TrashDir;
|
||||||
|
|
||||||
|
#[derive(StructOpt)]
|
||||||
|
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)
|
||||||
|
#[structopt(long = "trash-dir", parse(from_os_str))]
|
||||||
|
trash_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore(options: RestoreOptions) -> Result<()> {
|
||||||
|
let trash_dir = TrashDir::from_opt(options.trash_dir);
|
||||||
|
|
||||||
|
// get list of files sorted by deletion date
|
||||||
|
let files = {
|
||||||
|
let mut files = trash_dir
|
||||||
|
.iter()
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|entry| match entry {
|
||||||
|
Ok(info) => Some(info),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("failed to get file info: {:?}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
files.sort_unstable_by_key(|info| info.deletion_date);
|
||||||
|
files
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i, info) in files.iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
"[{}]\t{}\t{}",
|
||||||
|
i,
|
||||||
|
info.deletion_date,
|
||||||
|
info.path.to_str().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.get(i).unwrap();
|
||||||
|
eprintln!("moving {:?} to {:?}", &info.deleted_path, &info.path);
|
||||||
|
fs::rename(&info.deleted_path, &info.path)?;
|
||||||
|
}
|
||||||
|
_ => eprintln!("Invalid number."),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
158
src/strategy.rs
158
src/strategy.rs
|
@ -1,158 +0,0 @@
|
||||||
//! Contains [DeletionStrategy][1], which determines how the target
|
|
||||||
//! file will actually be deleted.
|
|
||||||
//!
|
|
||||||
//! [1]: crate::strategy::DeletionStrategy
|
|
||||||
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use chrono::Local;
|
|
||||||
|
|
||||||
use crate::dir::TrashDir;
|
|
||||||
use crate::info::TrashInfo;
|
|
||||||
use crate::utils;
|
|
||||||
use crate::{HOME_MOUNT, HOME_TRASH, MOUNTS};
|
|
||||||
|
|
||||||
// TODO: implement the other ones
|
|
||||||
|
|
||||||
/// DeletionStrategy describes whether the deleting a directory will:
|
|
||||||
///
|
|
||||||
/// * Move: move the candidate files/directories to the trash directory (this requires that both the candidate and the trash directories be on the same filesystem)
|
|
||||||
/// * Copy: recursively copy the candidate files/directories to the trash directory
|
|
||||||
/// * Topdir:
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum DeletionStrategy {
|
|
||||||
Move,
|
|
||||||
Copy,
|
|
||||||
Topdir,
|
|
||||||
TopdirOrCopy,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeletionStrategy {
|
|
||||||
/// This method picks the ideal strategy
|
|
||||||
pub fn pick_strategy(trash_dir: &TrashDir) -> DeletionStrategy {
|
|
||||||
DeletionStrategy::Move
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_target_trash(
|
|
||||||
&self,
|
|
||||||
mount: impl AsRef<Path>,
|
|
||||||
path: impl AsRef<Path>,
|
|
||||||
) -> Option<(TrashDir, bool)> {
|
|
||||||
let mount = mount.as_ref();
|
|
||||||
let _path = path.as_ref();
|
|
||||||
|
|
||||||
// first, are we on the home mount?
|
|
||||||
if mount == *HOME_MOUNT {
|
|
||||||
return Some((HOME_TRASH.clone(), false));
|
|
||||||
}
|
|
||||||
|
|
||||||
// are we just copying?
|
|
||||||
if let DeletionStrategy::Copy = self {
|
|
||||||
return Some((HOME_TRASH.clone(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to use the $topdir/.Trash directory
|
|
||||||
let topdir_trash = mount.join(".Trash");
|
|
||||||
if self.should_use_topdir_trash(&topdir_trash) {
|
|
||||||
return Some((
|
|
||||||
TrashDir(topdir_trash.join(utils::get_uid().to_string())),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to use the $topdir/.Trash-$uid directory
|
|
||||||
let topdir_trash_uid = mount.join(format!(".Trash-{}", utils::get_uid()));
|
|
||||||
if self.should_use_topdir_trash_uid(&topdir_trash_uid) {
|
|
||||||
return Some((TrashDir(topdir_trash_uid), false));
|
|
||||||
}
|
|
||||||
|
|
||||||
// do we have the copy option
|
|
||||||
if let DeletionStrategy::TopdirOrCopy = self {
|
|
||||||
return Some((HOME_TRASH.clone(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_use_topdir_trash(&self, path: impl AsRef<Path>) -> bool {
|
|
||||||
let path = path.as_ref();
|
|
||||||
if !path.exists() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dir = match File::open(path) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
let meta = match dir.metadata() {
|
|
||||||
Ok(meta) => meta,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
if meta.file_type().is_symlink() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let perms = meta.permissions();
|
|
||||||
|
|
||||||
perms.mode() & 0o1000 > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_use_topdir_trash_uid(&self, path: impl AsRef<Path>) -> bool {
|
|
||||||
let path = path.as_ref();
|
|
||||||
if !path.exists() {
|
|
||||||
match fs::create_dir(path) {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(&self, target: impl AsRef<Path>) -> Result<()> {
|
|
||||||
let target = target.as_ref();
|
|
||||||
|
|
||||||
let target_mount = MOUNTS
|
|
||||||
.get_mount_point(target)
|
|
||||||
.ok_or_else(|| anyhow!("couldn't get mount point"))?;
|
|
||||||
let (trash_dir, copy) = match self.get_target_trash(target_mount, target) {
|
|
||||||
Some(x) => x,
|
|
||||||
None => bail!("no trash dir could be selected, u suck"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// preparing metadata
|
|
||||||
let now = Local::now();
|
|
||||||
let elapsed = now.timestamp_millis();
|
|
||||||
let file_name = format!(
|
|
||||||
"{}.{}",
|
|
||||||
elapsed,
|
|
||||||
target.file_name().unwrap().to_str().unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let trash_file_path = trash_dir.files_dir()?.join(&file_name);
|
|
||||||
let trash_info_path = trash_dir.info_dir()?.join(file_name + ".trashinfo");
|
|
||||||
|
|
||||||
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 trash_info_file = File::create(trash_info_path)?;
|
|
||||||
trash_info.write(&trash_info_file)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy the file over
|
|
||||||
if copy {
|
|
||||||
utils::recursive_copy(&target, &trash_file_path)?;
|
|
||||||
fs::remove_dir_all(&target)?;
|
|
||||||
} else {
|
|
||||||
fs::rename(&target, &trash_file_path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
FROM rust:1.39
|
|
||||||
WORKDIR /usr/src/garbage
|
|
||||||
COPY . .
|
|
||||||
RUN pwd
|
|
||||||
RUN ls -l
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
COPY --from=0 /usr/src/garbage/target/release/garbage .
|
|
Loading…
Reference in a new issue