use actix_web::{cookie::Key, http::header::HeaderValue};
pub const DEFAULT_CACHE_CONTROL: &str = "public, max-age=31536000, immutable";
#[derive(Clone)]
pub struct Config {
pub port: u16,
pub cache_control: Option<HeaderValue>,
pub db_path: String,
pub reset_db: bool,
pub invite_secret: Option<String>,
pub session_key: Key,
}
impl Config {
pub fn new(
port: u16,
cache_control: impl Into<Option<HeaderValue>>,
db_path: String,
reset_db: bool,
) -> Self {
Self {
port,
cache_control: cache_control.into(),
db_path,
reset_db,
invite_secret: None,
session_key: Key::generate(),
}
}
pub fn invites_enabled(&self) -> bool {
self.invite_secret
.as_deref()
.is_some_and(|secret| !secret.is_empty())
}
pub fn invite_secret_matches(&self, provided: &str) -> bool {
self.invites_enabled() && self.invite_secret.as_deref() == Some(provided)
}
/// Deletes the database file if `RESET_DB` is set to true.
pub fn maybe_reset_database(&self) {
if !self.reset_db {
return;
}
log::warn!(
"RESET_DB is set to true, deleting database file: {}",
self.db_path
);
if std::path::Path::new(&self.db_path).exists() {
if let Err(e) = std::fs::remove_file(&self.db_path) {
log::error!("Failed to delete database file: {e}");
} else {
log::info!("Database file deleted successfully");
}
}
}
}
pub fn parse_cache_control(val: Option<&str>) -> Option<HeaderValue> {
val.and_then(|v| {
let trimmed = v.trim();
if trimmed.is_empty() {
None
} else if trimmed.eq_ignore_ascii_case("true") || trimmed == "1" {
Some(HeaderValue::from_static("no-cache"))
} else {
HeaderValue::from_str(trimmed).ok()
}
})
}
pub fn from_env() -> Config {
let port = std::env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse()
.unwrap_or(8080);
let cache_control = parse_cache_control(std::env::var("CACHE_CONTROL").ok().as_deref());
let db_path = std::env::var("DB_PATH").unwrap_or_else(|_| "pear.db".to_string());
let reset_db = std::env::var("RESET_DB").unwrap_or_default() == "true";
let mut config = Config::new(port, cache_control, db_path, reset_db);
config.invite_secret = std::env::var("INVITE_SECRET")
.ok()
.filter(|secret| !secret.is_empty());
config.session_key = if let Ok(secret) = std::env::var("SESSION_SECRET") {
Key::try_from(secret.as_bytes())
.unwrap_or_else(|_| panic!("SESSION_SECRET must be at least 64 bytes long"))
} else {
log::warn!("SESSION_SECRET is unset; sessions will not survive process restarts");
Key::generate()
};
config
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_cache_control() {
assert_eq!(
parse_cache_control(Some("no-cache")),
Some(HeaderValue::from_static("no-cache"))
);
assert_eq!(
parse_cache_control(Some("true")),
Some(HeaderValue::from_static("no-cache"))
);
assert_eq!(parse_cache_control(Some("")), None);
assert_eq!(parse_cache_control(None), None);
}
#[test]
fn test_maybe_reset_database_with_flag() {
let path = format!(
"/tmp/test_pear_reset_{}.db",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
std::fs::write(&path, b"test").unwrap();
let config = Config::new(
8080,
HeaderValue::from_static(DEFAULT_CACHE_CONTROL),
path.clone(),
true,
);
config.maybe_reset_database();
assert!(!std::path::Path::new(&path).exists());
}
}