warren-cli 0.1.6

Install any CLI tool unlimited times. Every instance is its own world.
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub struct InstanceLayout {
    pub root: PathBuf,
    pub alias: String,
}

impl InstanceLayout {
    pub fn new(instances_dir: &Path, alias: &str) -> Self {
        let root = instances_dir.join(alias);
        Self {
            root,
            alias: alias.to_string(),
        }
    }

    pub fn bin_dir(&self) -> PathBuf {
        self.root.join("bin")
    }
    pub fn home_dir(&self) -> PathBuf {
        self.root.join("home")
    }
    pub fn config_dir(&self) -> PathBuf {
        self.root.join("config")
    }
    pub fn cache_dir(&self) -> PathBuf {
        self.root.join("cache")
    }
    pub fn data_dir(&self) -> PathBuf {
        self.root.join("data")
    }
    pub fn state_dir(&self) -> PathBuf {
        self.root.join("state")
    }
    pub fn runtime_dir(&self) -> PathBuf {
        self.root.join("runtime")
    }
    pub fn tmp_dir(&self) -> PathBuf {
        self.root.join("tmp")
    }
    pub fn logs_dir(&self) -> PathBuf {
        self.root.join("logs")
    }
    pub fn installers_dir(&self) -> PathBuf {
        self.root.join("installers")
    }
    pub fn metadata_path(&self) -> PathBuf {
        self.root.join("metadata.toml")
    }
    pub fn launcher_path(&self) -> PathBuf {
        self.root.join("launcher")
    }

    pub fn exists(&self) -> bool {
        self.root.exists()
    }

    pub fn create(&self) -> Result<()> {
        let dirs = [
            self.bin_dir(),
            self.home_dir(),
            self.config_dir(),
            self.cache_dir(),
            self.data_dir(),
            self.state_dir(),
            self.runtime_dir(),
            self.tmp_dir(),
            self.logs_dir(),
            self.installers_dir(),
        ];
        for dir in &dirs {
            std::fs::create_dir_all(dir)
                .with_context(|| format!("failed to create directory {}", dir.display()))?;
        }
        tracing::debug!(alias = %self.alias, root = %self.root.display(), "created instance layout");
        Ok(())
    }

    pub fn destroy(&self) -> Result<()> {
        if self.root.exists() {
            std::fs::remove_dir_all(&self.root).with_context(|| {
                format!(
                    "failed to remove instance directory {}",
                    self.root.display()
                )
            })?;
        }
        Ok(())
    }

    pub fn disk_usage(&self) -> Result<DiskUsage> {
        Ok(DiskUsage {
            bin: dir_size(&self.bin_dir()),
            config: dir_size(&self.config_dir()),
            cache: dir_size(&self.cache_dir()),
            data: dir_size(&self.data_dir()),
            state: dir_size(&self.state_dir()),
            home: dir_size(&self.home_dir()),
            tmp: dir_size(&self.tmp_dir()),
            logs: dir_size(&self.logs_dir()),
        })
    }
}

#[derive(Debug, Clone)]
pub struct DiskUsage {
    pub bin: u64,
    pub config: u64,
    pub cache: u64,
    pub data: u64,
    pub state: u64,
    pub home: u64,
    pub tmp: u64,
    pub logs: u64,
}

impl DiskUsage {
    pub fn total(&self) -> u64 {
        self.bin
            + self.config
            + self.cache
            + self.data
            + self.state
            + self.home
            + self.tmp
            + self.logs
    }
}

fn dir_size(path: &Path) -> u64 {
    if !path.exists() {
        return 0;
    }
    walkdir::WalkDir::new(path)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| e.file_type().is_file())
        .filter_map(|e| e.metadata().ok())
        .map(|m| m.len())
        .sum()
}

pub fn format_bytes(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!("{:.0} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}