use std::net::SocketAddr;
use std::path::PathBuf;
use crate::error::ApiError;
use crate::keys::{now_unix_secs, KeyStore};
use crate::tenancy::Tenancy;
pub const DEFAULT_BIND: &str = "127.0.0.1:8080";
pub const DEFAULT_DATA_DIR: &str = "./data";
pub const DEFAULT_KEYS_FILE: &str = "./api_keys";
pub const DEFAULT_METERING_LOG: &str = "./metering.log";
#[derive(Debug, Clone)]
pub struct Config {
pub data_dir: PathBuf,
pub keys_file: PathBuf,
pub metering_log: PathBuf,
pub bind: SocketAddr,
pub index_ttl_secs: Option<u64>,
pub deny_ownerless_indexes: Option<bool>,
}
impl Config {
pub fn from_env() -> Result<Self, ApiError> {
let data_dir = env_path("CODETOPO_DATA_DIR", DEFAULT_DATA_DIR);
let keys_file = env_path("CODETOPO_KEYS_FILE", DEFAULT_KEYS_FILE);
let metering_log = env_path("CODETOPO_METERING_LOG", DEFAULT_METERING_LOG);
let bind = env_string("CODETOPO_BIND", DEFAULT_BIND);
let bind: SocketAddr = bind.parse().map_err(|_| {
ApiError::Internal(format!(
"CODETOPO_BIND must be a 'host:port' socket address, got '{bind}'"
))
})?;
let index_ttl_secs = match std::env::var("CODETOPO_INDEX_TTL_SECS") {
Ok(value) if !value.trim().is_empty() => {
Some(value.trim().parse::<u64>().map_err(|_| {
ApiError::Internal(format!(
"CODETOPO_INDEX_TTL_SECS must be a non-negative integer, got '{value}'"
))
})?)
}
_ => None,
};
let deny_ownerless_indexes = match std::env::var("CODETOPO_DENY_OWNERLESS_INDEXES") {
Ok(value) if !value.trim().is_empty() => {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" => Some(true),
"0" | "false" => Some(false),
_ => {
return Err(ApiError::Internal(format!(
"CODETOPO_DENY_OWNERLESS_INDEXES must be 1/true or 0/false, \
got '{value}'"
)))
}
}
}
_ => None,
};
Ok(Config {
data_dir,
keys_file,
metering_log,
bind,
index_ttl_secs,
deny_ownerless_indexes,
})
}
pub fn index_db_path(&self, index_id: &str) -> PathBuf {
self.data_dir.join(format!("{index_id}.db"))
}
pub fn index_workdir_path(&self, index_id: &str) -> PathBuf {
self.data_dir.join("ingest").join(index_id)
}
pub fn remove_index_files(&self, index_id: &str) {
let db = self.index_db_path(index_id);
if let Err(err) = std::fs::remove_file(&db) {
if err.kind() != std::io::ErrorKind::NotFound {
eprintln!(
"warning: cannot delete index file {}: {err}",
db.display()
);
}
}
if uuid::Uuid::parse_str(index_id).is_ok() {
let workdir = self.index_workdir_path(index_id);
if let Err(err) = std::fs::remove_dir_all(&workdir) {
if err.kind() != std::io::ErrorKind::NotFound {
eprintln!(
"warning: cannot delete ingest workdir {}: {err}",
workdir.display()
);
}
}
}
}
pub fn keys_db_path(&self) -> PathBuf {
self.data_dir.join(crate::keys::STORE_FILE_NAME)
}
pub fn tenancy_db_path(&self) -> PathBuf {
self.data_dir.join(crate::tenancy::STORE_FILE_NAME)
}
pub async fn prepare(&self) -> Result<(KeyStore, Tenancy), ApiError> {
tokio::fs::create_dir_all(&self.data_dir)
.await
.map_err(|err| {
ApiError::Internal(format!(
"cannot create data dir {}: {err}",
self.data_dir.display()
))
})?;
let keys = KeyStore::open(&self.keys_db_path())?;
if keys.active_len() == 0 && self.keys_file.is_file() {
keys.import_legacy_file(&self.keys_file)?;
}
if keys.active_len() == 0 {
return Err(ApiError::Internal(format!(
"no active API keys: issue one with `codetopo-server keys issue <key_id>` \
or provide a keys file at {}",
self.keys_file.display()
)));
}
let tenancy = Tenancy::open(&self.tenancy_db_path())?;
for index_id in tenancy.prune_expired(now_unix_secs())? {
self.remove_index_files(&index_id);
}
Ok((keys, tenancy))
}
}
impl Default for Config {
fn default() -> Self {
Config {
data_dir: PathBuf::from(DEFAULT_DATA_DIR),
keys_file: PathBuf::from(DEFAULT_KEYS_FILE),
metering_log: PathBuf::from(DEFAULT_METERING_LOG),
bind: DEFAULT_BIND.parse().expect("default bind address is valid"),
index_ttl_secs: None,
deny_ownerless_indexes: None,
}
}
}
fn env_string(key: &str, default: &str) -> String {
match std::env::var(key) {
Ok(value) if !value.trim().is_empty() => value,
_ => default.to_string(),
}
}
fn env_path(key: &str, default: &str) -> PathBuf {
PathBuf::from(env_string(key, default))
}