// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Shared plumbing for store-backed subcommands. //! //! Every CLI mutation operates directly on the database — there is no IPC with a //! running `serve`. These helpers turn the loaded configuration into an open //! [`Store`] and give the (otherwise synchronous) command handlers a place to //! block on async work. use std::future::Future; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use config::Config; use store::Store; /// Load configuration, open the store, and apply any pending migrations. /// /// Migrations run on every store-backed command so a fresh install is usable /// immediately: the first `fabrica user add` creates the database file, brings /// the schema up to date, and inserts the row in one step. Migrations are /// idempotent, so this is a cheap no-op once the schema is current. /// /// Configuration warnings are printed to stderr, matching `config check`/`show`. /// /// # Errors /// /// Returns an error if the configuration is invalid, the database cannot be /// opened, or migrations fail. pub(crate) async fn open_store(config_path: Option<&Path>) -> anyhow::Result<(Config, Store)> { let loaded = config::load(config_path)?; for warning in &loaded.warnings { eprintln!("warning: {warning}"); } let store = Store::connect( &loaded.config.database.url, loaded.config.database.max_connections, ) .await?; store.migrate().await?; Ok((loaded.config, store)) } /// Resolve the instance HS256 signing secret, generating and persisting it on /// first use. /// /// Resolution order matches the server's: an inline `auth.secret`, else the /// contents of `auth.secret_file` (default `{data_dir}/secret`) if it exists, else /// a freshly generated 32-byte secret written to that path with mode `0600`. The /// same secret signs API tokens and session material, so `token add` and a future /// `serve` agree on one key. /// /// # Errors /// /// Returns an error if the secret file cannot be read or written. pub(crate) fn ensure_secret(config: &Config) -> anyhow::Result { if let Some(secret) = &config.auth.secret { return Ok(secret.expose().to_string()); } let path = config .auth .secret_file .clone() .unwrap_or_else(|| config.storage.data_dir.join("secret")); if path.exists() { let raw = std::fs::read_to_string(&path)?; return Ok(raw.trim_end_matches(['\r', '\n']).to_string()); } let secret = auth::new_secret(); if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(&path, &secret)?; std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?; Ok(secret) } /// The absolute path to write into repository hooks so they can invoke `fabrica /// hook post-receive`. Uses the running executable (§3.4), falling back to the /// bare command name if it cannot be resolved. pub(crate) fn hook_binary() -> PathBuf { std::env::current_exe().unwrap_or_else(|_| PathBuf::from("fabrica")) } /// Run an async command body to completion on a fresh current-thread runtime. /// /// The CLI is a short-lived, single-shot process, so a current-thread runtime is /// the right size — no worker pool to spin up and tear down per invocation. /// /// # Errors /// /// Propagates any error from building the runtime or from `fut` itself. pub(crate) fn block_on(fut: F) -> anyhow::Result where F: Future>, { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; runtime.block_on(fut) }