use clap::CommandFactory;
use fs_extra::dir::get_size;
use std::fs::Metadata;
use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::{env, fs};
use walkdir::WalkDir;
#[derive(Debug, Clone)]
pub struct DirToCreate {
pub path: PathBuf,
pub permissions: Option<fs::Permissions>,
}
#[cfg(unix)]
use nix::libc;
#[cfg(unix)]
use nix::sys::stat::Mode;
#[cfg(unix)]
use nix::unistd::mkfifo;
#[cfg(unix)]
use std::os::unix::fs::{symlink, FileTypeExt, PermissionsExt};
#[cfg(target_os = "windows")]
use std::os::windows::fs::symlink_file as symlink;
pub mod args;
pub mod completions;
pub mod record;
pub mod util;
use args::Args;
use record::{Record, RecordItem, DEFAULT_FILE_LOCK};
const LINES_TO_INSPECT: usize = 6;
const FILES_TO_INSPECT: usize = 6;
pub const BIG_FILE_THRESHOLD: u64 = 500_000_000;
pub fn run(cli: &Args, mode: impl util::TestingMode, stream: &mut impl Write) -> Result<(), Error> {
args::validate_args(cli)?;
let graveyard: &PathBuf = &get_graveyard(cli.graveyard.clone());
if !graveyard.exists() {
fs::create_dir_all(graveyard)?;
#[cfg(unix)]
{
fs::set_permissions(graveyard, fs::Permissions::from_mode(0o700))?;
}
}
let record = Record::<DEFAULT_FILE_LOCK>::new(graveyard);
let cwd = &env::current_dir()?;
if cli.decompose {
if cli.force || util::prompt_yes("Really unlink the entire graveyard?", &mode, stream)? {
fs::remove_dir_all(graveyard)?;
}
} else if let Some(ref mut graves_to_exhume) = cli.unbury.clone() {
if cli.seance && record.open().is_ok() {
let gravepath = util::join_absolute(graveyard, dunce::canonicalize(cwd)?);
for grave in record.seance(&gravepath)? {
graves_to_exhume.push(grave.dest);
}
}
if graves_to_exhume.is_empty() {
if let Ok(s) = record.get_last_bury() {
graves_to_exhume.push(s);
}
}
let allow_rename = util::allow_rename();
for line in record.lines_of_graves(graves_to_exhume) {
let entry = RecordItem::new(&line);
let orig: PathBuf = if util::symlink_exists(&entry.orig) {
util::rename_grave(&entry.orig)
} else {
PathBuf::from(&entry.orig)
};
let dirs_to_create = build_dirs_to_create_from_graveyard(&entry.dest, &orig);
move_target(
&entry.dest,
&orig,
allow_rename,
&mode,
stream,
cli.force,
&dirs_to_create,
)
.map_err(|e| {
Error::new(
e.kind(),
format!(
"Unbury failed: couldn't copy files from {} to {}",
entry.dest.display(),
orig.display()
),
)
})?;
writeln!(
stream,
"Returned {} to {}",
entry.dest.display(),
orig.display()
)?;
}
record.log_exhumed_graves(graves_to_exhume)?;
} else if cli.seance {
let gravepath = util::join_absolute(graveyard, dunce::canonicalize(cwd)?);
writeln!(stream, "{: <19}\tpath", "deletion_time")?;
for grave in record.seance(&gravepath)? {
let formatted_time = grave.format_time_for_display()?;
writeln!(stream, "{}\t{}", formatted_time, grave.dest.display())?;
}
} else if cli.targets.is_empty() {
Args::command().print_help()?;
} else {
let allow_rename = util::allow_rename();
for target in &cli.targets {
bury_target(
target,
graveyard,
&record,
cwd,
cli.inspect,
allow_rename,
&mode,
stream,
cli.force,
)?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn bury_target<const FILE_LOCK: bool>(
target: &PathBuf,
graveyard: &PathBuf,
record: &Record<FILE_LOCK>,
cwd: &Path,
inspect: bool,
allow_rename: bool,
mode: &impl util::TestingMode,
stream: &mut impl Write,
force: bool,
) -> Result<(), Error> {
let metadata = &fs::symlink_metadata(target).map_err(|_| {
Error::new(
ErrorKind::NotFound,
format!(
"Cannot remove {}: no such file or directory",
target.to_str().unwrap()
),
)
})?;
let source = &if metadata.file_type().is_symlink() {
cwd.join(target)
} else {
dunce::canonicalize(cwd.join(target))
.map_err(|e| Error::new(e.kind(), "Failed to canonicalize path"))?
};
if inspect && !should_we_bury_this(target, source, metadata, mode, stream)? {
} else if source.starts_with(
dunce::canonicalize(graveyard)
.map_err(|e| Error::new(e.kind(), "Failed to canonicalize graveyard path"))?,
) {
if force
|| util::prompt_yes(
format!(
"{} is already in the graveyard.\nPermanently unlink it?",
source.display()
),
mode,
stream,
)?
{
if fs::remove_dir_all(source).is_err() {
fs::remove_file(source).map_err(|e| {
Error::new(e.kind(), format!("Couldn't unlink {}", source.display()))
})?;
}
} else {
writeln!(stream, "Skipping {}", source.display())?;
}
} else {
let (dest, dirs_to_create) = build_graveyard_dest(graveyard, source);
let dest: &Path = &{
if util::symlink_exists(&dest) {
util::rename_grave(dest)
} else {
dest
}
};
let moved = move_target(
source,
dest,
allow_rename,
mode,
stream,
force,
&dirs_to_create,
)
.map_err(|e| {
fs::remove_dir_all(dest).ok();
Error::new(e.kind(), "Failed to bury file")
})?;
if moved {
record.write_log(source, dest)?;
}
}
Ok(())
}
fn should_we_bury_this(
target: &Path,
source: &PathBuf,
metadata: &Metadata,
mode: &impl util::TestingMode,
stream: &mut impl Write,
) -> Result<bool, Error> {
if metadata.is_dir() {
{
let num_bytes = get_size(source).map_err(|_| {
Error::other(format!(
"Failed to get size of directory: {}",
source.display()
))
})?;
writeln!(
stream,
"{}: directory, {} including:",
target.to_str().unwrap(),
util::humanize_bytes(num_bytes)
)?;
}
for entry in WalkDir::new(source)
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
.min_depth(1)
.max_depth(1)
.into_iter()
.filter_map(Result::ok)
.take(FILES_TO_INSPECT)
{
writeln!(stream, "{}", entry.path().display())?;
}
} else {
writeln!(
stream,
"{}: file, {}",
&target.to_str().unwrap(),
util::humanize_bytes(metadata.len())
)?;
if let Ok(source_file) = fs::File::open(source) {
for line in BufReader::new(source_file)
.lines()
.take(LINES_TO_INSPECT)
.filter_map(Result::ok)
{
writeln!(stream, "> {line}")?;
}
} else {
writeln!(stream, "Error reading {}", source.display())?;
}
}
util::prompt_yes(
format!("Send {} to the graveyard?", target.to_str().unwrap()),
mode,
stream,
)
}
fn build_graveyard_dest(graveyard: &Path, source: &Path) -> (PathBuf, Vec<DirToCreate>) {
let mut dest = graveyard.to_path_buf();
let mut dirs_to_create = Vec::new();
let mut cumulative_source = PathBuf::new();
for component in source.components() {
cumulative_source.push(component.as_os_str());
if util::push_component_to_dest(&mut dest, &component) {
if cumulative_source.is_dir() {
let permissions = fs::metadata(&cumulative_source)
.map(|m| m.permissions())
.ok();
dirs_to_create.push(DirToCreate {
path: dest.clone(),
permissions,
});
}
}
}
(dest, dirs_to_create)
}
fn build_dirs_to_create_from_graveyard(
graveyard_path: &Path,
orig_path: &Path,
) -> Vec<DirToCreate> {
let mut dirs_to_create = Vec::new();
let mut graveyard_current = graveyard_path.parent();
let mut orig_current = orig_path.parent();
while let (Some(g), Some(o)) = (graveyard_current, orig_current) {
let permissions = fs::metadata(g).map(|m| m.permissions()).ok();
dirs_to_create.push(DirToCreate {
path: o.to_path_buf(),
permissions,
});
graveyard_current = g.parent();
orig_current = o.parent();
}
dirs_to_create.reverse();
dirs_to_create
}
fn create_dirs_for_copy(dirs_to_create: &[DirToCreate]) -> Result<Vec<DirToCreate>, Error> {
let mut created = Vec::new();
for dir in dirs_to_create {
if dir.path.exists() {
continue;
}
fs::create_dir(&dir.path).map_err(|e| {
Error::new(
e.kind(),
format!("Failed to create directory {}: {}", dir.path.display(), e),
)
})?;
created.push(dir.clone());
}
Ok(created)
}
fn apply_dir_permissions(dirs: &[DirToCreate]) -> Result<(), Error> {
for dir in dirs.iter().rev() {
if let Some(perms) = &dir.permissions {
fs::set_permissions(&dir.path, perms.clone()).map_err(|e| {
Error::new(
e.kind(),
format!("Failed to set permissions on {}: {}", dir.path.display(), e),
)
})?;
}
}
Ok(())
}
pub fn move_target(
target: &Path,
dest: &Path,
allow_rename: bool,
mode: &impl util::TestingMode,
stream: &mut impl Write,
force: bool,
dirs_to_create: &[DirToCreate],
) -> Result<bool, Error> {
if allow_rename && fs::rename(target, dest).is_ok() {
return Ok(true);
}
let created_dirs = create_dirs_for_copy(dirs_to_create)?;
if fs::symlink_metadata(target)?.is_dir() {
let moved = move_dir(target, dest, mode, stream, force)?;
apply_dir_permissions(&created_dirs)?;
Ok(moved)
} else {
let moved = copy_file(target, dest, mode, stream, force).map_err(|e| {
Error::new(
e.kind(),
format!(
"Failed to copy file from {} to {}",
target.display(),
dest.display()
),
)
})?;
fs::remove_file(target).map_err(|e| {
Error::new(
e.kind(),
format!("Failed to remove file: {}", target.display()),
)
})?;
apply_dir_permissions(&created_dirs)?;
Ok(moved)
}
}
pub fn move_dir(
target: &Path,
dest: &Path,
mode: &impl util::TestingMode,
stream: &mut impl Write,
force: bool,
) -> Result<bool, Error> {
let mut dest_dirs_and_perms: Vec<(PathBuf, fs::Permissions)> = Vec::new();
for entry in WalkDir::new(target).into_iter().filter_map(Result::ok) {
let orphan = entry
.path()
.strip_prefix(target)
.map_err(|_| Error::other("Parent directory isn't a prefix of child directories?"))?;
if entry.file_type().is_dir() {
let dest_dir = dest.join(orphan);
fs::create_dir_all(&dest_dir).map_err(|e| {
Error::new(
e.kind(),
format!(
"Failed to create dir: {} in {}",
entry.path().display(),
dest_dir.display()
),
)
})?;
let source_metadata = fs::metadata(entry.path()).map_err(|e| {
Error::new(
e.kind(),
format!("Failed to get metadata for: {}", entry.path().display()),
)
})?;
dest_dirs_and_perms.push((dest_dir, source_metadata.permissions()));
} else {
copy_file(entry.path(), &dest.join(orphan), mode, stream, force).map_err(|e| {
Error::new(
e.kind(),
format!(
"Failed to copy file from {} to {}",
entry.path().display(),
dest.join(orphan).display()
),
)
})?;
}
}
fs::remove_dir_all(target).map_err(|e| {
Error::new(
e.kind(),
format!("Failed to remove dir: {}", target.display()),
)
})?;
for (dest_dir, perms) in dest_dirs_and_perms.into_iter().rev() {
fs::set_permissions(&dest_dir, perms).map_err(|e| {
Error::new(
e.kind(),
format!("Failed to set permissions on: {}", dest_dir.display()),
)
})?;
}
Ok(true)
}
pub fn copy_file(
source: &Path,
dest: &Path,
mode: &impl util::TestingMode,
stream: &mut impl Write,
force: bool,
) -> Result<bool, Error> {
let metadata = fs::symlink_metadata(source)?;
let filetype = metadata.file_type();
if metadata.len() > BIG_FILE_THRESHOLD {
if !force
&& util::prompt_yes(
format!(
"About to copy a big file ({} is {})\nPermanently delete this file instead?",
source.display(),
util::humanize_bytes(metadata.len())
),
mode,
stream,
)?
{
return Ok(false);
}
}
if filetype.is_file() {
fs::copy(source, dest)?;
return Ok(true);
}
#[cfg(unix)]
if filetype.is_fifo() {
let perm: libc::mode_t = (metadata.permissions().mode() & 0o777) as libc::mode_t;
let mode = Mode::from_bits_truncate(perm);
mkfifo(dest, mode)?;
return Ok(true);
}
if filetype.is_symlink() {
let target = fs::read_link(source)?;
symlink(target, dest)?;
return Ok(true);
}
match fs::copy(source, dest) {
Err(e) => {
if !force
&& util::prompt_yes(
format!(
"Non-regular file or directory: {}\nPermanently delete the file?",
source.display()
),
mode,
stream,
)?
{
Ok(false)
} else {
Err(e)
}
}
Ok(_) => Ok(true),
}
}
pub fn get_graveyard(graveyard: Option<PathBuf>) -> PathBuf {
graveyard.unwrap_or_else(|| {
if let Ok(env_graveyard) = env::var("RIP_GRAVEYARD") {
PathBuf::from(env_graveyard)
} else if let Ok(mut env_graveyard) = env::var("XDG_DATA_HOME") {
if !env_graveyard.ends_with(std::path::MAIN_SEPARATOR) {
env_graveyard.push(std::path::MAIN_SEPARATOR);
}
env_graveyard.push_str("graveyard");
PathBuf::from(env_graveyard)
} else {
let user = util::get_user();
env::temp_dir().join(format!("graveyard-{user}"))
}
})
}
pub mod testing {
use super::{should_we_bury_this, util, Error, Metadata, Path, PathBuf, Write};
pub fn testable_should_we_bury_this(
target: &Path,
source: &PathBuf,
metadata: &Metadata,
stream: &mut impl Write,
) -> Result<bool, Error> {
should_we_bury_this(target, source, metadata, &util::TestMode, stream)
}
}