use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use serde::Deserialize;
pub use crate::error::ConfigError;
#[derive(Debug, Default, Deserialize)]
pub struct AnytypeHeadlessConfig {
#[serde(default, rename = "accountKey")]
pub account_key: Option<String>,
#[serde(default, rename = "sessionToken")]
pub session_token: Option<String>,
#[serde(default, rename = "accountId")]
pub account_id: Option<String>,
}
pub fn default_headless_config_path() -> Result<PathBuf, ConfigError> {
let home = env::var_os("HOME")
.filter(|value| !value.is_empty())
.or({
#[cfg(windows)]
{
env::var_os("USERPROFILE").filter(|value| !value.is_empty())
}
#[cfg(not(windows))]
{
None
}
})
.ok_or(ConfigError::MissingHome)?;
Ok(PathBuf::from(home).join(".anytype").join("config.json"))
}
pub fn load_headless_config(
path: Option<&Path>,
) -> Result<Option<AnytypeHeadlessConfig>, ConfigError> {
let path = match path {
Some(path) => path.to_path_buf(),
None => default_headless_config_path()?,
};
let content = match fs::read_to_string(&path) {
Ok(content) => content,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
match fs::symlink_metadata(&path) {
Err(metadata_error) if metadata_error.kind() == std::io::ErrorKind::NotFound => {
return Ok(None);
}
_ => return Err(error.into()),
}
}
Err(error) => return Err(error.into()),
};
let config = serde_json::from_str(&content)?;
Ok(Some(config))
}
#[cfg(test)]
mod tests {
use std::{
fs,
sync::atomic::{AtomicU64, Ordering},
};
use super::*;
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn missing_config_is_distinct_from_unreadable_config() {
let root = temp_path("missing-vs-unreadable");
fs::create_dir(&root).expect("create test directory");
assert!(
load_headless_config(Some(&root.join("missing.json")))
.expect("missing config is allowed")
.is_none()
);
assert!(load_headless_config(Some(&root)).is_err());
fs::remove_dir(root).expect("remove test directory");
}
#[cfg(unix)]
#[test]
fn broken_config_symlink_is_not_treated_as_missing() {
use std::os::unix::fs::symlink;
let root = temp_path("broken-symlink");
fs::create_dir(&root).expect("create test directory");
let path = root.join("config.json");
symlink(root.join("absent-target.json"), &path).expect("create broken symlink");
assert!(load_headless_config(Some(&path)).is_err());
fs::remove_dir_all(root).expect("remove test directory");
}
#[test]
fn loads_account_credentials_from_headless_config() {
let path = temp_path("credentials");
fs::write(
&path,
r#"{"accountId":"account-id","accountKey":"account-key","sessionToken":"session-token"}"#,
)
.expect("write config");
let config = load_headless_config(Some(&path))
.expect("load config")
.expect("config exists");
assert_eq!(config.account_id.as_deref(), Some("account-id"));
assert_eq!(config.account_key.as_deref(), Some("account-key"));
assert_eq!(config.session_token.as_deref(), Some("session-token"));
fs::remove_file(path).expect("remove config");
}
fn temp_path(label: &str) -> PathBuf {
let unique = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"anytype-rpc-config-{label}-{}-{unique}",
std::process::id()
))
}
}