trasher 1.3.3

A small command-line utility to replace 'rm' and 'del' by a trash system
use std::fs;
use std::path::PathBuf;
use std::io::ErrorKind;
use super::{debug, fail};
use super::command::*;
use super::items::*;
use super::fsutils::*;

pub fn list(action: &ListTrashItems) {
    let ListTrashItems { name, details } = action;

    debug!("Listing trash items...");
    let mut items = list_trash_items(&OPTS.trash_dir).unwrap();

    if let Some(name) = &name {
        debug!("Filtering {} items by name...", items.len());
        items = items.into_iter().filter(|item| item.filename().contains(name)).collect();
    }

    match items.len() {
        0 => println!("{}", if name.is_some() { "No item in trash match the provided name." } else { "Trash is empty." }),
        count => {
            println!("Found {} item{} in trash:", count, if count >= 2 { "s" } else { "" });

            let mut total_size = 0;
            let mut total_dirs = 0;
            let mut total_files = 0;

            for item in items {
                println!("* {}{}", item, if *details || OPTS.verbose {
                    let details = get_fs_details(complete_trash_item_path(&item)).unwrap();
                    let dir_one = if details.is_dir { 1 } else { 0 };

                    total_size += details.size;
                    total_dirs += details.sub_files + dir_one;
                    total_files += details.sub_files + (1 - dir_one);

                    format!("{}", details)
                } else {
                    "".to_string()
                });
            }

            if *details || OPTS.verbose {
                println!(
                    "{} directories, {} files, total size is: {}",
                    total_dirs, total_files, human_readable_size(total_size)
                );
            }
        }
    }
}

pub fn remove(action: &MoveToTrash) {
    let MoveToTrash {
        path,
        permanently,
        move_ext_filesystems,
        size_limit_move_ext_filesystems,
        allow_invalid_utf8_item_names
    } = action;

    let size_limit_move_ext_filesystems = size_limit_move_ext_filesystems.as_ref().map(|size_limit|
        parse_human_readable_size(&size_limit).unwrap_or_else(|err|
            fail!("Invalid size limit provided for externals filesystems' items moving: {}", err)
        )
    );

    let path = PathBuf::from(path);

    debug!("Checking if item exists...");

    if !path.exists() {
        fail!("Item path does not exist.");
    }

    if *permanently {
        match fs::remove_dir_all(&path) {
            Err(err) => fail!("Failed to permanently remove item: {}", err),
            Ok(()) => return
        }
    }

    let filename = path.file_name().unwrap_or_else(|| fail!("Specified item path has no file name"));
    let filename = match filename.to_str() {
        Some(str) => str.to_string(),
        None => if *allow_invalid_utf8_item_names {
            filename.to_string_lossy().to_string()
        } else {
            fail!("Specified item does not have a valid UTF-8 file name")
        }
    };

    let trash_item = TrashItem::new_now(filename.to_string());

    debug!("Moving item to trash under name '{}'...", trash_item.trash_filename());

    let trash_item_path = transfer_trash_item_path(&trash_item);

    if !TRASH_TRANSFER_DIR.exists() {
        fs::create_dir_all(TRASH_TRANSFER_DIR.as_path()).unwrap_or_else(|err|
            fail!("Failed to create trash's transfer directory: {}", err)
        );
    }

    if let Err(err) = fs::rename(&path, &trash_item_path) {
        debug!("Renaming failed: {:?}", err);
        
        if err.kind() != ErrorKind::Other {
            fail!("An error occured while trying to move item to trash: {}", err);
        }

        if !move_ext_filesystems {
            fail!("Failed to move item to trash: {}\nHelp: Item may be located on another drive, try with '--move-ext-filesystems'.", err);
        }
        
        debug!("Falling back to copying.");

        if let Some(size_limit) = size_limit_move_ext_filesystems {
            debug!("Size limit was provided: {}", human_readable_size(size_limit));
            debug!("Computing size of the item to remove...");

            let details = get_fs_details(&path).unwrap_or_else(|err|
                fail!("Failed to compute size of item before sending it to the trash: {}", err)
            );

            if details.size > size_limit {
                fail!(
                    "This item ({}) is larger than the provided size limit ({}).",
                    human_readable_size(details.size),
                    human_readable_size(size_limit)
                );
            }
        }

        move_item_pbr(&path, &trash_item_path).unwrap_or_else(|err|
            fail!("Failed to move item to trash (using copying fallback): {}", err)
        )
    }

    let mut rename_errors = 0;

    while let Err(err) = move_transferred_trash_item(&trash_item) {
        rename_errors += 1;

        debug!("Failed to rename transferred item in trash (try n°{}): {}", rename_errors, err);

        if rename_errors == 5 {
            fail!("Failed to rename transferred item in trash after {} tries: {}", rename_errors, err);
        }
    }
}

pub fn drop(action: &DropItem) {
    let DropItem { filename, id } = action;
    
    debug!("Listing trash items...");

    match expect_trash_item(&OPTS.trash_dir, &filename, id.as_deref()).unwrap() {
        FoundTrashItems::Single(item) => {
            let item_path = complete_trash_item_path(&item);

            debug!("Permanently removing item from trash...");

            if let Err(err) = fs::remove_dir_all(&item_path) {
                fail!("Failed to remove item '{}' from trash: {}", item.filename(), err);
            }
        },

        FoundTrashItems::Multi(candidates) => println!(
            "Multiple items with this filename were found in the trash:{}",
            candidates.iter().map(|c| format!("\n* {}", c)).collect::<String>()
        )
    }
}

pub fn restore(action: &RestoreItem) {
    let RestoreItem {
        filename,
        to,
        id,
        force,
        move_ext_filesystems
    } = action;

    debug!("Listing trash items...");

    match expect_trash_item(&OPTS.trash_dir, &filename, id.as_deref()).unwrap() {
        FoundTrashItems::Single(item) => {
            let item_path = complete_trash_item_path(&item);
            let target_path = to.clone()
                .unwrap_or_else(|| std::env::current_dir().unwrap())
                .join(item.filename());

            if target_path.exists() {
                if !force {
                    fail!("Target path already exists, use '-f' / '--force' to override the existing item.");
                }

                debug!("Restoration path already exists, permanently removing it...");

                if let Err(err) = fs::remove_dir_all(&target_path) {
                    fail!("Failed to remove existing item at restoration path: {}", err);
                }
            }

            debug!("Restoring item from trash...");

            if let Err(err) = fs::rename(&item_path, &target_path) {
                if !move_ext_filesystems {
                    fail!("Failed to restore item '{}' from trash: {}", item.filename(), err);
                }
                
                debug!("Renaming failed: {}", err);
                debug!("Falling back to copying.");

                move_item_pbr(&item_path, &target_path).unwrap_or_else(|err|
                    fail!("Failed to restore item from trash (using copying fallback): {}", err)
                )
            }
        },

        FoundTrashItems::Multi(candidates) => println!(
            "Multiple items with this filename were found in the trash:{}",
            candidates.iter().map(|c| format!("\n* {}", c)).collect::<String>()
        )
    }
}

pub fn clear(action: &EmptyTrash) {
    let EmptyTrash {} = action;

    debug!("Emptying the trash...");

    // TODO: Ask confirmation
    
    if let Err(err) = fs::remove_dir_all(&OPTS.trash_dir) {
        fail!("Failed to empty trash: {}", err);
    }

    fs::create_dir_all(&OPTS.trash_dir).unwrap();
    println!("Trash has been emptied.");
}