This commit is contained in:
Michael Zhang 2020-03-03 22:48:06 -06:00
parent 70d983be98
commit a7bd15b0d9
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
12 changed files with 271 additions and 190 deletions

View file

@ -7,14 +7,23 @@ use crate::Error;
use crate::TrashInfo;
use crate::XDG;
/// A trash directory represented by a path.
#[derive(Clone, Debug)]
pub struct TrashDir(pub PathBuf);
impl TrashDir {
/// 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;
/// > $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"))
}
/// Returns the path to this trash directory.
pub fn path(&self) -> &Path {
self.0.as_ref()
}

View file

@ -1,4 +1,6 @@
/// All errors that could happen
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
@ -10,7 +12,9 @@ pub enum Error {
ParseDate(#[from] chrono::format::ParseError),
}
/// Errors related to .trashinfo files
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum TrashInfoError {
#[error("Missing [TrashInfo] header")]
MissingHeader,

View file

@ -13,27 +13,33 @@ lazy_static! {
const DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
/// .trashinfo Data
#[derive(Debug)]
pub struct TrashInfo {
/// The original path where this file was located before it was deleted.
pub path: PathBuf,
/// The date the file was deleted.
pub deletion_date: DateTime<Local>,
/// The location of the deleted file after deletion.
pub deleted_path: PathBuf,
/// The location of the `info` description file.
pub info_path: PathBuf,
}
impl TrashInfo {
/// Create a new TrashInfo based on the .trashinfo path and the deleted file path
///
/// This is useful for reading files from the Trash.
pub fn from_files(
info_path: impl AsRef<Path>,
deleted_path: impl AsRef<Path>,
) -> Result<Self, Error> {
let path = info_path.as_ref();
let info_path = path.to_path_buf();
let info_path = info_path.as_ref().to_path_buf();
let deleted_path = deleted_path.as_ref().to_path_buf();
let file = File::open(path)?;
let file = File::open(&info_path)?;
let reader = BufReader::new(file);
let mut path = None;
@ -88,6 +94,7 @@ impl TrashInfo {
})
}
/// Write the current TrashInfo into a .trashinfo file.
pub fn write(&self, mut out: impl Write) -> Result<(), io::Error> {
writeln!(out, "[Trash Info]")?;
writeln!(out, "Path={}", self.path.to_str().unwrap())?;

View file

@ -1,7 +1,11 @@
#![deny(warnings)]
//! garbage
#![warn(missing_docs)]
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate structopt;
extern crate log;
#[macro_use]
extern crate anyhow;
@ -13,6 +17,7 @@ mod errors;
mod info;
mod mounts;
pub mod ops;
mod strategy;
mod utils;
use std::path::PathBuf;
@ -25,8 +30,9 @@ pub use crate::info::TrashInfo;
use crate::mounts::Mounts;
lazy_static! {
static ref XDG: BaseDirectories = BaseDirectories::new().unwrap();
#[allow(missing_docs)]
pub static ref MOUNTS: Mounts = Mounts::read().unwrap();
static ref XDG: BaseDirectories = BaseDirectories::new().unwrap();
static ref HOME_TRASH: TrashDir = TrashDir::get_home_trash();
static ref HOME_MOUNT: PathBuf = MOUNTS.get_mount_point(HOME_TRASH.path()).unwrap();
}

View file

@ -7,23 +7,16 @@ use std::io;
use std::path::PathBuf;
use anyhow::Result;
use garbage::{
ops::{self, EmptyOptions},
TrashDir,
};
use structopt::StructOpt;
use garbage::*;
#[derive(StructOpt)]
enum Command {
#[structopt(name = "empty")]
Empty {
/// Only list the files that are to be deleted, without
/// actually deleting anything.
#[structopt(long = "dry")]
dry: bool,
/// Delete all files older than (this number) of days.
/// Removes everything if this option is not specified
days: Option<u32>,
},
Empty(EmptyOptions),
#[structopt(name = "list")]
List,
@ -52,24 +45,9 @@ fn run() -> Result<()> {
let cmd = Command::from_args();
match cmd {
Command::Empty { dry, days } => ops::empty(dry, days),
Command::Empty(options) => ops::empty(options),
Command::List => {
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 info in files {
println!("{}\t{}", info.deletion_date, info.path.to_str().unwrap());
}
ops::list();
Ok(())
}
Command::Put {

View file

@ -1,29 +1,52 @@
use std::fs;
use std::path::PathBuf;
use anyhow::Result;
use chrono::{Duration, Local};
use crate::TrashDir;
pub fn empty(dry: bool, days: Option<u32>) -> Result<()> {
let home_trash = TrashDir::get_home_trash();
let cutoff = if let Some(days) = days {
/// Options to pass to empty
#[derive(StructOpt)]
pub struct EmptyOptions {
/// Only list the files that are to be deleted, without
/// actually deleting anything.
pub dry: bool,
/// Delete all files older than (this number) of days.
/// Removes everything if this option is not specified
days: Option<u32>,
/// The path to the trash directory to empty.
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());
// cutoff date
let cutoff = if let Some(days) = options.days {
Local::now() - Duration::days(days.into())
} else {
Local::now()
};
for file in home_trash.iter()? {
for file in trash_dir.iter()? {
let file = file?;
// ignore files that were deleted after the cutoff (younger)
let ignore = file.deletion_date > cutoff;
if !ignore {
if dry {
if options.dry {
println!("{:?}", file.path);
} else {
fs::remove_file(file.info_path)?;
if file.deleted_path.exists() {
if file.deleted_path.is_dir() {
fs::remove_dir_all(file.deleted_path)?;

20
src/ops/list.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::TrashDir;
pub fn list() {
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 info in files {
println!("{}\t{}", info.deletion_date, info.path.to_str().unwrap());
}
}

View file

@ -1,5 +1,11 @@
mod empty;
mod put;
//! Operations that garbage can do.
pub use self::empty::empty;
mod empty;
mod list;
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;

View file

@ -1,15 +1,10 @@
use std::env;
use std::fs::{self, File};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use anyhow::Result;
use chrono::{Local};
use crate::utils;
use crate::TrashDir;
use crate::TrashInfo;
use crate::{HOME_MOUNT, HOME_TRASH, MOUNTS};
use crate::strategy::DeletionStrategy;
use crate::HOME_TRASH;
#[derive(Debug, Error)]
pub enum Error {
@ -17,151 +12,22 @@ pub enum Error {
CannotTrashDotDirs,
}
/// Throw some files into the trash.
pub fn put(paths: Vec<PathBuf>, _recursive: bool, _force: bool) -> Result<()> {
let strategy = DeletionStrategy::Copy;
let strategy = DeletionStrategy::pick_strategy(&HOME_TRASH);
for path in paths {
// don't allow deleting '.' or '..'
let current_dir = env::current_dir()?;
ensure!(
!(path == current_dir
|| (current_dir.parent().is_some() && path == current_dir.parent().unwrap())),
Error::CannotTrashDotDirs
);
if let Err(err) = strategy.delete(path) {
eprintln!("{:?}", err);
eprintln!("{}", err);
}
}
Ok(())
}
// TODO: implement the other ones
#[allow(dead_code)]
pub enum DeletionStrategy {
Copy,
Topdir,
TopdirOrCopy,
}
impl DeletionStrategy {
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();
// don't allow deleting '.' or '..'
let current_dir = env::current_dir()?;
ensure!(
!(target == current_dir
|| (current_dir.parent().is_some() && target == current_dir.parent().unwrap())),
Error::CannotTrashDotDirs
);
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(())
}
}

3
src/ops/restore.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn restore() {
// let trash = select_trash();
}

158
src/strategy.rs Normal file
View file

@ -0,0 +1,158 @@
//! 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(())
}
}

View file

@ -19,6 +19,7 @@ pub fn get_uid() -> u64 {
unsafe { libc::getuid().into() }
}
/// This function recursively copies all the contents of src into dst.
pub fn recursive_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
let src = src.as_ref();
let dst = dst.as_ref();