use std::collections::VecDeque;
use std::{fs::read_dir, path::Path, sync::Arc};
use once_cell::unsync::OnceCell;
use rayon::{prelude::*, Scope, ThreadPool};
use skim::prelude::*;
use crate::config::generate::{Config, DeletedMode, ExecMode};
use crate::data::paths::{BasicDirEntryInfo, PathData};
use crate::display::primary::display_exec;
use crate::exec::interactive::SelectionCandidate;
use crate::library::results::{HttmError, HttmResult};
use crate::library::utility::{httm_is_dir, is_channel_closed, print_output_buf, HttmIsDir, Never};
use crate::lookup::deleted::deleted_lookup_exec;
use crate::lookup::versions::versions_lookup_exec;
use crate::{BTRFS_SNAPPER_HIDDEN_DIRECTORY, ZFS_HIDDEN_DIRECTORY};
#[allow(unused_variables)]
pub fn display_recursive_wrapper(config: Arc<Config>) -> HttmResult<()> {
let (dummy_skim_tx_item, _): (SkimItemSender, SkimItemReceiver) = unbounded();
let (hangup_tx, hangup_rx): (Sender<Never>, Receiver<Never>) = bounded(0);
let config_clone = config.clone();
match &config.opt_requested_dir {
Some(requested_dir) => {
recursive_exec(
config_clone,
&requested_dir.path_buf,
dummy_skim_tx_item,
hangup_rx,
)?;
}
None => {
return Err(HttmError::new(
"requested_dir should never be None in Display Recursive mode",
)
.into())
}
}
Ok(())
}
pub fn recursive_exec(
config: Arc<Config>,
requested_dir: &Path,
skim_tx_item: SkimItemSender,
hangup_rx: Receiver<Never>,
) -> HttmResult<()> {
const DEFAULT_STACK_SIZE: usize = 1_048_576;
let pool: ThreadPool = rayon::ThreadPoolBuilder::new()
.stack_size(DEFAULT_STACK_SIZE)
.build()
.expect("Could not initialize rayon threadpool for recursive deleted search");
pool.in_place_scope(|deleted_scope| {
iterative_enumeration(
config.clone(),
requested_dir,
deleted_scope,
&skim_tx_item,
&hangup_rx,
)
.unwrap_or_else(|error| {
eprintln!("Error: {}", error);
std::process::exit(1)
});
});
Ok(())
}
fn iterative_enumeration(
config: Arc<Config>,
requested_dir: &Path,
deleted_scope: &Scope,
skim_tx_item: &SkimItemSender,
hangup_rx: &Receiver<Never>,
) -> HttmResult<()> {
let mut queue: VecDeque<BasicDirEntryInfo> = enumerate_live(
config.clone(),
requested_dir,
deleted_scope,
skim_tx_item,
hangup_rx,
)?
.into();
if config.opt_recursive {
while let Some(item) = queue.pop_back() {
if let Ok(vec_dirs) = enumerate_live(
config.clone(),
&item.path,
deleted_scope,
skim_tx_item,
hangup_rx,
) {
queue.extend(vec_dirs.into_iter())
}
}
}
Ok(())
}
fn enumerate_live(
config: Arc<Config>,
requested_dir: &Path,
deleted_scope: &Scope,
skim_tx_item: &SkimItemSender,
hangup_rx: &Receiver<Never>,
) -> HttmResult<Vec<BasicDirEntryInfo>> {
let (vec_dirs, vec_files): (Vec<BasicDirEntryInfo>, Vec<BasicDirEntryInfo>) =
get_entries_partitioned(config.as_ref(), requested_dir)?;
combine_and_send_entries(
config.clone(),
vec_files,
&vec_dirs,
false,
requested_dir,
skim_tx_item,
)?;
spawn_deleted(
config,
requested_dir,
deleted_scope,
skim_tx_item,
hangup_rx,
);
Ok(vec_dirs)
}
fn combine_and_send_entries(
config: Arc<Config>,
vec_files: Vec<BasicDirEntryInfo>,
vec_dirs: &[BasicDirEntryInfo],
is_phantom: bool,
requested_dir: &Path,
skim_tx_item: &SkimItemSender,
) -> HttmResult<()> {
let mut combined = vec_files;
combined.extend_from_slice(vec_dirs);
let entries = if is_phantom {
get_pseudo_live_versions(combined, requested_dir)
} else {
match config.deleted_mode {
Some(DeletedMode::Only) => Vec::new(),
Some(DeletedMode::DepthOfOne) | Some(DeletedMode::Enabled) | None => {
if matches!(config.exec_mode, ExecMode::DisplayRecursive(_)) {
Vec::new()
} else {
combined
}
}
}
};
display_or_transmit(config, entries, is_phantom, skim_tx_item)?;
Ok(())
}
fn spawn_deleted(
config: Arc<Config>,
requested_dir: &Path,
deleted_scope: &Scope,
skim_tx_item: &SkimItemSender,
hangup_rx: &Receiver<Never>,
) {
match config.deleted_mode {
Some(_) => {
let requested_dir_clone = requested_dir.to_path_buf();
let skim_tx_item_clone = skim_tx_item.clone();
let hangup_rx_clone = hangup_rx.clone();
deleted_scope.spawn(move |_| {
let _ = enumerate_deleted(
config,
&requested_dir_clone,
&skim_tx_item_clone,
&hangup_rx_clone,
);
});
}
None => (),
}
}
fn get_entries_partitioned(
config: &Config,
requested_dir: &Path,
) -> HttmResult<(Vec<BasicDirEntryInfo>, Vec<BasicDirEntryInfo>)> {
let (vec_dirs, vec_files) = read_dir(&requested_dir)?
.flatten()
.map(|dir_entry| BasicDirEntryInfo::from(&dir_entry))
.filter(|entry| {
if config.opt_no_filter {
return true;
} else if let Ok(file_type) = entry.get_filetype() {
if file_type.is_dir() {
return !is_filter_dir(config, entry);
}
}
true
})
.partition(|entry| recursive_is_entry_dir(config, entry));
Ok((vec_dirs, vec_files))
}
fn recursive_is_entry_dir(config: &Config, entry: &BasicDirEntryInfo) -> bool {
if config.opt_no_traverse {
if let Ok(file_type) = entry.get_filetype() {
return file_type.is_dir();
}
}
httm_is_dir(entry)
}
fn is_filter_dir(config: &Config, entry: &BasicDirEntryInfo) -> bool {
let path = entry.path.as_path();
if path.ends_with(ZFS_HIDDEN_DIRECTORY) || path.ends_with(BTRFS_SNAPPER_HIDDEN_DIRECTORY) {
return true;
}
if let Some(common_snap_dir) = &config.dataset_collection.opt_common_snap_dir {
if path == *common_snap_dir {
return true;
}
}
let user_requested_dir = config
.opt_requested_dir
.as_ref()
.expect("opt_requested_dir must always be Some in any recursive mode")
.path_buf
.as_path();
if path == user_requested_dir {
false
} else {
config
.dataset_collection
.vec_of_filter_dirs
.par_iter()
.any(|filter_dir| path == filter_dir)
}
}
fn enumerate_deleted(
config: Arc<Config>,
requested_dir: &Path,
skim_tx_item: &SkimItemSender,
hangup_rx: &Receiver<Never>,
) -> HttmResult<()> {
if is_channel_closed(hangup_rx) {
return Err(HttmError::new("Thread requested to quit. Quitting.").into());
}
let vec_deleted = deleted_lookup_exec(config.as_ref(), requested_dir)?;
let (vec_dirs, vec_files): (Vec<BasicDirEntryInfo>, Vec<BasicDirEntryInfo>) =
vec_deleted.into_iter().partition(|entry| {
recursive_is_entry_dir(config.as_ref(), entry)
});
combine_and_send_entries(
config.clone(),
vec_files,
&vec_dirs,
true,
requested_dir,
skim_tx_item,
)?;
if config.deleted_mode != Some(DeletedMode::DepthOfOne) && config.opt_recursive {
vec_dirs
.into_iter()
.map(|basic_dir_entry_info| basic_dir_entry_info.path)
.try_for_each(|deleted_dir| {
let config_clone = config.clone();
let requested_dir_clone = requested_dir.to_path_buf();
get_entries_behind_deleted_dir(
config_clone,
&deleted_dir,
&requested_dir_clone,
skim_tx_item,
hangup_rx,
)
})
} else {
Ok(())
}
}
fn get_entries_behind_deleted_dir(
config: Arc<Config>,
deleted_dir: &Path,
requested_dir: &Path,
skim_tx_item: &SkimItemSender,
hangup_rx: &Receiver<Never>,
) -> HttmResult<()> {
fn recurse_behind_deleted_dir(
config: Arc<Config>,
dir_name: &Path,
from_deleted_dir: &Path,
from_requested_dir: &Path,
skim_tx_item: &SkimItemSender,
hangup_rx: &Receiver<Never>,
) -> HttmResult<()> {
if is_channel_closed(hangup_rx) {
return Err(HttmError::new("Thread requested to quit. Quitting.").into());
}
let deleted_dir_on_snap = &from_deleted_dir.to_path_buf().join(&dir_name);
let pseudo_live_dir = &from_requested_dir.to_path_buf().join(&dir_name);
let (vec_dirs, vec_files): (Vec<BasicDirEntryInfo>, Vec<BasicDirEntryInfo>) =
get_entries_partitioned(config.as_ref(), deleted_dir_on_snap)?;
combine_and_send_entries(
config.clone(),
vec_files,
&vec_dirs,
true,
pseudo_live_dir,
skim_tx_item,
)?;
vec_dirs.into_iter().try_for_each(|basic_dir_entry_info| {
recurse_behind_deleted_dir(
config.clone(),
Path::new(&basic_dir_entry_info.file_name),
deleted_dir_on_snap,
pseudo_live_dir,
skim_tx_item,
hangup_rx,
)
})
}
match &deleted_dir.file_name() {
Some(dir_name) => recurse_behind_deleted_dir(
config,
Path::new(dir_name),
deleted_dir.parent().unwrap_or_else(|| Path::new("/")),
requested_dir,
skim_tx_item,
hangup_rx,
)?,
None => return Err(HttmError::new("Not a valid file name!").into()),
}
Ok(())
}
fn get_pseudo_live_versions(
entries: Vec<BasicDirEntryInfo>,
pseudo_live_dir: &Path,
) -> Vec<BasicDirEntryInfo> {
entries
.into_iter()
.map(|basic_dir_entry_info| BasicDirEntryInfo {
path: pseudo_live_dir.join(&basic_dir_entry_info.file_name),
file_name: basic_dir_entry_info.file_name,
file_type: basic_dir_entry_info.file_type,
modify_time: OnceCell::new(),
})
.collect()
}
fn display_or_transmit(
config: Arc<Config>,
entries: Vec<BasicDirEntryInfo>,
is_phantom: bool,
skim_tx_item: &SkimItemSender,
) -> HttmResult<()> {
match &config.exec_mode {
ExecMode::Interactive(_) => {
transmit_entries(config.clone(), entries, is_phantom, skim_tx_item)?
}
ExecMode::DisplayRecursive(progress_bar) => {
if entries.is_empty() {
progress_bar.tick();
} else {
print_display_recursive(config.as_ref(), entries)?;
eprintln!();
}
}
_ => unreachable!(),
}
Ok(())
}
fn transmit_entries(
config: Arc<Config>,
entries: Vec<BasicDirEntryInfo>,
is_phantom: bool,
skim_tx_item: &SkimItemSender,
) -> HttmResult<()> {
entries
.into_iter()
.try_for_each(|basic_dir_entry_info| {
skim_tx_item.try_send(Arc::new(SelectionCandidate::new(
config.clone(),
basic_dir_entry_info,
is_phantom,
)))
})
.map_err(|err| err.into())
}
fn print_display_recursive(config: &Config, entries: Vec<BasicDirEntryInfo>) -> HttmResult<()> {
let pseudo_live_set: Vec<PathData> = entries
.iter()
.map(|basic_dir_entry_info| PathData::from(basic_dir_entry_info.path.as_path()))
.collect();
let map_live_to_snaps = versions_lookup_exec(config, &pseudo_live_set)?;
let output_buf = display_exec(config, &map_live_to_snaps)?;
print_output_buf(output_buf)?;
Ok(())
}