minecli 0.1.0

A CLI for managing Minecraft server mods, datapacks, and plugins.
use std::fs;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::core::server::{ServerPaths, ServerType};
use crate::error::{IoResultExt, MinecliError, Result};

pub const MINECLI_DIR: &str = ".minecli";
pub const SERVER_FILE: &str = "server.toml";

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerConfig {
    pub name: String,
    pub minecraft_version: String,
    pub server_type: ServerType,
    pub world: String,
    pub paths: ServerPaths,
}

impl ServerConfig {
    pub fn new(
        name: String,
        minecraft_version: String,
        server_type: ServerType,
        world: String,
    ) -> Self {
        let paths = ServerPaths::defaults(&world);
        Self {
            name,
            minecraft_version,
            server_type,
            world,
            paths,
        }
    }
}

pub fn minecli_dir(server_dir: &Path) -> PathBuf {
    server_dir.join(MINECLI_DIR)
}

pub fn server_file(server_dir: &Path) -> PathBuf {
    minecli_dir(server_dir).join(SERVER_FILE)
}

pub fn load_server_config(server_dir: &Path) -> Result<ServerConfig> {
    let path = server_file(server_dir);
    let contents = fs::read_to_string(&path).at(&path)?;
    toml::from_str(&contents).map_err(|source| MinecliError::TomlDeserialize { path, source })
}

pub fn write_server_config(server_dir: &Path, config: &ServerConfig) -> Result<()> {
    let path = server_file(server_dir);
    fs::create_dir_all(minecli_dir(server_dir)).at(minecli_dir(server_dir))?;
    let contents =
        toml::to_string_pretty(config).map_err(|source| MinecliError::TomlSerialize {
            path: path.clone(),
            source,
        })?;
    write_atomic(&path, contents.as_bytes())
}

pub fn write_atomic(path: &Path, contents: &[u8]) -> Result<()> {
    let parent = path.parent().ok_or_else(|| {
        MinecliError::message(format!(
            "cannot write {} without a parent directory",
            path.display()
        ))
    })?;
    let parent = if parent.as_os_str().is_empty() {
        Path::new(".")
    } else {
        parent
    };
    fs::create_dir_all(parent).at(parent)?;

    let temp_path = parent.join(format!(
        ".{}.tmp-{}",
        path.file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("minecli"),
        std::process::id()
    ));
    fs::write(&temp_path, contents).at(&temp_path)?;
    fs::rename(&temp_path, path).at(path)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::core::manifest::{ServerConfig, load_server_config, write_server_config};
    use crate::core::server::ServerType;

    #[test]
    fn round_trips_server_config() {
        let temp = tempfile::tempdir().unwrap();
        let config = ServerConfig::new(
            "survival".to_owned(),
            "1.21.5".to_owned(),
            ServerType::Fabric,
            "world".to_owned(),
        );

        write_server_config(temp.path(), &config).unwrap();
        let loaded = load_server_config(temp.path()).unwrap();

        assert_eq!(loaded, config);
    }
}