use crate::MAP_OF_SNAPS;
use crate::config::generate::{Config, DedupBy, ExecMode, LastSnapMode};
use crate::data::paths::{CompareContentsContainer, PathData, PathDeconstruction};
use crate::filesystem::mounts::LinkType;
use crate::filesystem::snaps::MapOfSnaps;
use crate::library::results::{HttmError, HttmResult};
use hashbrown::HashSet;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::io::ErrorKind;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, OnceLock, RwLock, TryLockError};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionsMap {
inner: BTreeMap<PathData, Vec<PathData>>,
}
impl From<BTreeMap<PathData, Vec<PathData>>> for VersionsMap {
fn from(map: BTreeMap<PathData, Vec<PathData>>) -> Self {
Self { inner: map }
}
}
impl From<[(PathData, Vec<PathData>); 1]> for VersionsMap {
fn from(slice: [(PathData, Vec<PathData>); 1]) -> Self {
Self {
inner: slice.into(),
}
}
}
impl Deref for VersionsMap {
type Target = BTreeMap<PathData, Vec<PathData>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for VersionsMap {
fn deref_mut(&mut self) -> &mut BTreeMap<PathData, Vec<PathData>> {
&mut self.inner
}
}
impl VersionsMap {
pub fn new(config: &Config, path_set: &[PathData]) -> HttmResult<VersionsMap> {
let versions_map: VersionsMap = if path_set.len() == 1 {
Self::from_one_path(config, &path_set[0])
.into_iter()
.map(|v| v.into_inner())
.collect::<BTreeMap<PathData, Vec<PathData>>>()
.into()
} else {
Self::from_many_paths(config, path_set).into()
};
if versions_map.values().all(std::vec::Vec::is_empty)
&& versions_map
.keys()
.all(|path_data| path_data.opt_path_metadata().is_none())
{
let paths: Vec<&Path> = path_set.iter().map(|path_data| path_data.path()).collect();
let description = format!(
"Requested paths do not currently exist, and httm could not find any deleted versions in snapshots:\n{:?}\nSo, umm, 🤷, please check that you typed the correct path, and/or try again with a different path.",
paths
);
return HttmError::from(description).into();
}
Ok(versions_map)
}
#[inline(always)]
fn from_many_paths(
config: &Config,
path_set: &[PathData],
) -> BTreeMap<PathData, Vec<PathData>> {
path_set
.iter()
.flat_map(|path_data| Self::from_one_path(config, path_data))
.map(|versions| versions.into_inner())
.collect()
}
#[inline(always)]
fn from_one_path(config: &Config, path_data: &PathData) -> Option<Versions> {
match Versions::new(config, path_data) {
Ok(versions) => Some(versions),
Err(err) => {
if !matches!(config.exec_mode, ExecMode::Interactive(_)) {
eprintln!("WARN: {}", err.to_string())
}
None
}
}
.map(|mut versions| {
if config.opt_omit_ditto {
versions.omit_ditto();
}
versions
})
.map(|mut versions| {
if let Some(last_snap_mode) = &config.opt_last_snap {
versions.last_snap(last_snap_mode);
}
versions
})
}
}
pub struct Versions {
path_data_key: PathData,
snap_versions: Vec<PathData>,
}
impl Versions {
#[inline(always)]
pub fn new(config: &Config, path_data: &PathData) -> HttmResult<Self> {
let prox_opt_alts = ProximateDatasetAndOptAlts::new(config, path_data)?;
let path_data_key = prox_opt_alts.path_data.clone();
let snap_versions: Vec<PathData> = prox_opt_alts
.into_search_bundles()
.flat_map(|relative_path_snap_mounts| {
relative_path_snap_mounts.version_search(&config.dedup_by)
})
.collect();
Ok(Self {
path_data_key,
snap_versions,
})
}
pub fn live_path_data(&self) -> &PathData {
&self.path_data_key
}
pub fn snap_versions(&self) -> &[PathData] {
&self.snap_versions
}
pub fn from_raw(path_data_key: PathData, snap_versions: Vec<PathData>) -> Self {
Self {
path_data_key,
snap_versions,
}
}
#[inline(always)]
pub fn into_inner(self) -> (PathData, Vec<PathData>) {
(self.path_data_key, self.snap_versions)
}
#[inline(always)]
pub fn is_live_version_redundant(&self) -> bool {
if let Some(last_snap) = self.snap_versions.last() {
return last_snap.metadata_infallible() == self.path_data_key.metadata_infallible();
}
false
}
#[inline(always)]
fn omit_ditto(&mut self) {
if self.is_live_version_redundant() {
self.snap_versions.pop();
}
}
#[inline(always)]
fn last_snap(&mut self, last_snap_mode: &LastSnapMode) {
self.snap_versions = match self.snap_versions.last() {
Some(last) => match last_snap_mode {
LastSnapMode::Any => vec![last.to_owned()],
LastSnapMode::DittoOnly
if self.path_data_key.opt_path_metadata() == last.opt_path_metadata() =>
{
vec![last.to_owned()]
}
LastSnapMode::NoDittoExclusive | LastSnapMode::NoDittoInclusive
if self.path_data_key.opt_path_metadata() != last.opt_path_metadata() =>
{
vec![last.to_owned()]
}
_ => Vec::new(),
},
None => match last_snap_mode {
LastSnapMode::Without | LastSnapMode::NoDittoInclusive => {
vec![self.path_data_key.clone()]
}
_ => Vec::new(),
},
};
}
}
#[derive(Debug, Clone)]
pub struct ProximateDatasetAndOptAlts<'a> {
config: &'a Config,
path_data: &'a PathData,
proximate_dataset: &'a Path,
relative_path: &'a Path,
opt_alts: Option<&'a [Box<Path>]>,
}
impl<'a> ProximateDatasetAndOptAlts<'a> {
#[inline(always)]
pub fn new(config: &'a Config, path_data: &'a PathData) -> HttmResult<Self> {
let (proximate_dataset, relative_path) = path_data
.alias()
.map(|alias| (alias.proximate_dataset(), alias.relative_path()))
.map_or_else(
|| {
path_data.proximate_dataset().and_then(|proximate_dataset| {
path_data
.relative_path(proximate_dataset)
.map(|relative_path| (proximate_dataset, relative_path))
})
},
Ok,
)?;
let opt_alts = config
.dataset_collection
.opt_map_of_alts
.as_ref()
.and_then(|map_of_alts| map_of_alts.get(proximate_dataset))
.and_then(|alt_metadata| alt_metadata.deref().as_deref());
Ok(Self {
config,
path_data,
proximate_dataset,
relative_path,
opt_alts,
})
}
#[inline(always)]
pub fn path_data(&self) -> &PathData {
&self.path_data
}
#[inline(always)]
pub fn proximate_dataset(&self) -> &Path {
&self.proximate_dataset
}
#[inline(always)]
pub fn datasets_of_interest(&'a self) -> impl Iterator<Item = &'a Path> {
let alts = self.opt_alts.into_iter().flatten().map(|p| p.as_ref());
let base = Some(self.proximate_dataset).into_iter();
alts.chain(base)
}
#[inline(always)]
pub fn into_search_bundles(&'a self) -> impl Iterator<Item = RelativePathAndSnapMounts<'a>> {
self.datasets_of_interest().flat_map(|dataset_of_interest| {
RelativePathAndSnapMounts::new(
self.config,
self.path_data,
&self.relative_path,
&dataset_of_interest,
)
})
}
}
#[derive(Debug, Clone)]
pub struct RelativePathAndSnapMounts<'a> {
config: &'a Config,
path_data: &'a PathData,
relative_path: &'a Path,
dataset_of_interest: &'a Path,
snap_mounts: Cow<'a, [Box<Path>]>,
}
impl<'a> RelativePathAndSnapMounts<'a> {
#[inline(always)]
pub fn new(
config: &'a Config,
path_data: &'a PathData,
relative_path: &'a Path,
dataset_of_interest: &'a Path,
) -> Option<Self> {
Self::snap_mounts_from_dataset_of_interest(dataset_of_interest, config).map(|snap_mounts| {
Self {
config,
path_data,
relative_path,
dataset_of_interest,
snap_mounts,
}
})
}
fn snap_mounts_from_dataset_of_interest(
dataset_of_interest: &Path,
config: &Config,
) -> Option<Cow<'a, [Box<Path>]>> {
if !config.opt_debug && config.opt_lazy {
return config
.dataset_collection
.map_of_datasets
.get(dataset_of_interest)
.map(|md| {
MapOfSnaps::from_defined_mounts(&dataset_of_interest, md, config.opt_debug)
})
.map(|snap_mounts| Cow::Owned(snap_mounts));
}
MAP_OF_SNAPS
.get(dataset_of_interest)
.map(|snap_mounts| Cow::Borrowed(snap_mounts.as_slice()))
}
#[inline(always)]
pub fn snap_mounts(&'a self) -> &'a [Box<Path>] {
&self.snap_mounts
}
#[inline(always)]
pub fn relative_path(&'a self) -> &'a Path {
&self.relative_path
}
#[inline(always)]
fn match_metadata(&self, joined_path: PathBuf) -> Option<PathData> {
match joined_path.symlink_metadata() {
Ok(md) => {
Some(PathData::without_styling(&joined_path, Some(md)))
}
Err(err) => {
match err.kind() {
ErrorKind::PermissionDenied => {
eprintln!(
"Error: When httm tried to find a file contained within a snapshot directory, permission was denied. \
Perhaps you need to use sudo or equivalent to view the contents of this snapshot (for instance, btrfs by default creates privileged snapshots). \
\nDetails: {err}"
);
std::process::exit(1)
}
_ => None,
}
}
}
}
#[inline(always)]
fn all_versions(&'a self) -> Vec<PathData> {
self.snap_mounts
.iter()
.map(|snap_path| snap_path.join(self.relative_path))
.filter_map(|joined_path| self.match_metadata(joined_path))
.collect()
}
#[inline(always)]
fn sort_dedup_versions(vec: &mut Vec<PathData>, dedup_by: &DedupBy) {
match dedup_by {
DedupBy::Disable => {
vec.sort_unstable_by_key(|path_data| path_data.metadata_infallible());
}
DedupBy::Metadata => {
vec.sort_unstable_by_key(|path_data| path_data.metadata_infallible());
vec.dedup_by_key(|a| a.metadata_infallible());
}
DedupBy::Contents | DedupBy::Suspect => {
let mut container_vec: Vec<CompareContentsContainer> = std::mem::take(vec)
.into_iter()
.map(|path_data| CompareContentsContainer::from(path_data))
.collect();
container_vec.sort_unstable();
container_vec.dedup();
container_vec.sort_unstable_by_key(|path_data| path_data.metadata_infallible());
*vec = container_vec
.into_iter()
.map(|container| container.into())
.collect()
}
}
}
#[inline(always)]
pub fn version_search(&'a self, dedup_by: &DedupBy) -> Vec<PathData> {
if PreheatCache::should_enable(self) {
let cache = PREHEAT_CACHE.get_or_init(|| PreheatCache::new());
cache.exec(self);
}
let mut versions = self.all_versions();
Self::sort_dedup_versions(&mut versions, dedup_by);
versions
}
}
pub static PREHEAT_CACHE: OnceLock<PreheatCache> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct PreheatCache {
inner: Arc<RwLock<HashSet<PathBuf>>>,
hangup: Arc<AtomicBool>,
}
impl PreheatCache {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(HashSet::new())),
hangup: Arc::new(AtomicBool::new(false)),
}
}
pub fn should_enable(bundle: &RelativePathAndSnapMounts) -> bool {
matches!(bundle.config.exec_mode, ExecMode::Preview)
|| bundle
.config
.dataset_collection
.map_of_datasets
.get(bundle.dataset_of_interest)
.is_some_and(|md| matches!(md.link_type, LinkType::Network))
}
#[allow(dead_code)]
pub fn clear(&self) {
self.hangup
.store(true, std::sync::atomic::Ordering::Relaxed);
let inner_clone = self.inner.clone();
rayon::spawn(move || {
match inner_clone.write() {
Ok(mut map) => map.clear(),
Err(_err) => {
inner_clone.clear_poison();
}
};
});
}
#[inline(always)]
pub fn exec(&self, bundle: &RelativePathAndSnapMounts) {
if self
.inner
.try_read()
.ok()
.map(|cached_result| cached_result.contains(bundle.dataset_of_interest))
.unwrap_or_else(|| true)
{
return;
}
if self.hangup.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
let map_clone = self.inner.clone();
let hangup_clone = self.hangup.clone();
let path_data_clone = bundle.path_data.clone();
let dataset_of_interest_clone = bundle.dataset_of_interest.to_path_buf();
let config_clone = bundle.config.clone();
rayon::spawn_fifo(move || {
let mut backoff: u64 = 2;
let vec: Vec<PathBuf> = loop {
if hangup_clone.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
match map_clone.try_write() {
Ok(mut locked) => {
break PathData::proximate_plus_neighbors(
&path_data_clone,
&dataset_of_interest_clone,
)
.into_iter()
.filter(|item| locked.insert(item.to_path_buf()))
.collect();
}
Err(err) => {
if let TryLockError::Poisoned(_) = err {
map_clone.clear_poison();
}
std::thread::sleep(Duration::from_millis(backoff));
backoff *= 2;
continue;
}
}
};
vec.iter()
.filter_map(|dataset| {
RelativePathAndSnapMounts::snap_mounts_from_dataset_of_interest(
&dataset,
&config_clone,
)
})
.take_while(|_bundle| !hangup_clone.load(std::sync::atomic::Ordering::Relaxed))
.for_each(|bundle| {
let _ = bundle
.into_iter()
.map(|snap_path| std::fs::read_dir(snap_path))
.flatten()
.flatten()
.flatten()
.next();
});
});
}
}