utmd 0.1.9

Minimalist developer sandbox and disposable VMs for UTM on macOS.
Documentation
use chrono::{DateTime, Utc};
use color_eyre::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VmMetadata {
    pub name: String,
    pub os: Option<String>,
    pub template: Option<String>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct StateFile {
    pub vms: HashMap<String, VmMetadata>,
}

pub fn load(path: &Path) -> Result<StateFile> {
    if !path.exists() {
        return Ok(StateFile::default());
    }

    let content = fs::read_to_string(path)?;
    if content.trim().is_empty() {
        return Ok(StateFile::default());
    }

    let parsed: StateFile = serde_json::from_str(&content).map_err(|e| {
        eyre::eyre!(
            "state file '{}' is corrupted (json parse error): {}",
            path.display(),
            e
        )
    })?;
    Ok(parsed)
}

pub fn save(path: &Path, state: &StateFile) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let tmp = path.with_extension("json.tmp");
    let content = serde_json::to_string_pretty(state)?;
    let mut file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .truncate(true)
        .open(&tmp)?;
    file.write_all(content.as_bytes())?;
    file.flush()?;
    fs::rename(&tmp, path)?;
    Ok(())
}

pub fn upsert_vm(path: &Path, metadata: VmMetadata) -> Result<()> {
    let mut state = load(path)?;
    state.vms.insert(metadata.name.clone(), metadata);
    save(path, &state)
}

pub fn remove_vm(path: &Path, name: &str) -> Result<()> {
    let mut state = load(path)?;
    state.vms.remove(name);
    save(path, &state)
}

pub fn remove_vm_from_state(state: &mut StateFile, name: &str) {
    state.vms.remove(name);
}