//! Entry point: read the config, start the web server. //! //! Usage: rustforge [CONFIG.toml] [--listen ADDR] use rustforge::config::Config; use rustforge::web::{self, AppState}; use std::path::PathBuf; const USAGE: &str = "usage: rustforge [CONFIG.toml] [--listen ADDR] Serves the git repositories listed in CONFIG.toml (default: rustforge.toml) as a read-only website, and allows `git clone` over HTTP."; #[tokio::main] async fn main() { if let Err(e) = run().await { eprintln!("rustforge: {e}"); std::process::exit(1); } } async fn run() -> Result<(), String> { // Tiny hand-rolled argument parsing; enough for two options. let mut config_path = PathBuf::from("rustforge.toml"); let mut listen_override = None; let mut args = std::env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { "-h" | "--help" => { println!("{USAGE}"); return Ok(()); } "--listen" => listen_override = Some(args.next().ok_or("--listen needs an address")?), other if other.starts_with('-') => return Err(format!("unknown option {other}\n\n{USAGE}")), other => config_path = PathBuf::from(other), } } let mut config = Config::load(&config_path)?; if let Some(listen) = listen_override { config.server.listen = listen; } let repos = config.validate()?; let listen = config.server.listen.clone(); let state = AppState::new(config.server, repos); let listener = tokio::net::TcpListener::bind(&listen) .await .map_err(|e| format!("cannot listen on {listen}: {e}"))?; eprintln!( "RustForge v{} serving {} repo(s) on http://{listen}/", env!("CARGO_PKG_VERSION"), state.repos.len() ); axum::serve(listener, web::router(state)) .with_graceful_shutdown(async { let _ = tokio::signal::ctrl_c().await; }) .await .map_err(|e| e.to_string()) }