refactor(config): require explicit BZOD data directory
This commit is contained in:
1 parent
d398341f01
commit
c2e138f823
55 files changed
+522
-170
No files matched your search
@@ -213,3 +213,34 @@ pub enum RepairCommands {
|
||||
data_dir: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
pub fn data_dir(&self) -> Option<&str> {
|
||||
match self {
|
||||
Commands::Serve { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Backup { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Restore { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Migrate { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Stats { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Validate { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::AuditDestinations { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::CreateAdmin { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::InitAdmin { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Doctor { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Shorten { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Expand { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::CreateUser { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::DeleteUser { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::DisableUser { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::EnableUser { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::ResetPassword { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::ListUsers { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::BackupUser { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::RestoreUser { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::AdminMigrate { data_dir, .. } => data_dir.as_deref(),
|
||||
Commands::Repair { command } => match command {
|
||||
RepairCommands::Registry { data_dir, .. } => data_dir.as_deref(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -277,8 +277,7 @@ pub fn perform_restore(
|
||||
}
|
||||
|
||||
// 5. Run validation on the normalized temp_dir
|
||||
let mut temp_config = Config::load();
|
||||
temp_config.data_dir = temp_dir.clone();
|
||||
let temp_config = Config::load_with_cli(Some(&temp_dir))?;
|
||||
|
||||
// Namespace audit
|
||||
match crate::db::users::audit_slug_namespace(&temp_config) {
|
||||
|
||||
+147
-61
@@ -3,6 +3,45 @@ use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const NX9_BZOD_DATA_DIR_ENV: &str = "NX9_BZOD_DATA_DIR";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ConfigError {
|
||||
MissingDataDir,
|
||||
InvalidConfig(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MissingDataDir => write!(
|
||||
f,
|
||||
"data directory is not configured; set NX9_BZOD_DATA_DIR or use --data-dir=<path>"
|
||||
),
|
||||
Self::InvalidConfig(msg) => write!(f, "configuration error: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DataDirSource {
|
||||
Cli,
|
||||
Env,
|
||||
ConfigFile,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DataDirSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Cli => write!(f, "CLI"),
|
||||
Self::Env => write!(f, "NX9_BZOD_DATA_DIR"),
|
||||
Self::ConfigFile => write!(f, "config.toml"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub host: String,
|
||||
@@ -45,11 +84,23 @@ struct TomlBackupConfig {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Self {
|
||||
// 1. Built-in defaults
|
||||
pub fn load() -> Result<Self, ConfigError> {
|
||||
Self::load_with_cli(None::<&std::path::Path>)
|
||||
}
|
||||
|
||||
pub fn load_with_cli<P: AsRef<std::path::Path>>(
|
||||
cli_data_dir: Option<P>,
|
||||
) -> Result<Self, ConfigError> {
|
||||
Self::load_from_sources(cli_data_dir, true)
|
||||
}
|
||||
|
||||
pub fn load_from_sources<P: AsRef<std::path::Path>>(
|
||||
cli_data_dir: Option<P>,
|
||||
load_dotenv: bool,
|
||||
) -> Result<Self, ConfigError> {
|
||||
// 1. Built-in defaults (no implicit data_dir default)
|
||||
let mut host = "0.0.0.0".to_string();
|
||||
let mut port = 8080u16;
|
||||
let mut data_dir = PathBuf::from("./data");
|
||||
let mut admin_username = "admin".to_string();
|
||||
let mut bootstrap_password_sha256 = "".to_string();
|
||||
let mut session_secret =
|
||||
@@ -63,71 +114,84 @@ impl Config {
|
||||
let mut backup_dir = PathBuf::from("./backups");
|
||||
let mut base_url = None;
|
||||
|
||||
// 2. Load bzod.toml if it exists
|
||||
let mut toml_path = "bzod.toml".to_string();
|
||||
if fs::metadata("bzod.toml").is_err() && fs::metadata("config/bzod.toml").is_ok() {
|
||||
toml_path = "config/bzod.toml".to_string();
|
||||
}
|
||||
if let Ok(toml_content) = fs::read_to_string(&toml_path) {
|
||||
if let Ok(toml_config) = toml::from_str::<TomlConfig>(&toml_content) {
|
||||
if let Some(h) = toml_config.host {
|
||||
host = h;
|
||||
}
|
||||
if let Some(p) = toml_config.port {
|
||||
port = p;
|
||||
}
|
||||
if let Some(d) = toml_config.data_dir {
|
||||
data_dir = PathBuf::from(d);
|
||||
}
|
||||
if let Some(u) = toml_config.admin_username {
|
||||
admin_username = u;
|
||||
}
|
||||
if let Some(s) = toml_config.bootstrap_password_sha256 {
|
||||
bootstrap_password_sha256 = s;
|
||||
}
|
||||
if let Some(sec) = toml_config.session_secret {
|
||||
session_secret = sec;
|
||||
}
|
||||
if let Some(c) = toml_config.cookie_secure {
|
||||
cookie_secure = c;
|
||||
}
|
||||
if let Some(ret) = toml_config.data_retention_days {
|
||||
if ret.eq_ignore_ascii_case("unlimited") {
|
||||
data_retention_days = None;
|
||||
} else if let Ok(parsed) = ret.parse::<i64>() {
|
||||
data_retention_days = Some(parsed);
|
||||
let mut resolved_data_dir: Option<PathBuf> = None;
|
||||
let mut data_dir_source: Option<DataDirSource> = None;
|
||||
|
||||
// 2. Load bzod.toml / config.toml if it exists
|
||||
let possible_tomls = [
|
||||
"bzod.toml",
|
||||
"config/bzod.toml",
|
||||
"config.toml",
|
||||
"config/config.toml",
|
||||
];
|
||||
for toml_path in possible_tomls {
|
||||
if let Ok(toml_content) = fs::read_to_string(toml_path) {
|
||||
if let Ok(toml_config) = toml::from_str::<TomlConfig>(&toml_content) {
|
||||
if let Some(h) = toml_config.host {
|
||||
host = h;
|
||||
}
|
||||
}
|
||||
if let Some(lc) = toml_config.link_check_interval_mins {
|
||||
link_check_interval_mins = lc;
|
||||
}
|
||||
if let Some(ag) = toml_config.aggregation_interval_mins {
|
||||
aggregation_interval_mins = ag;
|
||||
}
|
||||
if let Some(b) = toml_config.backup {
|
||||
if let Some(be) = b.enabled {
|
||||
backup_enabled = be;
|
||||
if let Some(p) = toml_config.port {
|
||||
port = p;
|
||||
}
|
||||
if let Some(bi) = b.interval_mins {
|
||||
backup_interval_mins = bi;
|
||||
if let Some(d) = toml_config.data_dir {
|
||||
if !d.trim().is_empty() {
|
||||
resolved_data_dir = Some(PathBuf::from(d));
|
||||
data_dir_source = Some(DataDirSource::ConfigFile);
|
||||
}
|
||||
}
|
||||
if let Some(bo) = b.out_dir {
|
||||
backup_dir = PathBuf::from(bo);
|
||||
if let Some(u) = toml_config.admin_username {
|
||||
admin_username = u;
|
||||
}
|
||||
}
|
||||
if let Some(bu) = toml_config.base_url {
|
||||
base_url = Some(bu);
|
||||
if let Some(s) = toml_config.bootstrap_password_sha256 {
|
||||
bootstrap_password_sha256 = s;
|
||||
}
|
||||
if let Some(sec) = toml_config.session_secret {
|
||||
session_secret = sec;
|
||||
}
|
||||
if let Some(c) = toml_config.cookie_secure {
|
||||
cookie_secure = c;
|
||||
}
|
||||
if let Some(ret) = toml_config.data_retention_days {
|
||||
if ret.eq_ignore_ascii_case("unlimited") {
|
||||
data_retention_days = None;
|
||||
} else if let Ok(parsed) = ret.parse::<i64>() {
|
||||
data_retention_days = Some(parsed);
|
||||
}
|
||||
}
|
||||
if let Some(lc) = toml_config.link_check_interval_mins {
|
||||
link_check_interval_mins = lc;
|
||||
}
|
||||
if let Some(ag) = toml_config.aggregation_interval_mins {
|
||||
aggregation_interval_mins = ag;
|
||||
}
|
||||
if let Some(b) = toml_config.backup {
|
||||
if let Some(be) = b.enabled {
|
||||
backup_enabled = be;
|
||||
}
|
||||
if let Some(bi) = b.interval_mins {
|
||||
backup_interval_mins = bi;
|
||||
}
|
||||
if let Some(bo) = b.out_dir {
|
||||
backup_dir = PathBuf::from(bo);
|
||||
}
|
||||
}
|
||||
if let Some(bu) = toml_config.base_url {
|
||||
base_url = Some(bu);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Load .env if present
|
||||
let _ = dotenvy::dotenv();
|
||||
if fs::metadata("config/.env").is_ok() {
|
||||
let _ = dotenvy::from_path("config/.env");
|
||||
if load_dotenv {
|
||||
let _ = dotenvy::dotenv();
|
||||
if fs::metadata("config/.env").is_ok() {
|
||||
let _ = dotenvy::from_path("config/.env");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Load from Environment Variables (taking highest precedence)
|
||||
// 4. Load from Environment Variables (taking precedence over config file)
|
||||
if let Ok(h) = env::var("HOST") {
|
||||
host = h;
|
||||
}
|
||||
@@ -136,8 +200,11 @@ impl Config {
|
||||
port = p;
|
||||
}
|
||||
}
|
||||
if let Ok(d_str) = env::var("DATA_DIR") {
|
||||
data_dir = PathBuf::from(d_str);
|
||||
if let Ok(d_str) = env::var(NX9_BZOD_DATA_DIR_ENV) {
|
||||
if !d_str.trim().is_empty() {
|
||||
resolved_data_dir = Some(PathBuf::from(d_str));
|
||||
data_dir_source = Some(DataDirSource::Env);
|
||||
}
|
||||
}
|
||||
if let Ok(u) = env::var("ADMIN_USERNAME") {
|
||||
admin_username = u;
|
||||
@@ -187,7 +254,26 @@ impl Config {
|
||||
base_url = Some(bu);
|
||||
}
|
||||
|
||||
Self {
|
||||
// 5. CLI override (highest precedence)
|
||||
if let Some(cli_dir) = cli_data_dir {
|
||||
let path_ref = cli_dir.as_ref();
|
||||
if !path_ref.as_os_str().is_empty() {
|
||||
resolved_data_dir = Some(path_ref.to_path_buf());
|
||||
data_dir_source = Some(DataDirSource::Cli);
|
||||
}
|
||||
}
|
||||
|
||||
let data_dir = match resolved_data_dir {
|
||||
Some(dir) => dir,
|
||||
None => return Err(ConfigError::MissingDataDir),
|
||||
};
|
||||
|
||||
if let Some(source) = data_dir_source {
|
||||
tracing::info!("Data directory: {}", data_dir.display());
|
||||
tracing::info!("Data directory source: {}", source);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
host,
|
||||
port,
|
||||
data_dir,
|
||||
@@ -202,6 +288,6 @@ impl Config {
|
||||
backup_interval_mins,
|
||||
backup_dir,
|
||||
base_url,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -392,12 +392,11 @@ mod db_init_tests {
|
||||
|
||||
#[test]
|
||||
fn test_db_init() {
|
||||
let temp_dir = PathBuf::from("./temp_test_db_dir");
|
||||
let temp_dir = std::env::temp_dir().join(format!("test_db_init_{}", uuid::Uuid::new_v4()));
|
||||
if temp_dir.exists() {
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
let mut config = Config::load();
|
||||
config.data_dir = temp_dir.clone();
|
||||
let config = Config::load_with_cli(Some(&temp_dir)).unwrap();
|
||||
let db = Db::init(&config);
|
||||
|
||||
// Cleanup
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
//! └── extensions/<extension>/<extension>.db
|
||||
//! ```
|
||||
//!
|
||||
//! `<data_dir>` is `Config.data_dir` (env `DATA_DIR`, default `./data`).
|
||||
//! `<data_dir>` is `Config.data_dir` (CLI `--data-dir`, env `NX9_BZOD_DATA_DIR`, or config file).
|
||||
//! It is not renamed to `database/`. Production Docker, CasaOS, and
|
||||
//! `deploy.sh` bind this physical root.
|
||||
|
||||
|
||||
+7
-1
@@ -13,7 +13,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
let config = Config::load();
|
||||
let config = match Config::load_with_cli(cli.command.data_dir()) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match cli.command {
|
||||
Commands::Serve {
|
||||
|
||||
Reference in new issue
Block a user