use anyhow::Result;
use serde::Deserialize;
use std::path::{Path, PathBuf};
use super::opencrabs_install::is_valid_install_id;
use crate::config::{atomic_write, keys_path, opencrabs_home, write_secret_keys};
const SECTION: &str = "providers.opencrabs";
const ID_FILE: &str = "install_id";
#[derive(Clone, PartialEq, Eq)]
pub struct StoredInstall {
pub install_id: String,
pub token: String,
}
impl std::fmt::Debug for StoredInstall {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StoredInstall")
.field("install_id", &self.install_id)
.field("token", &"<redacted>")
.finish()
}
}
#[derive(Deserialize, Default)]
struct KeysView {
#[serde(default)]
providers: ProvidersView,
}
#[derive(Deserialize, Default)]
struct ProvidersView {
#[serde(default)]
opencrabs: Option<OpencrabsView>,
}
#[derive(Deserialize)]
struct OpencrabsView {
install_id: Option<String>,
api_key: Option<String>,
}
pub fn install_id_path() -> PathBuf {
opencrabs_home().join(ID_FILE)
}
pub fn read_id_file() -> Option<String> {
read_id_file_at(&install_id_path())
}
fn read_id_file_at(path: &Path) -> Option<String> {
let raw = match std::fs::read_to_string(path) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => {
tracing::warn!("Could not read the install ID file: {e}");
return None;
}
};
let id = raw.trim();
if id.is_empty() {
tracing::warn!("The install ID file is empty: {}", path.display());
return None;
}
if !is_valid_install_id(id) {
tracing::warn!(
"The install ID file does not hold a valid ID: {}",
path.display()
);
return None;
}
Some(id.to_string())
}
pub fn load_install_id() -> Option<String> {
let legacy = read_keys_section(&keys_path()).and_then(|section| section.install_id);
resolve_id(&install_id_path(), legacy)
}
pub(crate) fn write_install_id(install_id: &str) -> std::io::Result<()> {
write_install_id_at(&install_id_path(), install_id)
}
fn write_install_id_at(path: &Path, install_id: &str) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
atomic_write(path, &format!("{install_id}\n"))
}
pub fn load() -> Option<StoredInstall> {
load_from(&install_id_path(), &keys_path())
}
fn load_from(id_path: &Path, keys: &Path) -> Option<StoredInstall> {
let section = read_keys_section(keys)?;
let token = section
.api_key
.as_deref()
.filter(|token| !token.is_empty())?
.to_string();
let install_id = resolve_id(id_path, section.install_id)?;
Some(StoredInstall { install_id, token })
}
fn resolve_id(id_path: &Path, legacy: Option<String>) -> Option<String> {
if let Some(id) = read_id_file_at(id_path) {
return Some(id);
}
let legacy = legacy.filter(|id| is_valid_install_id(id))?;
match write_install_id_at(id_path, &legacy) {
Ok(()) => tracing::info!(
"Migrated the built-in provider install ID into {}",
id_path.display()
),
Err(e) => tracing::warn!(
"Could not migrate the install ID into {} (keeping keys.toml as the only copy): {e}",
id_path.display()
),
}
Some(legacy)
}
fn read_keys_section(path: &Path) -> Option<OpencrabsView> {
let raw = match std::fs::read_to_string(path) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => {
tracing::warn!("Could not read keys.toml for the built-in provider: {e}");
return None;
}
};
match toml::from_str::<KeysView>(&raw) {
Ok(view) => view.providers.opencrabs,
Err(e) => {
tracing::warn!("keys.toml does not parse, built-in provider token unavailable: {e}");
None
}
}
}
pub fn save(install: &StoredInstall) -> Result<()> {
if let Err(e) = write_install_id_at(&install_id_path(), &install.install_id) {
tracing::warn!(
"Could not write the install ID file, keys.toml is the only copy of the identity: {e}"
);
}
write_secret_keys(
SECTION,
&[
("install_id", install.install_id.as_str()),
("api_key", install.token.as_str()),
],
)
}
pub fn token_store_is_writable() -> bool {
token_store_is_writable_at(&keys_path())
}
fn token_store_is_writable_at(path: &Path) -> bool {
match std::fs::read_to_string(path) {
Ok(raw) => raw.parse::<toml_edit::DocumentMut>().is_ok(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
Err(_) => false,
}
}