Krolyxon
Commit 4030c43

ferreniumsrcmain.rs

183 lines · 5.3 KB · Rust

1use std::path::PathBuf;
2use std::process::ExitCode;
3
4use anyhow::Context;
5use clap::{Parser, Subcommand, ValueEnum};
6
7use ferrenium::config::{self, Config, Listen};
8use ferrenium::index::Indexer;
9use ferrenium::render::time;
10use ferrenium::state::{AppState, Overrides};
11use ferrenium::{examples, logging, server};
12
13#[derive(Parser)]
14#[command(
15 name = "ferrenium",
16 version,
17 about = "A fast, read-only Git repository browser"
18)]
19struct Cli {
20 #[command(subcommand)]
21 command: Command,
22}
23
24#[derive(Subcommand)]
25enum Command {
26 /// Run the server in the foreground (this is what systemd runs).
27 Serve {
28 /// Config file [default: /etc/ferrenium/ferrenium.toml, then
29 /// $XDG_CONFIG_HOME/ferrenium/ferrenium.toml]
30 #[arg(long, env = "FERRENIUM_CONFIG")]
31 config: Option<PathBuf>,
32 /// Override server.listen (HOST:PORT or unix:/path/to.sock).
33 #[arg(long)]
34 listen: Option<Listen>,
35 },
36 /// Validate the config and report what would be served.
37 Check {
38 #[arg(long, env = "FERRENIUM_CONFIG")]
39 config: Option<PathBuf>,
40 },
41 /// Print a ready-to-use template to stdout.
42 Example {
43 #[arg(value_enum)]
44 kind: ExampleKind,
45 },
46}
47
48#[derive(Clone, Copy, ValueEnum)]
49enum ExampleKind {
50 /// ferrenium.toml with every option.
51 Config,
52 /// Sandboxed systemd unit.
53 Systemd,
54 /// Caddy site block.
55 Caddy,
56}
57
58fn main() -> ExitCode {
59 let cli = Cli::parse();
60 let result = match cli.command {
61 Command::Serve { config, listen } => serve(config, listen),
62 Command::Check { config } => check(config),
63 Command::Example { kind } => {
64 print!(
65 "{}",
66 match kind {
67 ExampleKind::Config => examples::CONFIG,
68 ExampleKind::Systemd => examples::SYSTEMD,
69 ExampleKind::Caddy => examples::CADDY,
70 }
71 );
72 Ok(())
73 }
74 };
75 match result {
76 Ok(()) => ExitCode::SUCCESS,
77 Err(e) => {
78 eprintln!("error: {e:#}");
79 ExitCode::FAILURE
80 }
81 }
82}
83
84fn serve(config_path: Option<PathBuf>, listen: Option<Listen>) -> anyhow::Result<()> {
85 let overrides = Overrides {
86 config_path: config::resolve_path(config_path.as_deref()),
87 listen,
88 };
89 let config = overrides.load()?;
90 logging::init(&config);
91 match &config.source {
92 Some(path) => tracing::info!("config: {}", path.display()),
93 None => tracing::warn!("no config file found; using built-in defaults"),
94 }
95
96 let state = AppState::new(config, overrides);
97 tokio::runtime::Builder::new_multi_thread()
98 .enable_all()
99 .thread_name("ferrenium")
100 .build()
101 .context("cannot start async runtime")?
102 .block_on(server::run(state))
103}
104
105fn check(config_path: Option<PathBuf>) -> anyhow::Result<()> {
106 let path = config::resolve_path(config_path.as_deref());
107 let config = Config::load(path.as_deref())?;
108 let mut warnings = Vec::new();
109
110 let source = match &config.source {
111 Some(p) => p.display().to_string(),
112 None => {
113 warnings.push("no config file found; built-in defaults apply".to_string());
114 "(built-in defaults)".to_string()
115 }
116 };
117 println!("config {source}");
118 println!("listen {}", config.server.listen);
119 println!("base_path {}", config.server.base_path);
120 if let Some(url) = &config.server.base_url {
121 println!("base_url {url}");
122 }
123 println!(
124 "scan_path {} (max depth {})",
125 config.repos.scan_path.display(),
126 config.repos.max_depth
127 );
128 if !config.repos.scan_path.is_dir() {
129 warnings.push(format!(
130 "repos.scan_path {} is not a readable directory",
131 config.repos.scan_path.display()
132 ));
133 }
134 if config.server.base_url.is_none() {
135 warnings.push("server.base_url is not set; feeds and clone URLs will be relative".into());
136 }
137
138 let indexer = Indexer::new();
139 let stats = indexer.full_scan(&config);
140 let index = indexer.load();
141 println!(
142 "\nrepositories {} visible, {} hidden (scanned in {:.1} ms)",
143 stats.total,
144 stats.hidden,
145 stats.elapsed.as_secs_f64() * 1000.0
146 );
147 if !index.repos.is_empty() {
148 let width = index
149 .repos
150 .keys()
151 .map(|n| n.chars().count())
152 .max()
153 .unwrap_or(0);
154 let now = time::now();
155 for repo in index.repos.values() {
156 let m = &repo.meta;
157 println!(
158 " {:<width$} {:<12} {:>3} br {:>3} tags {:<14} {}",
159 repo.name,
160 m.default_branch.as_deref().unwrap_or("-"),
161 m.branches,
162 m.tags,
163 m.last_activity
164 .map_or_else(|| "empty".into(), |t| time::relative(t, now)),
165 m.section
166 .as_deref()
167 .map(|s| format!("[{s}]"))
168 .unwrap_or_default(),
169 );
170 }
171 }
172 for name in &index.hidden {
173 println!(" {name} (hidden)");
174 }
175 warnings.extend(index.warnings.iter().cloned());
176
177 println!();
178 for w in &warnings {
179 println!("warning: {w}");
180 }
181 println!("ok");
182 Ok(())
183}