AkurAI Build
Menu

Public repository / edcee323076a7fb02387dd40cb06219db46e8579

AkurAI-Drive / client/src/config.rs

Latest change feb7ac86b5e60d55b429eacb7146cf0dc32c6628 - Add native Linux sync client (akurai-drive-sync) by olibuijr

//! Persistent configuration under `~/.config/akurai-drive/` (XDG-aware).

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

pub const DEFAULT_API: &str = "https://akurai-drive.olibuijr.com";

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Backend base URL (no trailing slash).
    #[serde(default)]
    pub api: Option<String>,
    /// Device token from the `login` flow.
    #[serde(default)]
    pub token: Option<String>,
    /// Absolute path of the local sync folder.
    #[serde(default)]
    pub folder: Option<String>,
    /// Bucket name to sync against.
    #[serde(default)]
    pub bucket: Option<String>,
}

/// `~/.config/akurai-drive`, honoring `$XDG_CONFIG_HOME`, falling back to
/// `$HOME/.config`.
pub fn config_dir() -> Result<PathBuf> {
    let base = dirs::config_dir()
        .or_else(|| dirs::home_dir().map(|h| h.join(".config")))
        .context("cannot determine a config directory (no HOME / XDG_CONFIG_HOME)")?;
    Ok(base.join("akurai-drive"))
}

pub fn config_path() -> Result<PathBuf> {
    Ok(config_dir()?.join("config.toml"))
}

pub fn state_db_path() -> Result<PathBuf> {
    Ok(config_dir()?.join("state.sqlite"))
}

impl Config {
    /// Load config, returning a default (empty) config if none exists yet.
    pub fn load() -> Result<Self> {
        let path = config_path()?;
        if !path.exists() {
            return Ok(Self::default());
        }
        let text = std::fs::read_to_string(&path)
            .with_context(|| format!("reading {}", path.display()))?;
        let cfg: Config =
            toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
        Ok(cfg)
    }

    /// Write config back to disk at mode 0600 (owner-only — it holds a token).
    pub fn save(&self) -> Result<()> {
        let dir = config_dir()?;
        std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
        let path = config_path()?;
        let text = toml::to_string_pretty(self)?;
        std::fs::write(&path, text).with_context(|| format!("writing {}", path.display()))?;
        set_owner_only(&path)?;
        Ok(())
    }

    /// Resolve the effective API base: `$AKURAI_DRIVE_API` overrides config,
    /// which overrides the built-in default.
    pub fn resolved_api(&self) -> String {
        if let Ok(env) = std::env::var("AKURAI_DRIVE_API") {
            if !env.trim().is_empty() {
                return env.trim().trim_end_matches('/').to_string();
            }
        }
        self.api
            .clone()
            .unwrap_or_else(|| DEFAULT_API.to_string())
            .trim_end_matches('/')
            .to_string()
    }

    pub fn is_logged_in(&self) -> bool {
        self.token
            .as_deref()
            .map(|t| !t.is_empty())
            .unwrap_or(false)
    }
}

/// chmod 0600 on Unix; no-op elsewhere.
fn set_owner_only(path: &Path) -> Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = std::fs::metadata(path)?.permissions();
        perms.set_mode(0o600);
        std::fs::set_permissions(path, perms)?;
    }
    #[cfg(not(unix))]
    {
        let _ = path;
    }
    Ok(())
}