Skip to main content

codetopo_server/
config.rs

1// Copyright (C) 2026 Nrupal Akolkar
2// SPDX-License-Identifier: AGPL-3.0-or-later
3// Part of codetopo — Owned by Nrupal Akolkar · Built with Muse by Meta.
4
5//! Runtime configuration, all of it from the environment.
6
7use std::net::SocketAddr;
8use std::path::PathBuf;
9
10use crate::error::ApiError;
11use crate::keys::KeyStore;
12
13/// Default bind address: loopback only. Exposing `codetopo-server` beyond the
14/// host is an explicit operator decision, so the default is the safe one.
15pub const DEFAULT_BIND: &str = "127.0.0.1:8080";
16/// Default directory holding one `<index_id>.db` per indexed repository.
17pub const DEFAULT_DATA_DIR: &str = "./data";
18/// Default newline-delimited `key_id:secret` file.
19pub const DEFAULT_KEYS_FILE: &str = "./api_keys";
20/// Default append-only metering log.
21pub const DEFAULT_METERING_LOG: &str = "./metering.log";
22
23/// Resolved paths and bind address for one server process.
24#[derive(Debug, Clone)]
25pub struct Config {
26    /// Directory where index databases live. One `<index_id>.db` per index.
27    pub data_dir: PathBuf,
28    /// File the bearer keys are read from at startup.
29    pub keys_file: PathBuf,
30    /// File metering records are appended to, one JSON object per line.
31    pub metering_log: PathBuf,
32    /// Address the HTTP server listens on.
33    pub bind: SocketAddr,
34}
35
36impl Config {
37    /// Reads configuration from the environment, applying documented defaults.
38    ///
39    /// Recognised variables: `CODETOPO_DATA_DIR`, `CODETOPO_KEYS_FILE`,
40    /// `CODETOPO_METERING_LOG`, `CODETOPO_BIND`. An empty value is treated as
41    /// unset so `CODETOPO_BIND=` behaves like an absent variable.
42    pub fn from_env() -> Result<Self, ApiError> {
43        let data_dir = env_path("CODETOPO_DATA_DIR", DEFAULT_DATA_DIR);
44        let keys_file = env_path("CODETOPO_KEYS_FILE", DEFAULT_KEYS_FILE);
45        let metering_log = env_path("CODETOPO_METERING_LOG", DEFAULT_METERING_LOG);
46        let bind = env_string("CODETOPO_BIND", DEFAULT_BIND);
47        let bind: SocketAddr = bind.parse().map_err(|_| {
48            ApiError::Internal(format!(
49                "CODETOPO_BIND must be a 'host:port' socket address, got '{bind}'"
50            ))
51        })?;
52        Ok(Config {
53            data_dir,
54            keys_file,
55            metering_log,
56            bind,
57        })
58    }
59
60    /// Database path for one index id.
61    pub fn index_db_path(&self, index_id: &str) -> PathBuf {
62        self.data_dir.join(format!("{index_id}.db"))
63    }
64
65    /// Creates `data_dir` if absent and loads the keys file.
66    pub async fn prepare(&self) -> Result<KeyStore, ApiError> {
67        tokio::fs::create_dir_all(&self.data_dir)
68            .await
69            .map_err(|err| {
70                ApiError::Internal(format!(
71                    "cannot create data dir {}: {err}",
72                    self.data_dir.display()
73                ))
74            })?;
75        KeyStore::load(&self.keys_file)
76    }
77}
78
79impl Default for Config {
80    fn default() -> Self {
81        Config {
82            data_dir: PathBuf::from(DEFAULT_DATA_DIR),
83            keys_file: PathBuf::from(DEFAULT_KEYS_FILE),
84            metering_log: PathBuf::from(DEFAULT_METERING_LOG),
85            bind: DEFAULT_BIND.parse().expect("default bind address is valid"),
86        }
87    }
88}
89
90fn env_string(key: &str, default: &str) -> String {
91    match std::env::var(key) {
92        Ok(value) if !value.trim().is_empty() => value,
93        _ => default.to_string(),
94    }
95}
96
97fn env_path(key: &str, default: &str) -> PathBuf {
98    PathBuf::from(env_string(key, default))
99}