garbage 0.4.3

CLI tool for interacting with the freedesktop trashcan
Documentation
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};

use anyhow::{Context, Error, Result};
use garbage_fs::Filesystem;
use walkdir::{DirEntry, WalkDir};

use crate::TrashInfo;
use crate::XDG;

/// A trash directory represented by a path.
#[derive(Clone, Debug)]
pub struct TrashDir {
  path: PathBuf,
}

impl TrashDir {
  /// Constructor for a new trash directory.
  pub fn from(path: impl AsRef<Path>) -> Self {
    let path = path.as_ref().to_path_buf();
    TrashDir { path }
  }

  /// 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::from(XDG.get_data_home().join("Trash"))
  }

  /// Create a trash directory from an optional path
  ///
  /// If the option is None, then the home trash will be selected instead.
  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())
  }

  /// Actually create the directory on disk corresponding to this trash
  /// directory
  pub fn mkdir(&self, fs: &impl Filesystem) -> Result<()> {
    let path = &self.path;
    if !fs.path_exists(&path)? {
      fs.create_dir_all(&path)?;
    }
    Ok(())
  }

  /// Returns the path to this trash directory.
  pub fn path(&self) -> &Path {
    self.path.as_ref()
  }

  /// Get the `files` directory
  pub fn files_dir(&self, fs: &impl Filesystem) -> Result<PathBuf> {
    let target = self.path.join("files");
    if !fs.path_exists(&target)? {
      fs.create_dir_all(&target)
        .with_context(|| format!("Could not create directory {:?}", target))?;
    }
    Ok(target)
  }

  /// Get the `info` directory
  pub fn info_dir(&self, fs: &impl Filesystem) -> Result<PathBuf> {
    let target = self.path.join("info");
    if !fs.path_exists(&target)? {
      fs.create_dir_all(&target)?;
    }
    Ok(target)
  }

  /// Get the `info` directory
  pub fn check_info_dir(
    &self,
    fs: &impl Filesystem,
  ) -> Result<Option<PathBuf>> {
    let target = self.path.join("info");
    if !fs.path_exists(&target)? {
      Ok(None)
    } else {
      Ok(Some(target))
    }
  }

  /// Iterate over trash infos within this trash directory
  pub fn iter(&self, fs: &impl Filesystem) -> Result<TrashDirIter> {
    let iter = WalkDir::new(&self.info_dir(fs)?)
      .contents_first(true)
      .into_iter()
      .filter_entry(|entry| match entry.path().extension() {
        Some(x) => x == "trashinfo",
        _ => false,
      });
    Ok(TrashDirIter(self.path.clone(), Box::new(iter)))
  }
}

pub struct TrashDirIter(
  PathBuf,
  Box<dyn Iterator<Item = walkdir::Result<DirEntry>>>,
);

impl Iterator for TrashDirIter {
  type Item = Result<TrashInfo>;

  fn next(&mut self) -> Option<Self::Item> {
    let entry = {
      let mut entry;
      loop {
        entry = match self.1.next() {
          Some(Ok(entry)) => entry,
          Some(Err(err)) => {
            return Some(Err(
              Error::from(err).context("Could not open next entry."),
            ));
          }
          None => return None,
        };
        if entry.path().is_dir() {
          continue;
        }
        break;
      }
      entry
    };

    let name = match entry.path().file_name() {
      Some(name) => name,
      None => return None,
    };
    let deleted_path = if !name.as_bytes().ends_with(b".trashinfo") {
      return self.next();
    } else {
      self.0.join("files").join(OsStr::from_bytes(
        &name.as_bytes()[..name.len() - b".trashinfo".len()],
      ))
    };

    Some(
      TrashInfo::from_files(entry.path(), deleted_path)
        .map_err(|err| err.into()),
    )
  }
}