1use std::env;
2use std::path::PathBuf;
3
4#[derive(Clone, Debug)]
6pub struct Config {
7 pub database_url: String,
8 pub data_dir: PathBuf,
9 pub recordings_dir: PathBuf,
10 pub clips_dir: PathBuf,
11 pub snapshots_dir: PathBuf,
12 pub frames_dir: PathBuf,
13 pub ffmpeg_bin: String,
14 pub ffprobe_bin: String,
15 pub mediamtx_api_url: String,
16 pub mediamtx_hls_base: String,
17 pub mediamtx_rtsp_base: String,
18 pub mediamtx_webrtc_base: String,
19 pub db_max_connections: u32,
22 pub recorder_enabled: bool,
23 pub default_segment_seconds: i64,
24 pub default_retention_hours: i64,
25 pub indexer_interval_s: u64,
26 pub health_interval_s: u64,
27 pub retention_interval_s: u64,
28 pub api_host: String,
29 pub api_port: u16,
30 pub cors_origins: Vec<String>,
31 pub max_recordings_bytes: u64,
33 pub min_free_disk_bytes: u64,
36 pub alert_webhook_url: Option<String>,
38 pub notifier_interval_s: u64,
40 pub ai_enabled: bool,
42 pub ai_max_total_fps: f64,
45 pub default_ai_fps: f64,
46 pub default_ai_width: i64,
47 pub detection_retention_hours: i64,
49 pub auth_enabled: bool,
54 pub session_ttl_hours: i64,
56 pub auth_cookie_secure: bool,
59 pub bootstrap_admin_user: Option<String>,
61 pub bootstrap_admin_password: Option<String>,
62 pub audit_retention_days: i64,
64 pub overlay_enabled: bool,
70 pub overlay_kind: String,
72 pub overlay_iface: Option<String>,
74}
75
76fn var(key: &str) -> Option<String> {
77 env::var(key).ok().filter(|s| !s.trim().is_empty())
78}
79
80fn var_or(key: &str, default: &str) -> String {
81 var(key).unwrap_or_else(|| default.to_string())
82}
83
84fn parse_or<T: std::str::FromStr>(key: &str, default: T) -> T {
85 var(key).and_then(|v| v.parse().ok()).unwrap_or(default)
86}
87
88fn parse_bool(key: &str, default: bool) -> bool {
89 match var(key) {
90 Some(v) => matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"),
91 None => default,
92 }
93}
94
95impl Config {
96 pub fn from_env() -> Self {
97 let data_dir = PathBuf::from(var_or("HELDAR_DATA_DIR", "./data"));
98 let recordings_dir = var("HELDAR_RECORDINGS_DIR")
99 .map(PathBuf::from)
100 .unwrap_or_else(|| data_dir.join("recordings"));
101 let clips_dir = var("HELDAR_CLIPS_DIR")
102 .map(PathBuf::from)
103 .unwrap_or_else(|| data_dir.join("clips"));
104 let snapshots_dir = var("HELDAR_SNAPSHOTS_DIR")
105 .map(PathBuf::from)
106 .unwrap_or_else(|| data_dir.join("snapshots"));
107 let frames_dir = var("HELDAR_FRAMES_DIR")
108 .map(PathBuf::from)
109 .unwrap_or_else(|| data_dir.join("frames"));
110
111 let cors_origins = var_or("HELDAR_CORS_ORIGINS", "http://localhost:5173")
112 .split(',')
113 .map(|s| s.trim().to_string())
114 .filter(|s| !s.is_empty())
115 .collect();
116
117 let max_recordings_gb: f64 = parse_or("HELDAR_MAX_RECORDINGS_GB", 20.0);
118 let min_free_disk_gb: f64 = parse_or("HELDAR_MIN_FREE_DISK_GB", 5.0);
119
120 Config {
121 database_url: var_or("HELDAR_DATABASE_URL", "sqlite://./data/heldar.db"),
122 data_dir,
123 recordings_dir,
124 clips_dir,
125 snapshots_dir,
126 frames_dir,
127 ffmpeg_bin: var_or("HELDAR_FFMPEG_BIN", "ffmpeg"),
128 ffprobe_bin: var_or("HELDAR_FFPROBE_BIN", "ffprobe"),
129 mediamtx_api_url: var_or("HELDAR_MEDIAMTX_API_URL", "http://127.0.0.1:9997"),
130 mediamtx_hls_base: var_or("HELDAR_MEDIAMTX_HLS_BASE", "http://127.0.0.1:8888"),
131 mediamtx_rtsp_base: var_or("HELDAR_MEDIAMTX_RTSP_BASE", "rtsp://127.0.0.1:8554"),
132 mediamtx_webrtc_base: var_or("HELDAR_MEDIAMTX_WEBRTC_BASE", "http://127.0.0.1:8889"),
133 db_max_connections: parse_or::<u32>("HELDAR_DB_MAX_CONNECTIONS", 16).clamp(2, 256),
134 recorder_enabled: parse_bool("HELDAR_RECORDER_ENABLED", true),
135 default_segment_seconds: parse_or("HELDAR_DEFAULT_SEGMENT_SECONDS", 60),
136 default_retention_hours: parse_or("HELDAR_DEFAULT_RETENTION_HOURS", 24),
137 indexer_interval_s: parse_or("HELDAR_INDEXER_INTERVAL_S", 10),
138 health_interval_s: parse_or("HELDAR_HEALTH_INTERVAL_S", 15),
139 retention_interval_s: parse_or("HELDAR_RETENTION_INTERVAL_S", 300),
140 api_host: var_or("HELDAR_API_HOST", "0.0.0.0"),
141 api_port: parse_or("HELDAR_API_PORT", 8000),
142 cors_origins,
143 max_recordings_bytes: (max_recordings_gb * 1024.0 * 1024.0 * 1024.0) as u64,
144 min_free_disk_bytes: (min_free_disk_gb * 1024.0 * 1024.0 * 1024.0) as u64,
145 alert_webhook_url: var("HELDAR_ALERT_WEBHOOK_URL"),
146 notifier_interval_s: parse_or("HELDAR_NOTIFIER_INTERVAL_S", 15),
147 ai_enabled: parse_bool("HELDAR_AI_ENABLED", true),
148 ai_max_total_fps: parse_or("HELDAR_AI_MAX_TOTAL_FPS", 40.0),
149 default_ai_fps: parse_or("HELDAR_DEFAULT_AI_FPS", 5.0),
150 default_ai_width: parse_or("HELDAR_DEFAULT_AI_WIDTH", 1280),
151 detection_retention_hours: parse_or("HELDAR_DETECTION_RETENTION_HOURS", 168),
152 auth_enabled: parse_bool("HELDAR_AUTH_ENABLED", false),
153 session_ttl_hours: parse_or("HELDAR_SESSION_TTL_HOURS", 12),
154 auth_cookie_secure: parse_bool("HELDAR_AUTH_COOKIE_SECURE", false),
155 bootstrap_admin_user: var("HELDAR_BOOTSTRAP_ADMIN_USER"),
156 bootstrap_admin_password: var("HELDAR_BOOTSTRAP_ADMIN_PASSWORD"),
157 audit_retention_days: parse_or("HELDAR_AUDIT_RETENTION_DAYS", 365),
158 overlay_enabled: parse_bool("HELDAR_OVERLAY_ENABLED", false),
159 overlay_kind: var_or("HELDAR_OVERLAY_KIND", "none"),
160 overlay_iface: var("HELDAR_OVERLAY_IFACE"),
161 }
162 }
163
164 pub fn camera_recordings_dir(&self, camera_id: &str) -> PathBuf {
166 self.recordings_dir.join(camera_id)
167 }
168
169 pub fn camera_frames_dir(&self, camera_id: &str) -> PathBuf {
171 self.frames_dir.join(camera_id)
172 }
173}