garbage 0.4.3

CLI tool for interacting with the freedesktop trashcan
Documentation
pub mod strategy;

use std::env;
use std::path::PathBuf;

use anyhow::Result;
use clap::ValueHint;
use garbage_fs::Filesystem;

use crate::ops::put::strategy::DeletionStrategy;
use crate::utils;
use crate::TrashDir;

/// Options to pass to put
#[derive(Parser)]
pub struct PutOptions {
  /// The target path to be trashed
  #[clap(value_parser, value_hint(ValueHint::AnyPath))]
  paths: Vec<PathBuf>,

  /// Don't actually move anything, just print the files to be removed
  #[clap(long = "dry")]
  dry: bool,

  /// Prompt before every removal
  #[clap(long = "prompt", short = 'i')]
  prompt: bool,

  /// Trashes directories recursively (ignored)
  #[clap(long = "recursive", short = 'r')]
  _recursive: bool,

  /// Suppress prompts/messages
  #[clap(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.
  #[clap(long = "trash-dir", value_parser, value_hint(ValueHint::DirPath))]
  trash_dir: Option<PathBuf>,
}

/// Throw some files into the trash.
pub fn put(options: PutOptions, fs: &impl Filesystem) -> Result<()> {
  let mut errors = vec![];

  for path in options.paths.iter() {
    let abs_path = utils::into_absolute(&path)?;

    if !options.force && !fs.symlink_exists(&abs_path)? {
      errors.push(anyhow!("Path {:?} doesn't exist.", path));
      continue;
    }

    // don't allow deleting '.' or '..'
    let current_directory = env::current_dir()?;
    let parent_directory = current_directory.parent();

    trace!(
      ?current_directory,
      ?parent_directory,
      "Checking if we are trying to delete current or parent",
    );

    if abs_path == current_directory.as_path()
      || (current_directory.parent().is_some()
        && abs_path == current_directory.parent().unwrap())
    {
      errors.push(anyhow!("Refusing to delete . or .., skipping..."));
      continue;
    }

    // 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(&abs_path, fs)?
    };
    // println!("Strategy: {:?}", strategy);

    debug!(?path, ?strategy, "Chosen strategy.");

    if options.dry {
      eprintln!("Dry-deleting {:?} with strategy {:?}", path, strategy);
    } else if let Err(err) = strategy.delete(abs_path, &options, fs) {
      eprintln!("{}", err);
    }
  }

  if !errors.is_empty() {
    if errors.len() == 1 {
      bail!("{}", errors[0]);
    }

    for error in errors {
      error!("{}", error);
    }
    bail!("Multiple errors occurred.");
  }

  Ok(())
}