garbage 0.4.3

CLI tool for interacting with the freedesktop trashcan
Documentation
use std::env;
use std::fs;
use std::path::PathBuf;

use anyhow::Context;
use anyhow::Result;
use chrono::{Duration, Local};
use clap::ValueHint;
use garbage_fs::Filesystem;

use crate::TrashDir;

/// Options to pass to empty
#[derive(Parser)]
pub struct EmptyOptions {
  /// Only list the files that are to be deleted, without
  /// actually deleting anything.
  #[clap(long = "dry")]
  pub dry: bool,

  /// Delete all files older than (this number) of integer days.
  /// Removes everything if this option is not specified
  #[clap(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)
  #[clap(long = "trash-dir", value_parser, value_hint(ValueHint::DirPath))]
  trash_dir: Option<PathBuf>,

  /// Delete all files in the trash (by default, only files in the current
  /// directory are listed)
  #[clap(short = 'a', long = "all")]
  all: bool,
}

/// Actually delete files in the trash.
pub fn empty(options: EmptyOptions, fs: &impl Filesystem) -> Result<()> {
  let trash_dir = TrashDir::from_opt(options.trash_dir.as_ref());

  // cutoff date
  let cutoff = if let Some(days) = options.days {
    Local::now() - Duration::days(days.into())
  } else {
    Local::now()
  };

  let current_dir = env::current_dir()?;
  trash_dir
    .iter(fs)?
    .collect::<Result<Vec<_>>>()?
    .into_iter()
    // ignore files that were deleted after the cutoff (younger)
    .filter(|info| info.deletion_date <= cutoff)
    .filter(|info| options.all || info.path.starts_with(&current_dir))
    .map(|info| -> Result<_> {
      if options.dry {
        println!("Deleting {:?}", info.path);
      } else {
        if info.deleted_path.exists() {
          if info.deleted_path.is_dir() {
            if let Err(err) = fs::remove_dir_all(&info.deleted_path) {
              error!(?info.deleted_path, "Failed to remove original directory.");
              return Err(err.into());
            }
          } else {
            if let Err(err) = fs::remove_file(&info.deleted_path) {
              error!(?info.deleted_path, "Failed to remove original file.");
              return Err(err.into());
            }
          }
        }

        fs::remove_file(&info.info_path).with_context(|| {
          format!("Failed to remove info file {:?}.", info.info_path.display())
        })?;
      }

      Ok(())
    })
    .collect::<Result<_>>()?;

  Ok(())
}