fabrica

hanna/fabrica

3854 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Shared plumbing for store-backed subcommands.
6//!
7//! Every CLI mutation operates directly on the database — there is no IPC with a
8//! running `serve`. These helpers turn the loaded configuration into an open
9//! [`Store`] and give the (otherwise synchronous) command handlers a place to
10//! block on async work.
11
12use std::future::Future;
13use std::os::unix::fs::PermissionsExt;
14use std::path::{Path, PathBuf};
15
16use config::Config;
17use store::Store;
18
19/// Load configuration, open the store, and apply any pending migrations.
20///
21/// Migrations run on every store-backed command so a fresh install is usable
22/// immediately: the first `fabrica user add` creates the database file, brings
23/// the schema up to date, and inserts the row in one step. Migrations are
24/// idempotent, so this is a cheap no-op once the schema is current.
25///
26/// Configuration warnings are printed to stderr, matching `config check`/`show`.
27///
28/// # Errors
29///
30/// Returns an error if the configuration is invalid, the database cannot be
31/// opened, or migrations fail.
32pub(crate) async fn open_store(config_path: Option<&Path>) -> anyhow::Result<(Config, Store)> {
33 let loaded = config::load(config_path)?;
34 for warning in &loaded.warnings {
35 eprintln!("warning: {warning}");
36 }
37 let store = Store::connect(
38 &loaded.config.database.url,
39 loaded.config.database.max_connections,
40 )
41 .await?;
42 store.migrate().await?;
43 Ok((loaded.config, store))
44}
45
46/// Resolve the instance HS256 signing secret, generating and persisting it on
47/// first use.
48///
49/// Resolution order matches the server's: an inline `auth.secret`, else the
50/// contents of `auth.secret_file` (default `{data_dir}/secret`) if it exists, else
51/// a freshly generated 32-byte secret written to that path with mode `0600`. The
52/// same secret signs API tokens and session material, so `token add` and a future
53/// `serve` agree on one key.
54///
55/// # Errors
56///
57/// Returns an error if the secret file cannot be read or written.
58pub(crate) fn ensure_secret(config: &Config) -> anyhow::Result<String> {
59 if let Some(secret) = &config.auth.secret {
60 return Ok(secret.expose().to_string());
61 }
62 let path = config
63 .auth
64 .secret_file
65 .clone()
66 .unwrap_or_else(|| config.storage.data_dir.join("secret"));
67 if path.exists() {
68 let raw = std::fs::read_to_string(&path)?;
69 return Ok(raw.trim_end_matches(['\r', '\n']).to_string());
70 }
71 let secret = auth::new_secret();
72 if let Some(parent) = path.parent() {
73 std::fs::create_dir_all(parent)?;
74 }
75 std::fs::write(&path, &secret)?;
76 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
77 Ok(secret)
78}
79
80/// The absolute path to write into repository hooks so they can invoke `fabrica
81/// hook post-receive`. Uses the running executable (§3.4), falling back to the
82/// bare command name if it cannot be resolved.
83pub(crate) fn hook_binary() -> PathBuf {
84 std::env::current_exe().unwrap_or_else(|_| PathBuf::from("fabrica"))
85}
86
87/// Run an async command body to completion on a fresh current-thread runtime.
88///
89/// The CLI is a short-lived, single-shot process, so a current-thread runtime is
90/// the right size — no worker pool to spin up and tear down per invocation.
91///
92/// # Errors
93///
94/// Propagates any error from building the runtime or from `fut` itself.
95pub(crate) fn block_on<F, T>(fut: F) -> anyhow::Result<T>
96where
97 F: Future<Output = anyhow::Result<T>>,
98{
99 let runtime = tokio::runtime::Builder::new_current_thread()
100 .enable_all()
101 .build()?;
102 runtime.block_on(fut)
103}