#[macro_use]
extern crate lazy_static;
extern crate proc_mounts;
use std::{
error::Error,
ffi::OsString,
fmt,
fs::{canonicalize, symlink_metadata, DirEntry, FileType, Metadata},
path::{Path, PathBuf},
time::SystemTime,
};
use clap::{crate_name, crate_version, Arg, ArgMatches};
use fxhash::FxHashMap as HashMap;
use rayon::prelude::*;
use crate::display::display_exec;
use crate::interactive::interactive_exec;
use crate::parse_mounts::{get_filesystems_list, precompute_alt_replicated};
use crate::process_dirs::display_recursive_wrapper;
use crate::utility::{httm_is_dir, install_hot_keys, read_stdin};
use crate::versions_lookup::get_versions_set;
mod deleted_lookup;
mod display;
mod interactive;
mod parse_mounts;
mod process_dirs;
mod utility;
mod versions_lookup;
pub const ZFS_FSTYPE: &str = "zfs";
pub const BTRFS_FSTYPE: &str = "btrfs";
pub const SMB_FSTYPE: &str = "smbfs";
pub const NFS_FSTYPE: &str = "nfs";
pub const AFP_FSTYPE: &str = "afpfs";
pub const ZFS_HIDDEN_DIRECTORY: &str = ".zfs";
pub const BTRFS_SNAPPER_HIDDEN_DIRECTORY: &str = ".snapshots";
pub const ZFS_SNAPSHOT_DIRECTORY: &str = ".zfs/snapshot";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FilesystemType {
Zfs,
Btrfs,
}
#[derive(Debug)]
pub struct HttmError {
details: String,
}
impl HttmError {
fn new(msg: &str) -> Self {
HttmError {
details: msg.to_owned(),
}
}
fn with_context(msg: &str, err: Box<dyn Error + 'static>) -> Self {
let msg_plus_context = format!("{} : {:?}", msg, err);
HttmError {
details: msg_plus_context,
}
}
}
impl fmt::Display for HttmError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for HttmError {
fn description(&self) -> &str {
&self.details
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct BasicDirEntryInfo {
file_name: OsString,
path: PathBuf,
file_type: Option<FileType>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct PathData {
system_time: SystemTime,
size: u64,
path_buf: PathBuf,
is_phantom: bool,
}
impl From<&Path> for PathData {
fn from(path: &Path) -> Self {
let metadata_res = symlink_metadata(path);
PathData::from_parts(path, metadata_res)
}
}
impl From<&DirEntry> for PathData {
fn from(dir_entry: &DirEntry) -> Self {
let metadata_res = dir_entry.metadata();
let path = dir_entry.path();
PathData::from_parts(&path, metadata_res)
}
}
impl PathData {
fn from_parts(path: &Path, metadata_res: Result<Metadata, std::io::Error>) -> Self {
let absolute_path: PathBuf = if path.is_relative() {
if let Ok(canonical_path) = path.canonicalize() {
canonical_path
} else {
path.to_path_buf()
}
} else {
path.to_path_buf()
};
let (len, time, phantom) = match metadata_res {
Ok(md) => {
let len = md.len();
let time = md.modified().unwrap_or(SystemTime::UNIX_EPOCH);
let phantom = false;
(len, time, phantom)
}
Err(_) => {
let len = 0u64;
let time = SystemTime::UNIX_EPOCH;
let phantom = true;
(len, time, phantom)
}
};
PathData {
system_time: time,
size: len,
path_buf: absolute_path,
is_phantom: phantom,
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum ExecMode {
Interactive,
DisplayRecursive,
Display,
}
#[derive(Debug, Clone, PartialEq)]
enum InteractiveMode {
None,
Browse,
Select,
Restore,
}
#[derive(Debug, Clone, PartialEq)]
enum DeletedMode {
Disabled,
DepthOfOne,
Enabled,
Only,
}
#[derive(Debug, Clone)]
enum SnapPoint {
Native(NativeDatasets),
UserDefined(UserDefinedDirs),
}
#[derive(Debug, Clone)]
pub struct NativeDatasets {
map_of_datasets: HashMap<PathBuf, (String, FilesystemType)>,
map_of_alts: Option<HashMap<PathBuf, Vec<PathBuf>>>,
map_of_snaps: Option<HashMap<PathBuf, Vec<PathBuf>>>,
}
#[derive(Debug, Clone)]
pub struct UserDefinedDirs {
snap_dir: PathBuf,
local_dir: PathBuf,
fstype: FilesystemType,
}
#[derive(Debug, Clone)]
pub struct Config {
paths: Vec<PathData>,
opt_alt_replicated: bool,
opt_raw: bool,
opt_zeros: bool,
opt_no_pretty: bool,
opt_no_live_vers: bool,
opt_recursive: bool,
opt_exact: bool,
exec_mode: ExecMode,
snap_point: SnapPoint,
deleted_mode: DeletedMode,
interactive_mode: InteractiveMode,
pwd: PathData,
requested_dir: Option<PathData>,
}
impl Config {
fn from(
matches: ArgMatches,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync + 'static>> {
if matches.is_present("ZSH_HOT_KEYS") {
install_hot_keys()?
}
let opt_zeros = matches.is_present("ZEROS");
let opt_raw = matches.is_present("RAW");
let opt_no_pretty = matches.is_present("NOT_SO_PRETTY");
let opt_no_live_vers = matches.is_present("NO_LIVE");
let opt_recursive = matches.is_present("RECURSIVE");
let opt_exact = matches.is_present("EXACT");
let mut deleted_mode = match matches.value_of("DELETED") {
None => DeletedMode::Disabled,
Some("") | Some("all") => DeletedMode::Enabled,
Some("single") => DeletedMode::DepthOfOne,
Some("only") => DeletedMode::Only,
_ => unreachable!(),
};
let mut exec_mode = if matches.is_present("INTERACTIVE")
|| matches.is_present("RESTORE")
|| matches.is_present("SELECT")
{
ExecMode::Interactive
} else if deleted_mode != DeletedMode::Disabled {
ExecMode::DisplayRecursive
} else {
deleted_mode = DeletedMode::Disabled;
ExecMode::Display
};
let env_snap_dir = std::env::var_os("HTTM_SNAP_POINT");
let env_local_dir = std::env::var_os("HTTM_LOCAL_DIR");
let interactive_mode = if matches.is_present("RESTORE") {
InteractiveMode::Restore
} else if matches.is_present("SELECT") {
InteractiveMode::Select
} else if matches.is_present("INTERACTIVE") {
InteractiveMode::Browse
} else {
InteractiveMode::None
};
if opt_recursive && exec_mode == ExecMode::Display {
return Err(
HttmError::new("Recursive search feature only allowed in select modes.").into(),
);
}
let pwd = if let Ok(pwd) = std::env::current_dir() {
if let Ok(path) = PathBuf::from(&pwd).canonicalize() {
PathData::from(path.as_path())
} else {
return Err(HttmError::new(
"Could not obtain a canonical path for your working directory",
)
.into());
}
} else {
return Err(HttmError::new(
"Working directory does not exist or your do not have permissions to access it.",
)
.into());
};
let raw_snap_var = if let Some(value) = matches.value_of_os("SNAP_POINT") {
Some(value.to_os_string())
} else {
env_snap_dir
};
let (opt_alt_replicated, snap_point) = if let Some(raw_value) = raw_snap_var {
if matches.is_present("ALT_REPLICATED") {
return Err(HttmError::new(
"Alternate replicated datasets are not available for search, when the user defines a snap point.",
)
.into());
}
let snap_dir = PathBuf::from(raw_value);
if snap_dir.metadata().is_err() {
return Err(HttmError::new(
"Manually set snap point directory does not exist. Perhaps it is not already mounted?",
)
.into());
}
let fstype = if snap_dir.join(ZFS_SNAPSHOT_DIRECTORY).metadata().is_ok() {
FilesystemType::Zfs
} else {
FilesystemType::Btrfs
};
let raw_local_var = if let Some(raw_value) = matches.value_of_os("LOCAL_DIR") {
Some(raw_value.to_os_string())
} else {
env_local_dir
};
let local_dir = if let Some(value) = raw_local_var {
let local_dir: PathBuf = PathBuf::from(value);
if local_dir.metadata().is_ok() {
local_dir
} else {
return Err(HttmError::new(
"Manually set local relative directory does not exist. Please try another.",
)
.into());
}
} else {
pwd.path_buf.clone()
};
(
false,
SnapPoint::UserDefined(UserDefinedDirs {
snap_dir,
local_dir,
fstype,
}),
)
} else {
let (map_of_datasets, map_of_snaps) = get_filesystems_list()?;
let map_of_alts =
if matches.is_present("ALT_REPLICATED") && exec_mode != ExecMode::Display {
Some(precompute_alt_replicated(&map_of_datasets))
} else {
None
};
(
matches.is_present("ALT_REPLICATED"),
SnapPoint::Native(NativeDatasets {
map_of_datasets,
map_of_alts,
map_of_snaps,
}),
)
};
let mut paths: Vec<PathData> = if let Some(input_files) =
matches.values_of_os("INPUT_FILES")
{
input_files
.par_bridge()
.map(Path::new)
.map(|path| canonicalize(path).unwrap_or_else(|_| pwd.clone().path_buf.join(path)))
.map(|path| PathData::from(path.as_path()))
.collect()
} else if exec_mode == ExecMode::Interactive || exec_mode == ExecMode::DisplayRecursive {
vec![pwd.clone()]
} else if exec_mode == ExecMode::Display {
read_stdin()?
.iter()
.par_bridge()
.map(|string| PathData::from(Path::new(&string)))
.collect()
} else {
unreachable!()
};
paths = if exec_mode == ExecMode::Display && paths.len() > 1 {
paths.par_sort_by_key(|pathdata| pathdata.path_buf.clone());
paths.dedup_by_key(|pathdata| pathdata.path_buf.clone());
paths
} else {
paths
};
let requested_dir: Option<PathData> = match exec_mode {
ExecMode::Interactive => {
match paths.len() {
0 => Some(pwd.clone()),
1 => match paths.get(0) {
Some(pathdata) => {
if httm_is_dir(pathdata) {
Some(pathdata.clone())
} else {
match interactive_mode {
InteractiveMode::Browse | InteractiveMode::None => {
return Err(HttmError::new(
"Path specified is not a directory, and therefore not suitable for browsing.",
)
.into());
}
InteractiveMode::Restore | InteractiveMode::Select => {
None
}
}
}
}
_ => unreachable!(),
},
n if n > 1 => {
return Err(HttmError::new(
"May only specify one path in interactive mode.",
)
.into())
}
_ => {
unreachable!()
}
}
}
ExecMode::DisplayRecursive => {
match paths.len() {
0 => Some(pwd.clone()),
1 => match paths.get(0) {
Some(pathdata) if httm_is_dir(pathdata) => Some(pathdata.clone()),
_ => {
exec_mode = ExecMode::Display;
deleted_mode = DeletedMode::Disabled;
None
}
},
n if n > 1 => {
return Err(HttmError::new(
"May only specify one path in display recursive mode.",
)
.into())
}
_ => {
unreachable!()
}
}
}
ExecMode::Display => {
None
}
};
let config = Config {
paths,
opt_alt_replicated,
opt_raw,
opt_zeros,
opt_no_pretty,
opt_no_live_vers,
opt_recursive,
opt_exact,
snap_point,
exec_mode,
deleted_mode,
interactive_mode,
pwd,
requested_dir,
};
Ok(config)
}
}
fn parse_args() -> ArgMatches {
clap::Command::new(crate_name!())
.about("\nby default, httm will display non-interactive information about unique file versions contained on snapshots.\n\n\
You may also select from the various interactive modes below to browse for, select, and/or restore files.")
.version(crate_version!())
.arg(
Arg::new("INPUT_FILES")
.help("in any non-interactive mode, put requested files here. If you enter no files, \
then httm will pause waiting for input on stdin(3). In any interactive mode, \
this is the directory search path. If no directory is entered, \
httm will use the current working directory.")
.takes_value(true)
.multiple_values(true)
.value_parser(clap::builder::ValueParser::os_string())
.display_order(1)
)
.arg(
Arg::new("INTERACTIVE")
.short('i')
.long("interactive")
.help("interactive browse and search a specified directory to display unique file versions.")
.display_order(2)
)
.arg(
Arg::new("SELECT")
.short('s')
.long("select")
.help("interactive browse and search a specified directory to display unique file versions. Continue to another dialog to select a snapshot version to dump to stdout(3).")
.conflicts_with("RESTORE")
.display_order(3)
)
.arg(
Arg::new("RESTORE")
.short('r')
.long("restore")
.help("interactive browse and search a specified directory to display unique file versions. Continue to another dialog to select a snapshot version to restore.")
.conflicts_with("SELECT")
.display_order(4)
)
.arg(
Arg::new("DELETED")
.short('d')
.long("deleted")
.takes_value(true)
.default_missing_value("all")
.possible_values(&["all", "single", "only"])
.help("show deleted files in interactive modes. In non-interactive modes, do a search for all files deleted from a specified directory. \
If \"--deleted only\" is specified, then, in interactive modes, non-deleted files will be excluded from the search. \
If \"--deleted single\" is specified, then, deleted files behind deleted directories, \
(files with a depth greater than one) will be ignored.")
.display_order(5)
)
.arg(
Arg::new("ALT_REPLICATED")
.short('a')
.long("alt-replicated")
.help("automatically discover locally replicated datasets and list their snapshots as well. \
NOTE: Be certain such replicated datasets are mounted before use, as httm will silently ignore any unmounted \
datasets in the interactive modes.")
.conflicts_with_all(&["SNAP_POINT", "LOCAL_DIR"])
.display_order(6)
)
.arg(
Arg::new("RECURSIVE")
.short('R')
.long("recursive")
.help("recurse into selected directory to find more files. Only available in interactive and deleted file modes.")
.display_order(7)
)
.arg(
Arg::new("EXACT")
.short('e')
.long("exact")
.help("use exact pattern matching for searches in the interactive modes (in contrast to the default fuzzy-finder searching).")
.display_order(8)
)
.arg(
Arg::new("SNAP_POINT")
.long("snap-point")
.help("ordinarily httm will automatically choose your dataset root directory (the most proximate ancestor directory which contains a \".zfs\" directory), \
but here you may manually specify your own mount point for that directory, such as the local mount point for a remote share. \
You may also set via the HTTM_SNAP_POINT environment variable. Note: Use of both \"snap-point\" and \"local-dir\" are not always necessary to view versions on remote shares. \
httm can also automatically detect ZFS datasets mounted as AFP, SMB, and NFS remote shares.")
.takes_value(true)
.display_order(9)
)
.arg(
Arg::new("LOCAL_DIR")
.long("local-dir")
.help("used with \"snap-point\" to determine where the corresponding live root filesystem of the dataset is. \
Put more simply, the \"local-dir\" is the directory you backup to your \"snap-point\". If not set, httm defaults to your current working directory. \
You can also set via the environment variable HTTM_LOCAL_DIR.")
.requires("SNAP_POINT")
.takes_value(true)
.display_order(10)
)
.arg(
Arg::new("RAW")
.short('n')
.long("raw")
.help("display the backup locations only, without extraneous information, delimited by a NEWLINE.")
.conflicts_with_all(&["ZEROS", "NOT_SO_PRETTY"])
.display_order(11)
)
.arg(
Arg::new("ZEROS")
.short('0')
.long("zero")
.help("display the backup locations only, without extraneous information, delimited by a NULL CHARACTER.")
.conflicts_with_all(&["RAW", "NOT_SO_PRETTY"])
.display_order(12)
)
.arg(
Arg::new("NOT_SO_PRETTY")
.long("not-so-pretty")
.help("display the ordinary output, but tab delimited, without any pretty border lines.")
.conflicts_with_all(&["RAW", "ZEROS"])
.display_order(13)
)
.arg(
Arg::new("NO_LIVE")
.long("no-live")
.help("only display information concerning snapshot versions, and no 'live' versions of files or directories.")
.display_order(14)
)
.arg(
Arg::new("ZSH_HOT_KEYS")
.long("install-zsh-hot-keys")
.help("install zsh hot keys to the users home directory, and then exit")
.exclusive(true)
.display_order(15)
)
.get_matches()
}
fn main() {
match exec() {
Ok(_) => std::process::exit(0),
Err(error) => {
eprintln!("Error: {}", error);
std::process::exit(1)
}
}
}
fn exec() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
let arg_matches = parse_args();
let config = Config::from(arg_matches)?;
let snaps_and_live_set = match config.exec_mode {
ExecMode::Interactive => get_versions_set(&config, &interactive_exec(&config)?)?,
ExecMode::Display => get_versions_set(&config, &config.paths)?,
ExecMode::DisplayRecursive => display_recursive_wrapper(&config)?,
};
let output_buf = display_exec(&config, snaps_and_live_set)?;
print!("{}", output_buf);
Ok(())
}