Jacob Schmidt 4532e7b73d Add SurrealDB-backed phone storage and message deletion
- Wire phone, garage, and locker stores to the new storage layer
- Add delete flows for messages and emails in the phone UI
- Update contact, mail, and message views for the new data model
2026-04-11 22:36:11 -05:00

216 lines
6.6 KiB
Rust

//! Configuration management for Redis connection and application settings.
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;
use crate::log::log;
static CONFIG_CACHE: OnceLock<Config> = OnceLock::new();
/// Main configuration structure for the entire application.
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
/// Durable storage backend selector.
#[serde(default)]
pub storage: StorageConfig,
/// Redis configuration with automatic defaults if not specified
#[serde(default)]
pub redis: RedisConfig,
/// SurrealDB configuration with automatic defaults if not specified
#[serde(default)]
pub surreal: SurrealConfig,
}
impl Default for Config {
/// Creates a default configuration with sensible values for development.
fn default() -> Self {
Self {
storage: StorageConfig::default(),
redis: RedisConfig::default(),
surreal: SurrealConfig::default(),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum StorageBackend {
Redis,
Surreal,
}
impl Default for StorageBackend {
fn default() -> Self {
Self::Redis
}
}
/// Durable storage backend selection.
#[derive(Debug, Clone, Deserialize)]
pub struct StorageConfig {
#[serde(default)]
pub backend: StorageBackend,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
backend: StorageBackend::Redis,
}
}
}
/// Redis connection and connection pool configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct RedisConfig {
/// Redis server hostname or IP address
pub host: String,
/// Redis server port number
pub port: u16,
/// Redis database number (0-15)
pub db: u8,
/// Username for Redis ACL authentication (Redis 6.0+)
pub username: Option<String>,
/// Password for Redis authentication
pub password: Option<String>,
/// Maximum number of connections in the pool
pub max_connections: Option<usize>,
/// Minimum number of idle connections to maintain
pub min_connections: Option<usize>,
/// Idle connection timeout in seconds
pub idle_timeout: Option<u64>,
/// Maximum time to wait for pool connection checkout in milliseconds
pub pool_get_timeout_ms: Option<u64>,
/// Maximum time to wait for individual Redis command execution in milliseconds
pub command_timeout_ms: Option<u64>,
/// Maximum time to wait for pool connection establishment in milliseconds
pub connect_timeout_ms: Option<u64>,
}
impl Default for RedisConfig {
/// Creates default Redis configuration suitable for local development.
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 6379,
db: 0,
username: None,
password: None,
max_connections: Some(10),
min_connections: Some(2),
idle_timeout: Some(60),
pool_get_timeout_ms: Some(2000),
command_timeout_ms: Some(2000),
connect_timeout_ms: Some(2000),
}
}
}
/// SurrealDB connection configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct SurrealConfig {
/// SurrealDB HTTP endpoint, for example `127.0.0.1:8000`.
pub endpoint: String,
/// SurrealDB namespace.
pub namespace: String,
/// SurrealDB database.
pub database: String,
/// Optional root username for authentication.
pub username: Option<String>,
/// Optional root password for authentication.
pub password: Option<String>,
/// Maximum time to wait for initial connection in milliseconds.
pub connect_timeout_ms: Option<u64>,
}
impl Default for SurrealConfig {
fn default() -> Self {
Self {
endpoint: "127.0.0.1:8000".to_string(),
namespace: "forge".to_string(),
database: "main".to_string(),
username: Some("root".to_string()),
password: Some("root".to_string()),
connect_timeout_ms: Some(5000),
}
}
}
impl RedisConfig {
/// Generates a Redis connection string from the configuration.
pub fn connection_string(&self) -> String {
// Build authentication part of the URL
let auth_part = match (&self.username, &self.password) {
(Some(username), Some(password)) => format!("{}:{}@", username, password),
(None, Some(password)) => format!(":{}@", password),
(Some(username), None) => format!("{}@", username),
(None, None) => String::new(),
};
let mut conn_str = format!("redis://{}{}", auth_part, self.host);
if self.port != 6379 {
conn_str.push_str(&format!(":{}", self.port));
}
if self.db != 0 {
conn_str.push_str(&format!("/{}", self.db));
}
log(
"main",
"INFO",
&format!("Redis connection string: {}", conn_str),
);
conn_str
}
}
/// Loads configuration from the `config.toml` file with graceful fallback to defaults.
pub fn load() -> Config {
CONFIG_CACHE
.get_or_init(|| {
let config_path = std::env::current_exe()
.ok()
.and_then(|exe| {
exe.parent()
.map(|dir| dir.join("@forge_server").join("config.toml"))
})
.filter(|p| p.exists())
.unwrap_or_else(|| PathBuf::from("@forge_server/config.toml"));
match fs::read_to_string(&config_path) {
Ok(contents) => {
log("main", "INFO", &format!("Config file found! Loading..."));
match toml::from_str::<Config>(&contents) {
Ok(config) => config,
Err(error) => {
log(
"main",
"ERROR",
&format!(
"Failed to parse config file '{}': {}. Using defaults.",
config_path.display(),
error
),
);
Config::default()
}
}
}
Err(_) => {
log(
"main",
"INFO",
&format!("Config file not found. Using default configuration."),
);
Config::default()
}
}
})
.clone()
}