From f949939508f4d3682e07bc9a6abf2fa34d8f3a80 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Sat, 1 Aug 2026 14:32:33 +0200 Subject: [PATCH] feat(storage): add cmd option --select-storage --- .../services/blobs_consistency_service.rs | 4 +- src/main.rs | 182 ++++++++++++++++-- 2 files changed, 170 insertions(+), 16 deletions(-) diff --git a/src/infrastructure/services/blobs_consistency_service.rs b/src/infrastructure/services/blobs_consistency_service.rs index 9112352b..ea142bbb 100644 --- a/src/infrastructure/services/blobs_consistency_service.rs +++ b/src/infrastructure/services/blobs_consistency_service.rs @@ -198,9 +198,7 @@ impl RecoverableJobHandler for BlobsConsistencyCheck { && let Err(e) = store.set_string_param(PROBED_STORAGE_PARAM, n).await { return RunOutcome::Failed { - message: format!( - "persist {PROBED_STORAGE_PARAM} to params: {e}" - ), + message: format!("persist {PROBED_STORAGE_PARAM} to params: {e}"), }; } name diff --git a/src/main.rs b/src/main.rs index 8e33fc4a..275edf3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,14 +127,22 @@ fn make_socket(addr: &SocketAddr, reuse_port: bool) -> std::io::Result { fn main() -> Result<(), Box> { // Minimal CLI: - // --version Print version + branch + commit hash and exit. - // --config Load env from this file. When given, the default - // `./.env` probe is INTENTIONALLY skipped — tests - // use this to isolate from a developer's repo-root - // `.env`, and operators get a reproducible "this - // file and nothing else" boot. + // --version Print version + branch + commit hash and exit. + // --config Load env from this file. When given, the default + // `./.env` probe is INTENTIONALLY skipped — tests + // use this to isolate from a developer's repo-root + // `.env`, and operators get a reproducible "this + // file and nothing else" boot. + // --select-storage One-shot repair: verify the named entry exists + // in the current .env, UPDATE + // admin_settings.storage.active_backend_name in + // the DB, and exit. Does NOT boot the server. + // Use to recover from the "boot fails on missing + // entry" case — see + // `docs/plan/storage-multi-entry.md` §Fallback. let mut args = std::env::args().skip(1); let mut config_path: Option = None; + let mut select_storage: Option = None; while let Some(arg) = args.next() { match arg.as_str() { "--version" | "-V" => { @@ -153,11 +161,15 @@ fn main() -> Result<(), Box> { }; config_path = Some(p); } + "--select-storage" => { + let Some(name) = args.next() else { + eprintln!("--select-storage requires an entry name"); + std::process::exit(2); + }; + select_storage = Some(name); + } "--help" | "-h" => { - println!( - "OxiCloud v{}\n\nUSAGE:\n oxicloud [--config ]\n oxicloud --version\n oxicloud --help\n", - env!("CARGO_PKG_VERSION"), - ); + print_help(); return Ok(()); } other => { @@ -195,13 +207,157 @@ fn main() -> Result<(), Box> { } } - // Build the Tokio runtime explicitly (not via `#[tokio::main]`) so the - // worker + blocking pools are sized from the cgroup CPU quota and bounded - // — with the `.env` loaded above already in scope. See `build_runtime`. + // Build the Tokio runtime — needed either way (the repair flag also + // needs async DB access). Sized from cgroup CPU quota — see + // `build_runtime`. let runtime = build_runtime()?; + + // Repair-flag short-circuit. `--select-storage` runs the small + // "verify entry + UPDATE pointer + exit" path and NEVER falls + // through to booting the server — the operator restarts normally + // after this exits. + if let Some(name) = select_storage { + return runtime.block_on(run_select_storage(&name)); + } + runtime.block_on(run()) } +/// Print the `--help` output. Kept as a fn (not an inline string) so +/// the layout is easy to eyeball + doesn't clutter the argv match arm. +/// +/// One `println!` per output line — source layout matches what the +/// user sees. Longer than one big raw string but grep-friendly (a +/// specific flag description shows up as its own hit) and diffs stay +/// line-local. +/// +/// Sections: USAGE (invocation shapes) → OPTIONS (per-flag +/// explanations) → ENVIRONMENT (the small set of env vars an operator +/// checks at first boot). Everything else lives in `example.env` — +/// listing all 80+ env knobs here would rot every release. +fn print_help() { + println!( + "OxiCloud v{} (branch={} commit={})", + env!("CARGO_PKG_VERSION"), + env!("GIT_BRANCH"), + env!("GIT_HASH"), + ); + println!(); + println!("USAGE:"); + println!(" oxicloud [--config ] Boot the server. This is the normal"); + println!(" invocation for a docker/systemd unit."); + println!(); + println!(" oxicloud --select-storage One-shot repair — set the active"); + println!(" storage entry in the DB and exit."); + println!(); + println!(" oxicloud --version Print version + commit and exit."); + println!(); + println!(" oxicloud --help Print this help and exit."); + println!(); + println!(); + println!("OPTIONS:"); + println!(" --config "); + println!(" Load environment variables from instead of the default `./.env`."); + println!(" When set, the file WINS over any pre-existing shell exports (the"); + println!(" overriding variant of dotenvy). Use for reproducible CI / systemd"); + println!(" unit boots where a leaked shell export must not silently corrupt"); + println!(" config. Without this flag, the default `./.env` probe is"); + println!(" non-overriding — shell exports win — matching dev convenience."); + println!(); + println!(" --select-storage "); + println!(" Verify is declared in `OXICLOUD_STORAGE_ENTRIES`, then set"); + println!(" `admin_settings.storage.active_backend_name = ` in the DB and"); + println!(" exit. Does NOT boot the server. Use to unblock boot after renaming"); + println!(" or removing a storage entry in `.env` while the DB still points at"); + println!(" the old name (the server aborts boot with a pointer to this flag"); + println!(" when that happens). See `docs/plan/storage-multi-entry.md`"); + println!(" §Fallback for the full recovery flow."); + println!(); + println!(" --version, -V"); + println!(" Print the version, git branch, and commit hash. Exits 0."); + println!(); + println!(" --help, -h"); + println!(" Print this help. Exits 0."); + println!(); + println!(); + println!("ENVIRONMENT:"); + println!(" DATABASE_URL PostgreSQL connection string (required for boot and"); + println!(" for --select-storage)."); + println!(); + println!(" OXICLOUD_SERVER_HOST Bind host (default: 127.0.0.1)."); + println!(" OXICLOUD_SERVER_PORT Bind port (default: 8086)."); + println!(); + println!(" OXICLOUD_STORAGE_ENTRIES=,,..."); + println!(" Comma-separated list of named storage entries. Each"); + println!(" entry N declared here reads its config from"); + println!(" `OXICLOUD_STORAGE__BACKEND`,"); + println!(" `OXICLOUD_STORAGE__S3_BUCKET`, etc. See"); + println!(" `docs/plan/storage-multi-entry.md` for the full"); + println!(" contract. When unset (with legacy flat storage"); + println!(" vars present) one entry named `default` is"); + println!(" synthesised."); + println!(); + println!("The full env-var surface is documented in `example.env` at the repo root."); +} + +/// Repair-flag body. Loads env config, parses entries, verifies the +/// requested name is declared, connects to PG, upserts +/// `admin_settings.storage.active_backend_name`. Never touches the +/// server — the operator restarts after this exits. +/// +/// Exit codes: +/// - `0` on success. +/// - Non-zero via `std::process::exit` on every failure path (name +/// not declared, DB unreachable, upsert failed). Printed to stderr. +async fn run_select_storage(name: &str) -> Result<(), Box> { + use common::config::AppConfig; + use infrastructure::services::entry_backend::persist_active_backend_name; + + // Parse entries + validate `name` is declared. Loading AppConfig + // here re-runs the same env-parse the server does at boot, so a + // successful --select-storage guarantees a subsequent normal + // boot will find the entry (no drift between the two code paths). + let config = AppConfig::from_env(); + if config.storage_entries.is_empty() { + eprintln!( + "OXICLOUD_STORAGE_ENTRIES is not set (or synthesised — legacy path). \ + `--select-storage` needs at least one named entry to switch to." + ); + std::process::exit(2); + } + if !config.storage_entries.iter().any(|e| e.name == name) { + let available = config + .storage_entries + .iter() + .map(|e| e.name.as_str()) + .collect::>() + .join(", "); + eprintln!( + "entry `{name}` is not declared in OXICLOUD_STORAGE_ENTRIES. Available: [{available}]" + ); + std::process::exit(2); + } + + // Connect to PG using the same DATABASE_URL the server uses. + let db_url = std::env::var("DATABASE_URL").map_err( + |_| "DATABASE_URL not set — `--select-storage` needs the same DB the server would boot on", + )?; + let pool = sqlx::PgPool::connect(&db_url) + .await + .map_err(|e| format!("failed to connect to DATABASE_URL: {e}"))?; + + persist_active_backend_name(&pool, name) + .await + .map_err(|e| { + format!("failed to write admin_settings.storage.active_backend_name = `{name}`: {e}") + })?; + + println!( + "active_backend_name = `{name}` written to admin_settings. Restart the server to switch." + ); + Ok(()) +} + /// Construct the multi-threaded Tokio runtime with explicit, CFS-quota-aware /// pool sizes. ///