use anyhow::{anyhow, Context, Result};
use note_to_self_lib::{decode_bytes, encode_bytes, DerivedKeys};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub default_journal: String,
pub editor: Option<String>,
pub sync: Option<SyncConfig>,
pub device_id: Uuid,
pub username: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_keys: Option<CachedAccountKeys>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncConfig {
pub server_url: String,
pub username: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedAccountKeys {
pub encryption_key: String,
pub auth_key: String,
}
impl Config {
pub fn new(
username: Option<String>,
default_journal: String,
server_url: Option<String>,
) -> Self {
let sync = server_url.map(|server_url| SyncConfig {
server_url: normalize_server_url(&server_url),
username: username.clone().unwrap_or_default(),
});
Self {
default_journal,
editor: None,
sync,
device_id: Uuid::now_v7(),
username,
account_keys: None,
}
}
pub fn sync_username(&self) -> Option<&str> {
self.sync
.as_ref()
.map(|sync| sync.username.as_str())
.or(self.username.as_deref())
.filter(|username| !username.is_empty())
}
pub fn set_account_keys(&mut self, keys: &DerivedKeys) {
self.account_keys = Some(CachedAccountKeys {
encryption_key: encode_bytes(&keys.encryption_key),
auth_key: encode_bytes(&keys.auth_key),
});
}
pub fn cached_account_keys(&self) -> Result<Option<DerivedKeys>> {
let Some(keys) = &self.account_keys else {
return Ok(None);
};
Ok(Some(DerivedKeys {
encryption_key: decode_key(&keys.encryption_key, "cached encryption key")?,
auth_key: decode_key(&keys.auth_key, "cached auth key")?,
}))
}
}
fn decode_key(encoded: &str, name: &str) -> Result<[u8; 32]> {
let bytes = decode_bytes(encoded).with_context(|| format!("invalid {name}"))?;
bytes
.try_into()
.map_err(|bytes: Vec<u8>| anyhow!("{name} has length {}, expected 32", bytes.len()))
}
pub fn note_home() -> Result<PathBuf> {
if let Ok(path) = std::env::var("NOTE_TO_SELF_HOME").or_else(|_| std::env::var("JRNL2_HOME")) {
return Ok(PathBuf::from(path));
}
let home = dirs::home_dir().ok_or_else(|| anyhow!("could not find home directory"))?;
Ok(home.join(".note-to-self"))
}
pub fn config_path(root: &Path) -> PathBuf {
root.join("config.toml")
}
pub fn load_config(root: &Path) -> Result<Option<Config>> {
let path = config_path(root);
if !path.exists() {
return Ok(None);
}
let raw =
fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
let config =
toml::from_str(&raw).with_context(|| format!("failed to parse {}", path.display()))?;
Ok(Some(config))
}
pub fn save_config(root: &Path, config: &Config) -> Result<()> {
fs::create_dir_all(root).with_context(|| format!("failed to create {}", root.display()))?;
let raw = toml::to_string_pretty(config).context("failed to serialize config")?;
let path = config_path(root);
fs::write(&path, raw).context("failed to write config")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(&path)?.permissions();
permissions.set_mode(0o600);
fs::set_permissions(&path, permissions)
.with_context(|| format!("failed to secure {}", path.display()))?;
}
Ok(())
}
pub fn normalize_server_url(server_url: &str) -> String {
server_url.trim().trim_end_matches('/').to_string()
}
pub fn device_node_id(device_id: Uuid) -> u64 {
let bytes = device_id.as_bytes();
u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
}