use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SitesFile {
#[serde(default)]
pub default_site: Option<String>,
#[serde(default)]
pub sites: BTreeMap<String, SiteEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SiteEntry {
pub endpoint: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
}
impl SitesFile {
pub fn load() -> Self {
let path = match sites_path() {
Ok(p) => p,
Err(_) => return Self::default(),
};
if !path.exists() {
return Self::default();
}
let raw = match fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return Self::default(),
};
if raw.trim().is_empty() {
return Self::default();
}
toml::from_str(&raw).unwrap_or_else(|e| {
eprintln!("warning: ~/.config/oxipage/sites.toml is corrupt: {e}");
Self::default()
})
}
pub fn save(&self) -> Result<()> {
let path = sites_path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
}
let raw = toml::to_string_pretty(self).context("serializing sites")?;
fs::write(&path, &raw).with_context(|| format!("writing {}", path.display()))?;
set_mode_0600(&path)?;
Ok(())
}
pub fn resolve_name<'a>(&'a self, cli_site: Option<&'a str>) -> Option<&'a str> {
if let Some(name) = cli_site
&& !name.is_empty()
&& self.sites.contains_key(name)
{
return Some(name);
}
if let Ok(env) = std::env::var("OXIPAGE_SITE")
&& !env.is_empty()
&& self.sites.contains_key(&env)
{
return self.sites.get_key_value(&env).map(|(k, _)| k.as_str());
}
self.default_site
.as_deref()
.and_then(|name| self.sites.contains_key(name).then_some(name))
}
pub fn exists(&self, name: &str) -> bool {
self.sites.contains_key(name)
}
pub fn get(&self, name: &str) -> Option<&SiteEntry> {
self.sites.get(name)
}
pub fn resolve_endpoint(&self, site_name: Option<&str>) -> Option<String> {
let name = site_name?;
self.sites.get(name).map(|s| s.endpoint.clone())
}
pub fn resolve_token(&self, site_name: Option<&str>) -> Option<String> {
let name = site_name?;
self.sites
.get(name)
.and_then(|s| s.token.as_ref())
.filter(|t| !t.is_empty())
.cloned()
}
pub fn site_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.sites.keys().cloned().collect();
names.sort();
names
}
}
fn sites_path() -> Result<PathBuf> {
let proj = directories::ProjectDirs::from("dev", "oxipage", "oxipage")
.context("could not determine config directory")?;
Ok(proj.config_dir().join("sites.toml"))
}
#[cfg(unix)]
fn set_mode_0600(p: &std::path::Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(p)?.permissions();
perms.set_mode(0o600);
fs::set_permissions(p, perms)?;
Ok(())
}
#[cfg(not(unix))]
fn set_mode_0600(_p: &std::path::Path) -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_sites() -> SitesFile {
let mut sites = BTreeMap::new();
sites.insert(
"selfhost".into(),
SiteEntry {
endpoint: "http://localhost:8787".into(),
token: Some("tok_self".into()),
},
);
sites.insert(
"flyio".into(),
SiteEntry {
endpoint: "https://oxipage.fly.dev".into(),
token: None,
},
);
SitesFile {
default_site: Some("selfhost".into()),
sites,
}
}
#[test]
fn test_resolve_name_flag() {
let sf = sample_sites();
assert_eq!(sf.resolve_name(Some("flyio")), Some("flyio"));
}
#[test]
fn test_resolve_name_default() {
let sf = sample_sites();
assert_eq!(sf.resolve_name(None), Some("selfhost"));
}
#[test]
fn test_resolve_name_none() {
let sf = SitesFile::default();
assert_eq!(sf.resolve_name(None), None);
}
#[test]
fn test_resolve_endpoint() {
let sf = sample_sites();
assert_eq!(
sf.resolve_endpoint(Some("selfhost")).as_deref(),
Some("http://localhost:8787")
);
assert_eq!(sf.resolve_endpoint(None), None);
}
#[test]
fn test_resolve_token_with_token() {
let sf = sample_sites();
assert_eq!(
sf.resolve_token(Some("selfhost")).as_deref(),
Some("tok_self")
);
}
#[test]
fn test_resolve_token_without_token() {
let sf = sample_sites();
assert_eq!(sf.resolve_token(Some("flyio")), None);
}
#[test]
fn test_resolve_token_none() {
let sf = sample_sites();
assert_eq!(sf.resolve_token(None), None);
}
#[test]
fn test_exists() {
let sf = sample_sites();
assert!(sf.exists("selfhost"));
assert!(!sf.exists("nonexistent"));
}
#[test]
fn test_site_names() {
let sf = sample_sites();
let names = sf.site_names();
assert_eq!(names, vec!["flyio", "selfhost"]);
}
#[test]
fn test_roundtrip_serialize() {
let sf = sample_sites();
let raw = toml::to_string_pretty(&sf).unwrap();
let deser: SitesFile = toml::from_str(&raw).unwrap();
assert_eq!(deser.default_site, sf.default_site);
assert_eq!(deser.sites.len(), 2);
assert_eq!(
deser.sites.get("selfhost").unwrap().endpoint,
"http://localhost:8787"
);
assert!(deser.sites.get("flyio").unwrap().token.is_none());
}
}