tapes-v2 0.1.4-beta

Swap your whole rice in one command. Eject it with one more. Atomic dotfiles switching for Linux.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

use crate::cli::insert::Target;

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AppState {
    pub active_tape: Option<String>,
    pub path: Option<PathBuf>,
    pub targets: Option<HashMap<String, Target>>,
}

impl AppState {
    pub fn default_path() -> Result<PathBuf, anyhow::Error> {
        let local_dir =
            dirs::data_dir().ok_or_else(|| anyhow::anyhow!("Could not get local dir"))?;
        let dir = local_dir.join("tapes");
        if !dir.exists() {
            fs::create_dir_all(&dir)?;
        }
        Ok(dir.join("state.toml"))
    }

    pub fn load_current() -> Result<Self, anyhow::Error> {
        Self::load(Self::default_path()?)
    }

    pub fn save_current(&self) -> Result<(), anyhow::Error> {
        self.save(Self::default_path()?)
    }

    pub fn load<P: AsRef<Path>>(file_path: P) -> Result<Self, anyhow::Error> {
        let path = file_path.as_ref();
        if !path.exists() {
            return Ok(AppState::default());
        }
        let content = fs::read_to_string(path)?;
        if content.trim().is_empty() {
            return Ok(AppState::default());
        }
        match toml::from_str(&content) {
            Ok(state) => Ok(state),
            Err(err) => {
                log::warn!(
                    "Corrupted state file at {}: {}. Falling back to default.",
                    path.display(),
                    err
                );
                if let Err(e) = fs::copy(path, path.with_extension("toml.bak")) {
                    log::debug!("Could not backup corrupted state file: {e}");
                }
                Ok(AppState::default())
            }
        }
    }

    pub fn save<P: AsRef<Path>>(&self, file_path: P) -> Result<(), anyhow::Error> {
        let path = file_path.as_ref();

        let parent = path
            .parent()
            .ok_or_else(|| anyhow::anyhow!("File path has no parent directory"))?;

        let file_name = path
            .file_name()
            .ok_or_else(|| anyhow::anyhow!("Invalid file path"))?;

        let mut temp_file_name = file_name.to_os_string();
        temp_file_name.push(".tmp");
        let temp_path = parent.join(temp_file_name);

        let content = toml::to_string_pretty(self)?;

        fs::write(&temp_path, content)?;
        fs::rename(&temp_path, path)?;

        Ok(())
    }

    pub fn eject(&mut self) -> Result<(), anyhow::Error> {
        self.active_tape = None;
        self.path = None;
        self.targets = None;

        Ok(())
    }
}