gitscope
← rgit / 0.1.2 / src/main.rs
https://dev.kronoz.win/r/rgit.git
src/main.rs · Rust
Blame Raw
  1#![deny(clippy::pedantic)]
  2
  3use std::{
  4    fmt::{Display, Formatter},
  5    net::SocketAddr,
  6    path::PathBuf,
  7    str::FromStr,
  8    sync::Arc,
  9    time::Duration,
 10};
 11
 12use askama::Template;
 13use axum::{
 14    body::Body,
 15    http,
 16    http::{HeaderValue, StatusCode},
 17    response::{IntoResponse, Response},
 18    routing::get,
 19    Extension, Router,
 20};
 21use bat::assets::HighlightingAssets;
 22use clap::Parser;
 23use once_cell::sync::{Lazy, OnceCell};
 24use sha2::digest::FixedOutput;
 25use sha2::Digest;
 26use sled::Db;
 27use syntect::html::ClassStyle;
 28use tokio::{
 29    signal::unix::{signal, SignalKind},
 30    sync::mpsc,
 31};
 32use tower_http::cors::CorsLayer;
 33use tower_layer::layer_fn;
 34use tracing::{error, info, instrument};
 35
 36use crate::{git::Git, layers::logger::LoggingMiddleware};
 37
 38mod database;
 39mod git;
 40mod layers;
 41mod methods;
 42mod syntax_highlight;
 43
 44const CRATE_VERSION: &str = clap::crate_version!();
 45
 46static GLOBAL_CSS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/statics/css/style.css",));
 47static GLOBAL_CSS_HASH: Lazy<Box<str>> = Lazy::new(|| build_asset_hash(GLOBAL_CSS));
 48
 49static HIGHLIGHT_CSS_HASH: OnceCell<Box<str>> = OnceCell::new();
 50static DARK_HIGHLIGHT_CSS_HASH: OnceCell<Box<str>> = OnceCell::new();
 51
 52#[derive(Parser, Debug)]
 53#[clap(author, version, about)]
 54pub struct Args {
 55    /// Path to a directory in which the Sled database should be stored, will be created if it doesn't already exist
 56    ///
 57    /// The Sled database is very quick to generate, so this can be pointed to temporary storage
 58    #[clap(short, long, value_parser)]
 59    db_store: PathBuf,
 60    /// The socket address to bind to (eg. 0.0.0.0:3333)
 61    bind_address: SocketAddr,
 62    /// The path in which your bare Git repositories reside (will be scanned recursively)
 63    scan_path: PathBuf,
 64    /// Configures the metadata refresh interval (eg. "never" or "60s")
 65    #[clap(long, default_value_t = RefreshInterval::Duration(Duration::from_secs(300)))]
 66    refresh_interval: RefreshInterval,
 67}
 68
 69#[derive(Debug, Clone, Copy)]
 70pub enum RefreshInterval {
 71    Never,
 72    Duration(Duration),
 73}
 74
 75impl Display for RefreshInterval {
 76    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 77        match self {
 78            Self::Never => write!(f, "never"),
 79            Self::Duration(s) => write!(f, "{}", humantime::format_duration(*s)),
 80        }
 81    }
 82}
 83
 84impl FromStr for RefreshInterval {
 85    type Err = &'static str;
 86
 87    fn from_str(s: &str) -> Result<Self, Self::Err> {
 88        if s == "never" {
 89            Ok(Self::Never)
 90        } else if let Ok(v) = humantime::parse_duration(s) {
 91            Ok(Self::Duration(v))
 92        } else {
 93            Err("must be seconds, a human readable duration (eg. '10m') or 'never'")
 94        }
 95    }
 96}
 97
 98#[tokio::main]
 99async fn main() -> Result<(), Box<dyn std::error::Error>> {
100    let args: Args = Args::parse();
101
102    let subscriber = tracing_subscriber::fmt();
103    #[cfg(debug_assertions)]
104    let subscriber = subscriber.pretty();
105    subscriber.init();
106
107    let db = sled::Config::default()
108        .use_compression(true)
109        .path(&args.db_store)
110        .open()
111        .unwrap();
112
113    let indexer_wakeup_task =
114        run_indexer(db.clone(), args.scan_path.clone(), args.refresh_interval);
115
116    let bat_assets = HighlightingAssets::from_binary();
117    let syntax_set = bat_assets.get_syntax_set().unwrap().clone();
118
119    let theme = bat_assets.get_theme("GitHub");
120    let css = syntect::html::css_for_theme_with_class_style(theme, ClassStyle::Spaced).unwrap();
121    let css = Box::leak(
122        format!(r#"@media (prefers-color-scheme: light){{{css}}}"#)
123            .into_boxed_str()
124            .into_boxed_bytes(),
125    );
126    HIGHLIGHT_CSS_HASH.set(build_asset_hash(css)).unwrap();
127
128    let dark_theme = bat_assets.get_theme("TwoDark");
129    let dark_css =
130        syntect::html::css_for_theme_with_class_style(dark_theme, ClassStyle::Spaced).unwrap();
131    let dark_css = Box::leak(
132        format!(r#"@media (prefers-color-scheme: dark){{{dark_css}}}"#)
133            .into_boxed_str()
134            .into_boxed_bytes(),
135    );
136    DARK_HIGHLIGHT_CSS_HASH
137        .set(build_asset_hash(dark_css))
138        .unwrap();
139
140    let static_favicon = |content: &'static [u8]| {
141        move || async move {
142            let mut resp = Response::new(Body::from(content));
143            resp.headers_mut().insert(
144                http::header::CONTENT_TYPE,
145                HeaderValue::from_static("image/x-icon"),
146            );
147            resp
148        }
149    };
150
151    let static_css = |content: &'static [u8]| {
152        move || async move {
153            let mut resp = Response::new(Body::from(content));
154            resp.headers_mut().insert(
155                http::header::CONTENT_TYPE,
156                HeaderValue::from_static("text/css"),
157            );
158            resp
159        }
160    };
161
162    let app = Router::new()
163        .route("/", get(methods::index::handle))
164        .route(
165            &format!("/style-{}.css", *GLOBAL_CSS_HASH),
166            get(static_css(GLOBAL_CSS)),
167        )
168        .route(
169            &format!("/highlight-{}.css", HIGHLIGHT_CSS_HASH.get().unwrap()),
170            get(static_css(css)),
171        )
172        .route(
173            &format!(
174                "/highlight-dark-{}.css",
175                DARK_HIGHLIGHT_CSS_HASH.get().unwrap()
176            ),
177            get(static_css(dark_css)),
178        )
179        .route(
180            "/favicon.ico",
181            get(static_favicon(include_bytes!("../statics/favicon.ico"))),
182        )
183        .fallback(methods::repo::service)
184        .layer(layer_fn(LoggingMiddleware))
185        .layer(Extension(Arc::new(Git::new(syntax_set))))
186        .layer(Extension(db))
187        .layer(Extension(Arc::new(args.scan_path)))
188        .layer(CorsLayer::new());
189
190    let server = axum::Server::bind(&args.bind_address)
191        .serve(app.into_make_service_with_connect_info::<SocketAddr>());
192
193    tokio::select! {
194        res = server => res.map_err(Box::from),
195        res = indexer_wakeup_task => res.map_err(Box::from),
196        _ = tokio::signal::ctrl_c() => {
197            info!("Received ctrl-c, shutting down");
198            Ok(())
199        }
200    }
201}
202
203async fn run_indexer(
204    db: Db,
205    scan_path: PathBuf,
206    refresh_interval: RefreshInterval,
207) -> Result<(), tokio::task::JoinError> {
208    let (indexer_wakeup_send, mut indexer_wakeup_recv) = mpsc::channel(10);
209
210    std::thread::spawn(move || loop {
211        info!("Running periodic index");
212        crate::database::indexer::run(&scan_path, &db);
213        info!("Finished periodic index");
214
215        if indexer_wakeup_recv.blocking_recv().is_none() {
216            break;
217        }
218    });
219
220    tokio::spawn({
221        let mut sighup = signal(SignalKind::hangup()).expect("could not subscribe to sighup");
222        let build_sleeper = move || async move {
223            match refresh_interval {
224                RefreshInterval::Never => futures::future::pending().await,
225                RefreshInterval::Duration(v) => tokio::time::sleep(v).await,
226            };
227        };
228
229        async move {
230            loop {
231                tokio::select! {
232                    _ = sighup.recv() => {},
233                    () = build_sleeper() => {},
234                }
235
236                if indexer_wakeup_send.send(()).await.is_err() {
237                    error!("Indexing thread has died and is no longer accepting wakeup messages");
238                }
239            }
240        }
241    })
242    .await
243}
244
245#[must_use]
246pub fn build_asset_hash(v: &[u8]) -> Box<str> {
247    let mut hasher = sha2::Sha256::default();
248    hasher.update(v);
249    let mut out = hex::encode(hasher.finalize_fixed());
250    out.truncate(10);
251    Box::from(out)
252}
253
254#[instrument(skip(t))]
255pub fn into_response<T: Template>(t: &T) -> Response {
256    match t.render() {
257        Ok(body) => {
258            let headers = [(
259                http::header::CONTENT_TYPE,
260                HeaderValue::from_static(T::MIME_TYPE),
261            )];
262
263            (headers, body).into_response()
264        }
265        Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
266    }
267}