use std::process::ExitCode;
use clap::{Parser, Subcommand};
mod doctor;
mod emergency_ui;
mod group;
mod migrate;
mod perm;
mod scaffold;
mod user;
#[derive(Parser)]
#[command(
name = "rustio",
version,
about = "The rustio-admin command-line tool."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
#[command(name = "startproject")]
Startproject {
name: String,
},
#[command(name = "startapp")]
Startapp {
name: String,
},
Migrate {
#[command(subcommand)]
action: migrate::Action,
},
User {
#[command(subcommand)]
action: user::Action,
},
Group {
#[command(subcommand)]
action: group::Action,
},
Perm {
#[command(subcommand)]
action: perm::Action,
},
Doctor,
}
fn main() -> ExitCode {
let _ = dotenvy::dotenv();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let cli = Cli::parse();
let result = match cli.command {
Command::Startproject { name } => scaffold::project(&name),
Command::Startapp { name } => scaffold::app(&name),
other => tokio_run(async {
match other {
Command::Startproject { .. } | Command::Startapp { .. } => {
unreachable!("handled above")
}
Command::Migrate { action } => migrate::run(action).await,
Command::User { action } => user::run(action).await,
Command::Group { action } => group::run(action).await,
Command::Perm { action } => perm::run(action).await,
Command::Doctor => doctor::run().await,
}
}),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
fn tokio_run<F>(fut: F) -> Result<(), String>
where
F: std::future::Future<Output = Result<(), String>>,
{
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|e| format!("tokio runtime: {e}"))?
.block_on(fut)
}
pub(crate) async fn db() -> Result<rustio_admin::Db, String> {
let url = std::env::var("DATABASE_URL").map_err(|_| {
"DATABASE_URL is not set. Add it to .env or your shell environment.".to_string()
})?;
rustio_admin::Db::connect(&url)
.await
.map_err(|e| format!("could not connect to {}: {e}", redact_password(&url)))
}
fn redact_password(url: &str) -> String {
if let Some(at) = url.rfind('@') {
if let Some(scheme_end) = url.find("://") {
let prefix = &url[..scheme_end + 3];
let creds_and_after = &url[scheme_end + 3..];
let creds_end = at - (scheme_end + 3);
let creds = &creds_and_after[..creds_end];
let after = &creds_and_after[creds_end..];
if let Some((user, _)) = creds.split_once(':') {
return format!("{prefix}{user}:***{after}");
}
}
}
url.to_string()
}
#[cfg(test)]
mod tests {
use super::redact_password;
#[test]
fn redact_strips_password() {
assert_eq!(
redact_password("postgres://postgres:secret@localhost/db"),
"postgres://postgres:***@localhost/db"
);
}
#[test]
fn redact_passthrough_when_no_password() {
assert_eq!(
redact_password("postgres://localhost/db"),
"postgres://localhost/db"
);
}
}