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 playback_dir: PathBuf,
15 pub ffmpeg_bin: String,
16 pub ffprobe_bin: String,
17 pub mediamtx_api_url: String,
18 pub mediamtx_hls_base: String,
19 pub mediamtx_rtsp_base: String,
20 pub mediamtx_webrtc_base: String,
21 pub db_max_connections: u32,
24 pub recorder_enabled: bool,
25 pub mirror_recordings_dir: Option<PathBuf>,
29 pub anr_enabled: bool,
32 pub anr_interval_s: u64,
34 pub anr_max_gap_hours: i64,
36 pub anr_max_attempts: i64,
38 pub default_segment_seconds: i64,
39 pub default_retention_hours: i64,
40 pub default_camera_quota_bytes: u64,
43 pub default_record_audio: bool,
46 pub default_pre_roll_seconds: i64,
49 pub default_post_roll_seconds: i64,
52 pub indexer_interval_s: u64,
53 pub health_interval_s: u64,
54 pub retention_interval_s: u64,
55 pub api_host: String,
56 pub api_port: u16,
57 pub cors_origins: Vec<String>,
58 pub max_recordings_bytes: u64,
60 pub min_free_disk_bytes: u64,
63 pub alert_webhook_url: Option<String>,
65 pub notifier_interval_s: u64,
67 pub ai_enabled: bool,
69 pub ai_max_total_fps: f64,
72 pub default_ai_fps: f64,
73 pub default_ai_width: i64,
74 pub detection_retention_hours: i64,
76 pub snapshot_scheduler_enabled: bool,
79 pub snapshot_scheduler_interval_s: u64,
81 pub snapshot_retention_hours: i64,
83 pub schedule_check_interval_s: u64,
87 pub playback_session_ttl_minutes: i64,
91 pub max_playback_seconds: f64,
93 pub auth_enabled: bool,
98 pub session_ttl_hours: i64,
100 pub auth_cookie_secure: bool,
103 pub bootstrap_admin_user: Option<String>,
105 pub bootstrap_admin_password: Option<String>,
106 pub audit_retention_days: i64,
108 pub overlay_enabled: bool,
114 pub overlay_kind: String,
116 pub overlay_iface: Option<String>,
118 pub rclone_bin: String,
122 pub backup_enabled: bool,
125 pub backup_scheduler_interval_s: u64,
127 pub backup_job_timeout_s: u64,
129 pub backup_max_concurrent_jobs: usize,
132 pub archive_dir: PathBuf,
134 pub archive_max_bytes: u64,
137 pub archive_retention_hours: i64,
139 pub onvif_discovery_timeout_ms: u64,
142 pub onvif_request_timeout_ms: u64,
144 pub isapi_request_timeout_ms: u64,
146 pub smart_check_enabled: bool,
150 pub smart_devices: Vec<String>,
152 pub mdstat_check_enabled: bool,
154 pub smart_check_interval_s: u64,
156 pub readyz_min_recording_percent: f64,
160 pub live_transcode_engine: String,
164 pub vaapi_device: String,
166 pub site_id: Option<String>,
170}
171
172fn var(key: &str) -> Option<String> {
173 env::var(key).ok().filter(|s| !s.trim().is_empty())
174}
175
176fn var_or(key: &str, default: &str) -> String {
177 var(key).unwrap_or_else(|| default.to_string())
178}
179
180fn parse_or<T: std::str::FromStr>(key: &str, default: T) -> T {
181 var(key).and_then(|v| v.parse().ok()).unwrap_or(default)
182}
183
184fn parse_bool(key: &str, default: bool) -> bool {
185 match var(key) {
186 Some(v) => matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"),
187 None => default,
188 }
189}
190
191impl Config {
192 pub fn from_env() -> Self {
193 let data_dir = PathBuf::from(var_or("HELDAR_DATA_DIR", "./data"));
194 let recordings_dir = var("HELDAR_RECORDINGS_DIR")
195 .map(PathBuf::from)
196 .unwrap_or_else(|| data_dir.join("recordings"));
197 let clips_dir = var("HELDAR_CLIPS_DIR")
198 .map(PathBuf::from)
199 .unwrap_or_else(|| data_dir.join("clips"));
200 let snapshots_dir = var("HELDAR_SNAPSHOTS_DIR")
201 .map(PathBuf::from)
202 .unwrap_or_else(|| data_dir.join("snapshots"));
203 let frames_dir = var("HELDAR_FRAMES_DIR")
204 .map(PathBuf::from)
205 .unwrap_or_else(|| data_dir.join("frames"));
206 let playback_dir = var("HELDAR_PLAYBACK_DIR")
207 .map(PathBuf::from)
208 .unwrap_or_else(|| data_dir.join("playback"));
209 let archive_dir = var("HELDAR_ARCHIVE_DIR")
210 .map(PathBuf::from)
211 .unwrap_or_else(|| data_dir.join("archives"));
212
213 let cors_origins = var_or("HELDAR_CORS_ORIGINS", "http://localhost:5173")
214 .split(',')
215 .map(|s| s.trim().to_string())
216 .filter(|s| !s.is_empty())
217 .collect();
218
219 let max_recordings_gb: f64 = parse_or("HELDAR_MAX_RECORDINGS_GB", 20.0);
220 let min_free_disk_gb: f64 = parse_or("HELDAR_MIN_FREE_DISK_GB", 5.0);
221 let default_camera_quota_gb: f64 = parse_or("HELDAR_DEFAULT_CAMERA_QUOTA_GB", 0.0);
222
223 Config {
224 database_url: var_or("HELDAR_DATABASE_URL", "sqlite://./data/heldar.db"),
225 data_dir,
226 recordings_dir,
227 clips_dir,
228 snapshots_dir,
229 frames_dir,
230 playback_dir,
231 ffmpeg_bin: var_or("HELDAR_FFMPEG_BIN", "ffmpeg"),
232 ffprobe_bin: var_or("HELDAR_FFPROBE_BIN", "ffprobe"),
233 mediamtx_api_url: var_or("HELDAR_MEDIAMTX_API_URL", "http://127.0.0.1:9997"),
234 mediamtx_hls_base: var_or("HELDAR_MEDIAMTX_HLS_BASE", "http://127.0.0.1:8888"),
235 mediamtx_rtsp_base: var_or("HELDAR_MEDIAMTX_RTSP_BASE", "rtsp://127.0.0.1:8554"),
236 mediamtx_webrtc_base: var_or("HELDAR_MEDIAMTX_WEBRTC_BASE", "http://127.0.0.1:8889"),
237 db_max_connections: parse_or::<u32>("HELDAR_DB_MAX_CONNECTIONS", 16).clamp(2, 256),
238 recorder_enabled: parse_bool("HELDAR_RECORDER_ENABLED", true),
239 mirror_recordings_dir: var("HELDAR_MIRROR_RECORDINGS_DIR").map(PathBuf::from),
240 anr_enabled: parse_bool("HELDAR_ANR_ENABLED", false),
241 anr_interval_s: parse_or("HELDAR_ANR_INTERVAL_S", 300),
242 anr_max_gap_hours: parse_or("HELDAR_ANR_MAX_GAP_HOURS", 24),
243 anr_max_attempts: parse_or("HELDAR_ANR_MAX_ATTEMPTS", 3),
244 default_segment_seconds: parse_or("HELDAR_DEFAULT_SEGMENT_SECONDS", 60),
245 default_retention_hours: parse_or("HELDAR_DEFAULT_RETENTION_HOURS", 24),
246 default_camera_quota_bytes: (default_camera_quota_gb * 1024.0 * 1024.0 * 1024.0) as u64,
247 default_record_audio: parse_bool("HELDAR_DEFAULT_RECORD_AUDIO", false),
248 default_pre_roll_seconds: parse_or("HELDAR_DEFAULT_PRE_ROLL_SECONDS", 10),
249 default_post_roll_seconds: parse_or("HELDAR_DEFAULT_POST_ROLL_SECONDS", 30),
250 indexer_interval_s: parse_or("HELDAR_INDEXER_INTERVAL_S", 10),
251 health_interval_s: parse_or("HELDAR_HEALTH_INTERVAL_S", 15),
252 retention_interval_s: parse_or("HELDAR_RETENTION_INTERVAL_S", 300),
253 api_host: var_or("HELDAR_API_HOST", "0.0.0.0"),
254 api_port: parse_or("HELDAR_API_PORT", 8000),
255 cors_origins,
256 max_recordings_bytes: (max_recordings_gb * 1024.0 * 1024.0 * 1024.0) as u64,
257 min_free_disk_bytes: (min_free_disk_gb * 1024.0 * 1024.0 * 1024.0) as u64,
258 alert_webhook_url: var("HELDAR_ALERT_WEBHOOK_URL"),
259 notifier_interval_s: parse_or("HELDAR_NOTIFIER_INTERVAL_S", 15),
260 ai_enabled: parse_bool("HELDAR_AI_ENABLED", true),
261 ai_max_total_fps: parse_or("HELDAR_AI_MAX_TOTAL_FPS", 40.0),
262 default_ai_fps: parse_or("HELDAR_DEFAULT_AI_FPS", 5.0),
263 default_ai_width: parse_or("HELDAR_DEFAULT_AI_WIDTH", 1280),
264 detection_retention_hours: parse_or("HELDAR_DETECTION_RETENTION_HOURS", 168),
265 snapshot_scheduler_enabled: parse_bool("HELDAR_SNAPSHOT_SCHEDULER_ENABLED", true),
266 snapshot_scheduler_interval_s: parse_or("HELDAR_SNAPSHOT_SCHEDULER_INTERVAL_S", 60),
267 snapshot_retention_hours: parse_or("HELDAR_SNAPSHOT_RETENTION_HOURS", 168),
268 schedule_check_interval_s: parse_or("HELDAR_SCHEDULE_CHECK_INTERVAL_S", 30),
269 playback_session_ttl_minutes: parse_or("HELDAR_PLAYBACK_SESSION_TTL_MINUTES", 60),
270 max_playback_seconds: parse_or("HELDAR_MAX_PLAYBACK_SECONDS", 7200.0),
271 auth_enabled: parse_bool("HELDAR_AUTH_ENABLED", false),
272 session_ttl_hours: parse_or("HELDAR_SESSION_TTL_HOURS", 12),
273 auth_cookie_secure: parse_bool("HELDAR_AUTH_COOKIE_SECURE", false),
274 bootstrap_admin_user: var("HELDAR_BOOTSTRAP_ADMIN_USER"),
275 bootstrap_admin_password: var("HELDAR_BOOTSTRAP_ADMIN_PASSWORD"),
276 audit_retention_days: parse_or("HELDAR_AUDIT_RETENTION_DAYS", 365),
277 overlay_enabled: parse_bool("HELDAR_OVERLAY_ENABLED", false),
278 overlay_kind: var_or("HELDAR_OVERLAY_KIND", "none"),
279 overlay_iface: var("HELDAR_OVERLAY_IFACE"),
280 rclone_bin: var_or("HELDAR_RCLONE_BIN", "rclone"),
281 backup_enabled: parse_bool("HELDAR_BACKUP_ENABLED", true),
282 backup_scheduler_interval_s: parse_or("HELDAR_BACKUP_SCHEDULER_INTERVAL_S", 60),
283 backup_job_timeout_s: parse_or("HELDAR_BACKUP_JOB_TIMEOUT_S", 3600),
284 backup_max_concurrent_jobs: parse_or::<usize>("HELDAR_BACKUP_MAX_CONCURRENT_JOBS", 2)
285 .max(1),
286 archive_dir,
287 archive_max_bytes: parse_or("HELDAR_ARCHIVE_MAX_BYTES", 10_737_418_240u64),
288 archive_retention_hours: parse_or("HELDAR_ARCHIVE_RETENTION_HOURS", 48),
289 onvif_discovery_timeout_ms: parse_or("HELDAR_ONVIF_DISCOVERY_TIMEOUT_MS", 2000),
290 onvif_request_timeout_ms: parse_or("HELDAR_ONVIF_REQUEST_TIMEOUT_MS", 5000),
291 isapi_request_timeout_ms: parse_or("HELDAR_ISAPI_REQUEST_TIMEOUT_MS", 8000),
292 smart_check_enabled: parse_bool("HELDAR_SMART_CHECK_ENABLED", false),
293 smart_devices: var("HELDAR_SMART_DEVICES")
294 .map(|v| {
295 v.split(',')
296 .map(|s| s.trim().to_string())
297 .filter(|s| !s.is_empty())
298 .collect()
299 })
300 .unwrap_or_default(),
301 mdstat_check_enabled: parse_bool("HELDAR_MDSTAT_CHECK_ENABLED", false),
302 smart_check_interval_s: parse_or("HELDAR_SMART_CHECK_INTERVAL_S", 300),
303 readyz_min_recording_percent: parse_or("HELDAR_READYZ_MIN_RECORDING_PERCENT", 0.0),
304 live_transcode_engine: var_or("HELDAR_LIVE_TRANSCODE_ENGINE", "software"),
305 vaapi_device: var_or("HELDAR_VAAPI_DEVICE", "/dev/dri/renderD128"),
306 site_id: var("HELDAR_SITE_ID"),
307 }
308 }
309
310 pub fn camera_recordings_dir(&self, camera_id: &str) -> PathBuf {
312 self.recordings_dir.join(camera_id)
313 }
314
315 pub fn camera_frames_dir(&self, camera_id: &str) -> PathBuf {
317 self.frames_dir.join(camera_id)
318 }
319}