mecha10-core 0.6.2

Mecha10 core runtime and foundational types
Documentation
//! Filesystem utility helpers shared across binaries (CLI, launcher-service, ...).
//!
//! These helpers were previously duplicated locally in each binary's cache-cleanup
//! code; they live here so there's a single implementation to maintain.

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

/// Build a path rooted at the user's home directory by joining the given segments.
///
/// Falls back to `.` (the current directory) if the home directory cannot be
/// determined, matching the previous ad-hoc `dirs::home_dir().unwrap_or_else(...)`
/// behavior duplicated across the CLI, auth, and launcher-service crates.
///
/// # Example
///
/// ```
/// use mecha10_core::fs_utils::home_subpath;
///
/// let path = home_subpath(&[".mecha10", "credentials.json"]);
/// assert!(path.ends_with(".mecha10/credentials.json"));
/// ```
pub fn home_subpath(segments: &[&str]) -> PathBuf {
    let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
    for segment in segments {
        path.push(segment);
    }
    path
}

/// Calculate the total size (in bytes) of a directory, recursively.
///
/// Unreadable directories/entries are silently skipped (best-effort accounting
/// for cache-cleanup UX, not a strict filesystem audit).
pub fn dir_size(path: &Path) -> u64 {
    let mut total = 0;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                total += dir_size(&path);
            } else if let Ok(metadata) = path.metadata() {
                total += metadata.len();
            }
        }
    }
    total
}

/// Format a byte count as a human-readable string (e.g. `1.5 MB`).
pub fn format_size(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = 1024 * KB;
    const GB: u64 = 1024 * MB;

    if bytes >= GB {
        format!("{:.1} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.1} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn format_size_formats_by_magnitude() {
        assert_eq!(format_size(0), "0 B");
        assert_eq!(format_size(512), "512 B");
        assert_eq!(format_size(1024), "1.0 KB");
        assert_eq!(format_size(1024 * 1024), "1.0 MB");
        assert_eq!(format_size(1024 * 1024 * 1024), "1.0 GB");
        assert_eq!(format_size(1536 * 1024 * 1024), "1.5 GB");
    }

    #[test]
    fn dir_size_sums_nested_files() {
        let dir = tempfile::tempdir().expect("tempdir");
        fs::write(dir.path().join("a.txt"), b"hello").unwrap();
        let nested = dir.path().join("nested");
        fs::create_dir(&nested).unwrap();
        fs::write(nested.join("b.txt"), b"world!").unwrap();

        assert_eq!(dir_size(dir.path()), 5 + 6);
    }

    #[test]
    fn dir_size_missing_dir_is_zero() {
        assert_eq!(dir_size(Path::new("/nonexistent/does/not/exist")), 0);
    }
}