~iptq/garbage

a7bd15b0d9ba42835c48db04be1bdba975dc1ee9 — Michael Zhang 1 year, 8 months ago 70d983b
ouais
M src/dir.rs => src/dir.rs +9 -0
@@ 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()
    }

M src/errors.rs => src/errors.rs +4 -0
@@ 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,

M src/info.rs => src/info.rs +10 -3
@@ 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())?;

M src/lib.rs => src/lib.rs +8 -2
@@ 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();
}

M src/main.rs => src/main.rs +7 -29
@@ 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 {

M src/ops/empty.rs => src/ops/empty.rs +28 -5
@@ 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)?;

A src/ops/list.rs => src/ops/list.rs +20 -0
@@ 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());
    }
}

M src/ops/mod.rs => src/ops/mod.rs +7 -1
@@ 1,5 1,11 @@
//! Operations that garbage can do.

mod empty;
mod list;
mod put;
mod restore;

pub use self::empty::empty;
pub use self::empty::{empty, EmptyOptions};
pub use self::list::list;
pub use self::put::put;
pub use self::restore::restore;

M src/ops/put.rs => src/ops/put.rs +11 -145
@@ 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 {
        if let Err(err) = strategy.delete(path) {
            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())),
            !(path == current_dir
                || (current_dir.parent().is_some() && path == 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)?;
        if let Err(err) = strategy.delete(path) {
            eprintln!("{}", err);
        }

        Ok(())
    }

    Ok(())
}

A src/ops/restore.rs => src/ops/restore.rs +3 -0
@@ 0,0 1,3 @@
pub fn restore() {
    // let trash = select_trash();
}

A src/strategy.rs => src/strategy.rs +158 -0
@@ 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(())
    }
}

M src/utils.rs => src/utils.rs +1 -0
@@ 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();