pub fn env_with_file_fallback(name: &str) -> Option<String> {
if let Ok(value) = std::env::var(name) {
if !value.trim().is_empty() {
return Some(value);
}
}
let file_var = format!("{name}_FILE");
let path = std::env::var(&file_var).ok()?;
let trimmed_path = path.trim();
if trimmed_path.is_empty() {
return None;
}
match std::fs::read_to_string(trimmed_path) {
Ok(contents) => {
let value = contents.trim_end_matches(['\n', '\r']).to_string();
if value.is_empty() {
None
} else {
Some(value)
}
}
Err(err) => {
tracing::warn!(
target: "reddb::secrets",
env = %file_var,
path = %trimmed_path,
error = %err,
"secret file referenced by {file_var} could not be read; falling back to None"
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
fn env_lock() -> &'static Mutex<()> {
static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
#[test]
fn returns_inline_when_set() {
let _g = env_lock().lock();
unsafe {
std::env::set_var("REDDB_TEST_INLINE", "value-from-env");
std::env::remove_var("REDDB_TEST_INLINE_FILE");
}
assert_eq!(
env_with_file_fallback("REDDB_TEST_INLINE"),
Some("value-from-env".to_string())
);
unsafe {
std::env::remove_var("REDDB_TEST_INLINE");
}
}
#[test]
fn falls_back_to_file_when_inline_empty() {
let _g = env_lock().lock();
let dir =
std::env::temp_dir().join(format!("reddb-env-secret-test-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("token");
std::fs::write(&path, "value-from-file\n").unwrap();
unsafe {
std::env::remove_var("REDDB_TEST_FALLBACK");
std::env::set_var("REDDB_TEST_FALLBACK_FILE", &path);
}
assert_eq!(
env_with_file_fallback("REDDB_TEST_FALLBACK"),
Some("value-from-file".to_string()) );
unsafe {
std::env::remove_var("REDDB_TEST_FALLBACK_FILE");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn inline_wins_over_file() {
let _g = env_lock().lock();
let dir = std::env::temp_dir().join(format!("reddb-env-precedence-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("token");
std::fs::write(&path, "from-file").unwrap();
unsafe {
std::env::set_var("REDDB_TEST_PRIORITY", "from-env");
std::env::set_var("REDDB_TEST_PRIORITY_FILE", &path);
}
assert_eq!(
env_with_file_fallback("REDDB_TEST_PRIORITY"),
Some("from-env".to_string())
);
unsafe {
std::env::remove_var("REDDB_TEST_PRIORITY");
std::env::remove_var("REDDB_TEST_PRIORITY_FILE");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn returns_none_when_neither_set() {
let _g = env_lock().lock();
unsafe {
std::env::remove_var("REDDB_TEST_NONE");
std::env::remove_var("REDDB_TEST_NONE_FILE");
}
assert_eq!(env_with_file_fallback("REDDB_TEST_NONE"), None);
}
#[test]
fn read_failure_returns_none() {
let _g = env_lock().lock();
unsafe {
std::env::remove_var("REDDB_TEST_BAD");
std::env::set_var("REDDB_TEST_BAD_FILE", "/nonexistent/path/zzz");
}
assert_eq!(env_with_file_fallback("REDDB_TEST_BAD"), None);
unsafe {
std::env::remove_var("REDDB_TEST_BAD_FILE");
}
}
}