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]]
|
||||
name = "garbage"
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"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)",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "garbage"
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
authors = ["Michael Zhang <iptq@protonmail.com>"]
|
||||
description = "cli tool for interacting with the freedesktop trashcan"
|
||||
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);
|
||||
|
||||
impl TrashDir {
|
||||
pub fn from(path: impl AsRef<Path>) -> Self {
|
||||
TrashDir(path.as_ref().to_path_buf())
|
||||
}
|
||||
|
||||
/// Gets your user's "home" trash directory.
|
||||
///
|
||||
/// According to Trash spec v1.0:
|
||||
///
|
||||
/// > 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.
|
||||
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.
|
||||
|
|
|
@ -17,7 +17,6 @@ mod errors;
|
|||
mod info;
|
||||
mod mounts;
|
||||
pub mod ops;
|
||||
mod strategy;
|
||||
mod utils;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
|
89
src/main.rs
89
src/main.rs
|
@ -2,101 +2,46 @@
|
|||
|
||||
extern crate anyhow;
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use garbage::{
|
||||
ops::{self, EmptyOptions},
|
||||
TrashDir,
|
||||
};
|
||||
use garbage::ops::{self, EmptyOptions, ListOptions, PutOptions, RestoreOptions};
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum Command {
|
||||
/// Empty a trash directory.
|
||||
#[structopt(name = "empty")]
|
||||
Empty(EmptyOptions),
|
||||
|
||||
/// List the contents of a trash directory.
|
||||
#[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")]
|
||||
Put {
|
||||
/// 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(PutOptions),
|
||||
|
||||
/// Restores files from the trash.
|
||||
#[structopt(name = "restore")]
|
||||
Restore,
|
||||
Restore(RestoreOptions),
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let cmd = Command::from_args();
|
||||
match cmd {
|
||||
Command::Empty(options) => ops::empty(options),
|
||||
Command::List => {
|
||||
ops::list();
|
||||
Ok(())
|
||||
}
|
||||
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(())
|
||||
}
|
||||
Command::List(options) => ops::list(options),
|
||||
Command::Put(options) => ops::put(options),
|
||||
Command::Restore(options) => ops::restore(options),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
match run() {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
|
|
|
@ -11,22 +11,23 @@ use crate::TrashDir;
|
|||
pub struct EmptyOptions {
|
||||
/// Only list the files that are to be deleted, without
|
||||
/// actually deleting anything.
|
||||
#[structopt(long = "dry")]
|
||||
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
|
||||
#[structopt(long = "days")]
|
||||
days: Option<u32>,
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// Actually delete files in the trash.
|
||||
pub fn empty(options: EmptyOptions) -> Result<()> {
|
||||
let trash_dir = options
|
||||
.trash_dir
|
||||
.map(TrashDir)
|
||||
.unwrap_or_else(|| TrashDir::get_home_trash());
|
||||
let trash_dir = TrashDir::from_opt(options.trash_dir);
|
||||
|
||||
// cutoff date
|
||||
let cutoff = if let Some(days) = options.days {
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::TrashDir;
|
||||
|
||||
pub fn list() {
|
||||
let home_trash = TrashDir::get_home_trash();
|
||||
let mut files = home_trash
|
||||
#[derive(StructOpt)]
|
||||
pub struct ListOptions {
|
||||
/// 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()
|
||||
.unwrap()
|
||||
.filter_map(|entry| match entry {
|
||||
|
@ -17,4 +30,6 @@ pub fn list() {
|
|||
for info in files {
|
||||
println!("{}\t{}", info.deletion_date, info.path.to_str().unwrap());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6,6 +6,6 @@ mod put;
|
|||
mod restore;
|
||||
|
||||
pub use self::empty::{empty, EmptyOptions};
|
||||
pub use self::list::list;
|
||||
pub use self::put::put;
|
||||
pub use self::restore::restore;
|
||||
pub use self::list::{list, ListOptions};
|
||||
pub use self::put::{put, PutOptions};
|
||||
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::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 chrono::Local;
|
||||
|
||||
use crate::strategy::DeletionStrategy;
|
||||
use crate::HOME_TRASH;
|
||||
use crate::utils;
|
||||
use crate::{TrashDir, TrashInfo};
|
||||
use crate::{HOME_MOUNT, HOME_TRASH, MOUNTS};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("File {0} doesn't exist.")]
|
||||
FileDoesntExist(PathBuf),
|
||||
|
||||
#[error("Refusing to remove '.' or '..', skipping...")]
|
||||
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.
|
||||
pub fn put(paths: Vec<PathBuf>, _recursive: bool, _force: bool) -> Result<()> {
|
||||
let strategy = DeletionStrategy::pick_strategy(&HOME_TRASH);
|
||||
for path in paths {
|
||||
pub fn put(options: PutOptions) -> Result<()> {
|
||||
for path in options.paths.iter() {
|
||||
// don't allow deleting '.' or '..'
|
||||
let current_dir = env::current_dir()?;
|
||||
ensure!(
|
||||
!(path == current_dir
|
||||
!(path.as_path() == current_dir.as_path()
|
||||
|| (current_dir.parent().is_some() && path == current_dir.parent().unwrap())),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// let trash = select_trash();
|
||||
use std::fs;
|
||||
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