easy-archive 0.2.4

easy-archive
Documentation
/// Path utility functions for the CLI
///
/// Provides helpers for determining output paths and generating
/// unique filenames to avoid overwriting existing files.
use crate::Fmt;

use path_clean::PathClean;
use std::path::{Path, PathBuf};

/// Generate an available (non-existing) path by appending a number suffix
///
/// If the base path already exists, this function tries appending `(1)`, `(2)`, etc.
/// until it finds a path that doesn't exist.
///
/// # Arguments
/// * `base_path` - The desired output path
/// * `is_directory` - Whether the output is a directory (affects naming)
///
/// # Returns
/// A string path that does not currently exist
pub fn get_available_path(base_path: &Path, is_directory: bool) -> String {
    if !base_path.exists() {
        return base_path.to_string_lossy().into_owned();
    }

    let mut i = 1;
    let parent = base_path.parent().unwrap_or_else(|| Path::new(""));
    let file_name = base_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("");
    let ext = base_path
        .extension()
        .and_then(|s| s.to_str())
        .unwrap_or("");
    let orig_name = base_path
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("");

    loop {
        let new_name = if is_directory || ext.is_empty() {
            format!("{}({})", orig_name, i)
        } else {
            format!("{}({}).{}", file_name, i, ext)
        };

        let new_path = parent.join(&new_name);
        if !new_path.exists() {
            return new_path.to_string_lossy().into_owned();
        }
        i += 1;
    }
}

/// Determine the default output path based on inputs and detected format
///
/// When no explicit output is provided, this function infers a sensible default:
/// - For decompression: derives directory name from archive filename
/// - For compression: uses the input's parent directory with `.zip` extension
/// - For multiple inputs: uses the parent directory's name
///
/// # Arguments
/// * `inputs` - List of input file/directory paths
/// * `input_fmt` - Optional detected format of the first input
///
/// # Returns
/// A default output path string
pub fn get_default_output(inputs: &[String], input_fmt: Option<Fmt>) -> String {
    if inputs.is_empty() {
        return get_available_path(&PathBuf::from("archive.zip"), false);
    }

    if inputs.len() == 1 {
        let input_path = Path::new(&inputs[0]).clean();

        if let Some(fmt) = input_fmt {
            let mut dir_name = input_path
                .file_name()
                .map(|s| s.to_string_lossy().into_owned())
                .unwrap_or_else(|| "archive".to_string());

            for ext in fmt.extensions() {
                if dir_name.ends_with(ext) {
                    dir_name = dir_name[..dir_name.len() - ext.len()].to_string();
                    break;
                }
            }

            let mut base_output = input_path
                .parent()
                .unwrap_or_else(|| Path::new(""))
                .join(dir_name);
            if base_output.as_os_str().is_empty() {
                base_output = PathBuf::from("archive");
            }

            return get_available_path(&base_output, true);
        }
    }

    let first_input = Path::new(&inputs[0]).clean();
    let parent = first_input.parent().unwrap_or_else(|| Path::new(""));

    let base_name = if inputs.len() > 1 {
        parent
            .file_name()
            .map(|s| s.to_string_lossy().into_owned())
            .unwrap_or_else(|| "archive".to_string())
    } else {
        if first_input.is_dir() {
            first_input
                .file_name()
                .map(|s| s.to_string_lossy().into_owned())
                .unwrap_or_else(|| "archive".to_string())
        } else {
            let stem = first_input
                .file_stem()
                .map(|s| s.to_string_lossy().into_owned())
                .unwrap_or_default();

            if stem.is_empty() || stem.starts_with('.') {
                parent
                    .file_name()
                    .map(|s| s.to_string_lossy().into_owned())
                    .unwrap_or_else(|| "archive".to_string())
            } else {
                stem
            }
        }
    };

    let final_base_name = if base_name.is_empty() {
        "archive".to_string()
    } else {
        base_name
    };
    let base_output = parent.join(format!("{}.zip", final_base_name));
    get_available_path(&base_output, false)
}