use super::items::TrashItem;
use fs_extra::dir::TransitProcessResult;
use indicatif::{ProgressBar, ProgressStyle};
use once_cell::sync::Lazy;
use regex::Regex;
use std::cell::RefCell;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt;
use std::fs;
use std::io::Result as IoResult;
use std::path::Component;
use std::path::{Path, PathBuf};
use std::rc::Rc;
pub const TRASH_TRANSFER_DIRNAME: &str = "#PARTIAL";
pub fn list_trash_items(trash_path: impl AsRef<Path>) -> IoResult<Vec<TrashItem>> {
Ok(fs::read_dir(trash_path)?
.collect::<Result<Vec<_>, _>>()?
.iter()
.filter_map(|item| {
match item.file_name().into_string() {
Err(invalid_filename) => eprintln!(
"WARN: Trash item '{}' does not have a valid UTF-8 filename!",
invalid_filename.to_string_lossy()
),
Ok(filename) => {
if filename == TRASH_TRANSFER_DIRNAME {
return None;
}
match TrashItem::decode(&filename, Some(item.file_type().unwrap())) {
Err(err) => {
eprintln!(
"WARN: Trash item '{}' does not have a valid trash filename!",
filename
);
super::debug!("Invalid trash item filename: {:?}", err);
}
Ok(trash_item) => return Some(trash_item),
}
}
}
None
})
.collect())
}
pub fn expect_trash_item(
trash_dir: impl AsRef<Path>,
filename: &str,
id: Option<&str>,
) -> FoundTrashItems {
let mut candidates: Vec<TrashItem> = list_trash_items(&trash_dir)
.unwrap()
.into_iter()
.filter(|item| item.filename() == filename)
.collect();
if candidates.is_empty() {
super::fail!("Specified item was not found in the trash.");
} else if candidates.len() > 1 {
match id {
None => return FoundTrashItems::Multi(candidates),
Some(id) => {
return FoundTrashItems::Single(
candidates
.into_iter()
.find(|c| c.id() == id)
.unwrap_or_else(|| {
super::fail!("There is no trash item with the provided ID")
}),
)
}
}
}
FoundTrashItems::Single(candidates.remove(0))
}
pub fn expect_single_trash_item(
trash_dir: impl AsRef<Path>,
filename: &str,
id: Option<&str>,
) -> TrashItem {
match expect_trash_item(trash_dir, filename, id) {
FoundTrashItems::Single(item) => item,
FoundTrashItems::Multi(candidates) => super::fail!(
"Multiple items with this filename were found in the trash:{}",
candidates
.iter()
.map(|c| format!("\n* {}", c))
.collect::<String>()
),
}
}
pub fn get_fs_details(path: impl AsRef<Path>) -> IoResult<FSDetails> {
let metadata = fs::metadata(&path)?;
let is_dir = metadata.is_dir();
if metadata.file_type().is_symlink() {
return Ok(FSDetails {
is_symlink: true,
is_dir,
sub_directories: 0,
sub_files: 0,
size: 0,
});
}
if !is_dir {
return Ok(FSDetails {
is_symlink: false,
is_dir: false,
sub_directories: 0,
sub_files: 0,
size: metadata.len(),
});
}
let mut details = FSDetails {
is_symlink: false,
is_dir: true,
sub_directories: 0,
sub_files: 0,
size: 0,
};
for item in fs::read_dir(&path)? {
let item_details = get_fs_details(item?.path())?;
let dir_one = if item_details.is_dir { 1 } else { 0 };
details.sub_directories += item_details.sub_directories + dir_one;
details.sub_files += item_details.sub_files + (1 - dir_one);
details.size += item_details.size;
}
Ok(details)
}
pub fn transfer_trash_item_path(item: &TrashItem, trash_dir: &Path) -> PathBuf {
trash_dir
.join(TRASH_TRANSFER_DIRNAME)
.join(item.trash_filename())
}
pub fn complete_trash_item_path(item: &TrashItem, trash_dir: &Path) -> PathBuf {
trash_dir.join(item.trash_filename())
}
pub fn move_transferred_trash_item(item: &TrashItem, trash_dir: &Path) -> IoResult<()> {
fs::rename(
transfer_trash_item_path(item, trash_dir),
complete_trash_item_path(item, trash_dir),
)
}
pub fn cleanup_transfer_dir(dir: &Path) -> IoResult<()> {
if dir.exists() {
fs::remove_dir_all(dir)
} else {
Ok(())
}
}
pub fn human_readable_size(bytes: u64) -> String {
let names = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
if bytes < 1024 {
return format!("{} B", bytes);
}
let mut compare = 1024;
for name in names.iter() {
compare *= 1024;
if bytes <= compare {
return format!("{:.2} {}", bytes as f64 * 1024f64 / compare as f64, name);
}
}
format!(
"{:.2} {}",
bytes as f64 / compare as f64,
names.last().unwrap()
)
}
static PARSE_SIZE_STR: Lazy<Regex> = Lazy::new(|| {
Regex::new("^(?i)(?P<intqty>\\d+)(?:\\.(?P<decqty>\\d+))?(?P<unit>[BKMGTPE])(?:i?B)?$").unwrap()
});
pub fn parse_human_readable_size(size: &str) -> Result<u64, &'static str> {
let captured = PARSE_SIZE_STR.captures(size).ok_or("Unknown size format")?;
let int = captured["intqty"].parse::<u64>().unwrap();
let dec = captured.name("decqty");
let unit_char = captured["unit"]
.chars()
.next()
.unwrap()
.to_ascii_uppercase();
let unit_size = 1024u64.pow("BKMGTPE".chars().position(|c| c == unit_char).unwrap() as u32);
if dec.is_some() && unit_size == 1 {
return Err("Cannot use decimal bytes");
}
let dec_size = match dec {
None => 0,
Some(dec) => {
let dec = dec.as_str();
let dec_num = dec.parse::<u64>().unwrap();
let unit_divider = 10u64.pow(dec.len() as u32);
if unit_divider.to_string().len() > unit_size.to_string().len() {
return Err("Too many decimals for this unit, would give decimal bytes");
}
unit_size * dec_num / unit_divider
}
};
Ok(int * unit_size + dec_size)
}
pub fn move_item_pbr(path: &Path, target: &Path) -> Result<(), Box<dyn Error>> {
let pbr = Rc::new(RefCell::new(None));
let update_pbr = |copied, total, item_name: &str| {
let mut pbr = pbr.borrow_mut();
let pbr = pbr.get_or_insert_with(|| {
let pbr = ProgressBar::new(total);
pbr.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.expect("Invalid progress bar template")
.progress_chars("#>-"));
pbr
});
pbr.set_position(copied);
pbr.set_message(item_name.to_string());
};
if path.metadata()?.is_file() {
let file_name = path.file_name().unwrap().to_string_lossy();
fs_extra::file::move_file_with_progress(
path,
target,
&fs_extra::file::CopyOptions::new(),
|tp| {
update_pbr(tp.copied_bytes, tp.total_bytes, &file_name);
},
)?;
} else {
let mut config = fs_extra::dir::CopyOptions::new();
config.copy_inside = true;
fs_extra::dir::move_dir_with_progress(path, target, &config, |tp| {
update_pbr(tp.copied_bytes, tp.total_bytes, &tp.file_name);
TransitProcessResult::ContinueOrAbort
})?;
}
let mut pbr = pbr.borrow_mut();
let pbr = pbr.as_mut();
if let Some(pbr) = pbr {
pbr.finish_with_message("Moving complete.")
}
Ok(())
}
pub enum FoundTrashItems {
Single(TrashItem),
Multi(Vec<TrashItem>),
}
pub struct FSDetails {
pub is_symlink: bool,
pub is_dir: bool,
pub sub_directories: u64,
pub sub_files: u64,
pub size: u64,
}
impl fmt::Display for FSDetails {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
" | [{}] Size: {}{}",
if self.is_symlink {
"Symlink"
} else if self.is_dir {
"Directory"
} else {
"File"
},
human_readable_size(self.size),
if self.is_dir {
format!(
", Items: {}, Directories: {}, Files: {}",
self.sub_directories + self.sub_files,
self.sub_directories,
self.sub_files
)
} else {
"".to_string()
}
)
}
}
pub fn is_dangerous_path(path: &Path) -> bool {
let mut components = path.components();
match (
components.next(),
components.next(),
components.next(),
components.next(),
) {
(Some(Component::RootDir), None, None, None) => true,
(Some(Component::RootDir), Some(_), None, None) => true,
(Some(Component::RootDir), Some(Component::Normal(dir)), Some(_), None) => {
dir == OsStr::new("home")
}
_ => false,
}
}