use crate::background::deleted::DeletedSearch;
use crate::config::generate::{
DeletedMode,
ExecMode,
};
use crate::data::paths::{
BasicDirEntryInfo,
PathData,
};
use crate::display::wrapper::DisplayWrapper;
use crate::library::results::{
HttmError,
HttmResult,
};
use crate::library::utility::{
HttmIsDir,
print_output_buf,
};
use crate::lookup::deleted::DeletedFiles;
use crate::{
GLOBAL_CONFIG,
VersionsMap,
exit_error,
};
use hashbrown::HashSet;
use rayon::{
Scope,
ThreadPool,
};
use skim::SkimItem;
use skim::prelude::*;
use std::fs::read_dir;
use std::hash::Hash;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
#[derive(Clone, Copy)]
pub enum PathProvenance {
FromLiveDataset,
IsPhantom,
}
pub struct EntriesPartitioned {
vec_dirs: Vec<BasicDirEntryInfo>,
vec_files: Vec<BasicDirEntryInfo>,
}
pub struct RecursiveSearch<'a> {
opt_skim_tx: Option<&'a SkimItemSender>,
hangup: Arc<AtomicBool>,
not_previously_displayed_cache: HashSet<UniqueInode>,
}
impl<'a> RecursiveSearch<'a> {
pub fn new(opt_skim_tx: Option<&'a SkimItemSender>, hangup: Arc<AtomicBool>) -> Self {
let not_previously_displayed_cache: HashSet<UniqueInode> = HashSet::new();
Self {
opt_skim_tx,
hangup,
not_previously_displayed_cache,
}
}
pub fn exec(&mut self, requested_dir: &'a Path) {
match GLOBAL_CONFIG.opt_deleted_mode {
Some(_) => {
let pool: ThreadPool = rayon::ThreadPoolBuilder::new()
.build()
.expect("Could not initialize rayon thread pool for recursive deleted search");
pool.in_place_scope(|deleted_scope| {
self.run_loop(requested_dir, Some(deleted_scope))
.unwrap_or_else(|err| exit_error(err));
})
}
None => {
self.run_loop(requested_dir, None)
.unwrap_or_else(|err| exit_error(err));
}
}
}
fn spawn_deleted_search(&self, requested_dir: &'a Path, deleted_scope: &Scope<'_>) {
let search = DeletedSearch::new(self.opt_skim_tx.cloned(), self.hangup.clone());
search.spawn(requested_dir, deleted_scope);
}
fn add_dot_entries(&'a self, requested_dir: &'a Path) -> HttmResult<()> {
let dot_as_entry = BasicDirEntryInfo::new(requested_dir, None);
let mut initial_vec_dirs = vec![dot_as_entry];
if let Some(parent) = requested_dir.parent() {
let double_dot_as_entry = BasicDirEntryInfo::new(parent, None);
initial_vec_dirs.push(double_dot_as_entry)
}
let entries = EntriesPartitioned {
vec_dirs: initial_vec_dirs,
vec_files: Vec::new(),
};
self.combine_and_deliver_entries(entries)?;
Ok(())
}
fn entry_not_previously_displayed(&mut self, entry: &BasicDirEntryInfo) -> bool {
let Some(file_id) = UniqueInode::new(entry) else {
return false;
};
self.not_previously_displayed_cache.insert(file_id)
}
}
impl CommonSearch for RecursiveSearch<'_> {
fn hangup(&self) -> bool {
self.hangup.load(Ordering::Relaxed)
}
fn entry_is_dir(&mut self, entry: &BasicDirEntryInfo) -> bool {
if GLOBAL_CONFIG.opt_no_traverse {
if let Some(file_type) = entry.opt_filetype() {
return file_type.is_dir();
}
}
entry.httm_is_dir::<BasicDirEntryInfo>() && self.entry_not_previously_displayed(entry)
}
fn run_loop(
&mut self,
requested_dir: &Path,
opt_deleted_scope: Option<&Scope>,
) -> HttmResult<()> {
self.add_dot_entries(requested_dir)?;
let mut queue: Vec<BasicDirEntryInfo> = Vec::new();
self.enter_directory(requested_dir, &mut queue)?;
if let Some(deleted_scope) = opt_deleted_scope {
self.spawn_deleted_search(requested_dir, deleted_scope);
}
if GLOBAL_CONFIG.opt_recursive {
while let Some(item) = queue.pop() {
if self.hangup() {
break;
}
if let Some(deleted_scope) = opt_deleted_scope {
self.spawn_deleted_search(&item.path(), deleted_scope);
}
let _ = self.enter_directory(item.path(), &mut queue);
}
}
Ok(())
}
fn opt_sender(&self) -> Option<&SkimItemSender> {
self.opt_skim_tx
}
fn path_provenance(&self) -> PathProvenance {
PathProvenance::FromLiveDataset
}
}
pub trait CommonSearch {
fn hangup(&self) -> bool;
fn entry_is_dir(&mut self, basic_dir_entry: &BasicDirEntryInfo) -> bool;
fn run_loop(
&mut self,
requested_dir: &Path,
opt_deleted_scope: Option<&Scope>,
) -> HttmResult<()>;
fn opt_sender(&self) -> Option<&SkimItemSender>;
fn path_provenance(&self) -> PathProvenance;
#[inline(always)]
fn enter_directory(
&mut self,
requested_dir: &Path,
queue: &mut Vec<BasicDirEntryInfo>,
) -> HttmResult<()>
where
Self: Sized,
{
if self.hangup() {
return Ok(());
}
let entries = self.entries_partitioned(requested_dir)?;
let mut vec_dirs = self.combine_and_deliver_entries(entries)?;
queue.append(&mut vec_dirs);
Ok(())
}
fn entries_partitioned(&mut self, requested_dir: &Path) -> HttmResult<EntriesPartitioned> {
let (vec_dirs, vec_files) = match self.path_provenance() {
PathProvenance::FromLiveDataset => {
read_dir(requested_dir)?
.flatten()
.map(|dir_entry| BasicDirEntryInfo::from(dir_entry))
.filter(|entry: &BasicDirEntryInfo| entry.recursive_search_filter())
.partition(|entry| self.entry_is_dir(entry))
}
PathProvenance::IsPhantom => {
DeletedFiles::new(requested_dir)
.into_inner()
.into_iter()
.partition(|pseudo_entry| self.entry_is_dir(pseudo_entry))
}
};
Ok(EntriesPartitioned {
vec_dirs,
vec_files,
})
}
#[inline(always)]
fn combine_and_deliver_entries(
&self,
entries: EntriesPartitioned,
) -> HttmResult<Vec<BasicDirEntryInfo>> {
let EntriesPartitioned {
vec_dirs,
vec_files,
} = entries;
let entries_ready_to_send = match self.path_provenance() {
PathProvenance::FromLiveDataset
if matches!(GLOBAL_CONFIG.opt_deleted_mode, Some(DeletedMode::Only))
|| matches!(
GLOBAL_CONFIG.exec_mode,
ExecMode::NonInteractiveRecursive(_)
) =>
{
Vec::new()
}
PathProvenance::FromLiveDataset | PathProvenance::IsPhantom => {
let mut combined = vec_files;
combined.extend_from_slice(&vec_dirs);
combined
}
};
self.display_or_transmit(entries_ready_to_send)?;
Ok(vec_dirs)
}
fn display_or_transmit(&self, combined_entries: Vec<BasicDirEntryInfo>) -> HttmResult<()> {
match &GLOBAL_CONFIG.exec_mode {
ExecMode::Interactive(_) => self.transmit(combined_entries)?,
ExecMode::NonInteractiveRecursive(progress_bar) if combined_entries.is_empty() => {
if !GLOBAL_CONFIG.opt_recursive {
eprintln!(
"NOTICE: httm could not find any deleted files at this directory level. \
Perhaps try specifying a deleted mode in combination with \"--recursive\"."
);
return Ok(());
}
progress_bar.tick();
}
ExecMode::NonInteractiveRecursive(_) => {
Self::display(combined_entries)?;
if GLOBAL_CONFIG.opt_recursive {
eprintln!();
}
}
_ => unreachable!(),
}
Ok(())
}
fn transmit(&self, combined_entries: Vec<BasicDirEntryInfo>) -> HttmResult<()> {
let vec = combined_entries
.into_iter()
.map(|basic_dir_entry_info| {
let item: Arc<dyn SkimItem> = Arc::new(
basic_dir_entry_info.into_selection_candidate(&self.path_provenance()),
);
item
})
.collect();
self.opt_sender()
.expect("Sender must be Some in any interactive mode")
.send(vec)
.map_err(std::convert::Into::into)
}
fn display(combined_entries: Vec<BasicDirEntryInfo>) -> HttmResult<()> {
let pseudo_live_set: Vec<PathData> =
combined_entries.into_iter().map(PathData::from).collect();
let versions_map = VersionsMap::new(&GLOBAL_CONFIG, &pseudo_live_set)?;
let output_buf = DisplayWrapper::from(&GLOBAL_CONFIG, versions_map).to_string();
print_output_buf(&output_buf)
}
}
pub struct UniqueInode {
ino: u64,
dev: u64,
}
impl UniqueInode {
fn new(entry: &BasicDirEntryInfo) -> Option<Self> {
if entry.opt_filetype().is_some_and(|ft| ft.is_symlink()) {
return entry.path().metadata().ok().map(|md| Self {
ino: md.ino(),
dev: md.dev(),
});
}
entry.opt_metadata().map(|md| Self {
ino: md.ino(),
dev: md.dev(),
})
}
}
impl PartialEq for UniqueInode {
fn eq(&self, other: &Self) -> bool {
self.ino == other.ino && self.dev == other.dev
}
}
impl Eq for UniqueInode {}
impl Hash for UniqueInode {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.ino.hash(state);
self.dev.hash(state);
}
}
pub struct NonInteractiveRecursiveWrapper;
impl NonInteractiveRecursiveWrapper {
#[allow(unused_variables)]
pub fn exec() -> HttmResult<()> {
let opt_skim_tx = None;
let hangup = Arc::new(AtomicBool::new(false));
match &GLOBAL_CONFIG.opt_requested_dir {
Some(requested_dir) => {
RecursiveSearch::new(opt_skim_tx, hangup).exec(&requested_dir);
}
None => {
return HttmError::new(
"requested_dir should never be None in Display Recursive mode",
)
.into();
}
}
Ok(())
}
}