geist_supervisor 0.1.28

Generic OTA supervisor for field devices
Documentation
use anyhow::Result;
use std::path::PathBuf;
use std::process::Command;

/// Centralized path management for the supervisor
///
/// Provides consistent paths using XDG-compliant local directories.
pub struct SupervisorPaths;

impl SupervisorPaths {
    /// Get the base supervisor data directory: ~/.local/share/geist-supervisor
    pub fn data_dir() -> Result<PathBuf> {
        let home = get_home_directory();
        Ok(PathBuf::from(home).join(".local/share/geist-supervisor"))
    }

    /// Get backups directory: ~/.local/share/geist-supervisor/backups
    pub fn backups_dir() -> Result<PathBuf> {
        Ok(Self::data_dir()?.join("backups"))
    }

    /// Get config directory: ~/.local/share/geist-supervisor/config
    pub fn config_dir() -> Result<PathBuf> {
        Ok(Self::data_dir()?.join("config"))
    }

    /// Get logs directory: ~/.local/share/geist-supervisor/logs
    pub fn logs_dir() -> Result<PathBuf> {
        Ok(Self::data_dir()?.join("logs"))
    }

    /// Get OTA metadata file path: ~/.local/share/geist-supervisor/ota_metadata.json
    pub fn ota_metadata_path() -> Result<PathBuf> {
        Ok(Self::data_dir()?.join("ota_metadata.json"))
    }

    /// Ensure all necessary directories exist
    pub fn ensure_directories() -> Result<()> {
        let dirs = [
            Self::data_dir()?,
            Self::backups_dir()?,
            Self::config_dir()?,
            Self::logs_dir()?,
        ];

        for dir in &dirs {
            if !dir.exists() {
                std::fs::create_dir_all(dir).map_err(|e| {
                    anyhow::anyhow!("Failed to create directory {}: {}", dir.display(), e)
                })?;
            }
        }

        Ok(())
    }
}

/// Get the current user's home directory
///
/// This function tries multiple methods to determine the home directory:
/// 1. HOME environment variable
/// 2. whoami command + /home/{username}
/// 3. USER environment variable + /home/{username}
/// 4. Falls back to /tmp as last resort
pub fn get_home_directory() -> String {
    // First try HOME environment variable
    if let Ok(home) = std::env::var("HOME") {
        if !home.is_empty() {
            return home;
        }
    }

    // Try to get username from whoami command
    if let Ok(output) = Command::new("whoami").output() {
        if output.status.success() {
            let username = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !username.is_empty() && username != "root" {
                return format!("/home/{}", username);
            }
        }
    }

    // Try USER environment variable
    if let Ok(user) = std::env::var("USER") {
        let username = user.split_whitespace().next().unwrap_or("").to_string();
        if !username.is_empty() && username != "root" {
            return format!("/home/{}", username);
        }
    }

    // Try LOGNAME as fallback
    if let Ok(logname) = std::env::var("LOGNAME") {
        let username = logname.split_whitespace().next().unwrap_or("").to_string();
        if !username.is_empty() && username != "root" {
            return format!("/home/{}", username);
        }
    }

    // Last resort fallback to /tmp for safety
    "/tmp".to_string()
}

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

    #[test]
    fn test_path_construction() {
        let data_dir = SupervisorPaths::data_dir().unwrap();
        assert!(data_dir
            .to_string_lossy()
            .contains(".local/share/geist-supervisor"));

        let backups_dir = SupervisorPaths::backups_dir().unwrap();
        assert!(backups_dir.to_string_lossy().contains("backups"));
    }

    #[test]
    fn test_ensure_directories() {
        let result = SupervisorPaths::ensure_directories();
        let _ = result;
    }

    #[test]
    fn test_get_home_dir() {
        let home = get_home_directory();
        assert!(!home.is_empty());
    }
}