1#![deny(clippy::pedantic)]
2
3use std::net::SocketAddr;
4use std::path::PathBuf;
5use std::{sync::Arc, time::Duration};
6
7use askama::Template;
8use axum::{
9 body::Body,
10 http,
11 http::{HeaderValue, StatusCode},
12 response::{IntoResponse, Response},
13 routing::get,
14 Extension, Router,
15};
16use bat::assets::HighlightingAssets;
17use clap::Parser;
18use syntect::html::ClassStyle;
19use tower_http::cors::CorsLayer;
20use tower_layer::layer_fn;
21use tracing::{info, instrument};
22
23use crate::{git::Git, layers::logger::LoggingMiddleware};
24
25mod database;
26mod git;
27mod layers;
28mod methods;
29mod syntax_highlight;
30
31const CRATE_VERSION: &str = clap::crate_version!();
32
33#[derive(Parser, Debug)]
34#[clap(author, version, about)]
35pub struct Args {
36 /// Path to a directory in which the Sled database should be stored, will be created if it doesn't already exist
37 ///
38 /// The Sled database is very quick to generate, so this can be pointed to temporary storage
39 #[clap(short, long, value_parser)]
40 db_store: PathBuf,
41 /// The socket address to bind to (eg. 0.0.0.0:3333)
42 bind_address: SocketAddr,
43 /// The path in which your bare Git repositories reside (will be scanned recursively)
44 scan_path: PathBuf,
45}
46
47#[tokio::main]
48async fn main() {
49 let args: Args = Args::parse();
50
51 let subscriber = tracing_subscriber::fmt();
52 #[cfg(debug_assertions)]
53 let subscriber = subscriber.pretty();
54 subscriber.init();
55
56 let db = sled::Config::default()
57 .use_compression(true)
58 .path(&args.db_store)
59 .open()
60 .unwrap();
61
62 std::thread::spawn({
63 let db = db.clone();
64 let scan_path = args.scan_path.clone();
65
66 move || loop {
67 info!("Running periodic index");
68 crate::database::indexer::run(&scan_path, &db);
69 info!("Finished periodic index");
70
71 std::thread::sleep(Duration::from_secs(300));
72 }
73 });
74
75 let bat_assets = HighlightingAssets::from_binary();
76 let syntax_set = bat_assets.get_syntax_set().unwrap().clone();
77
78 let theme = bat_assets.get_theme("GitHub");
79 let css = syntect::html::css_for_theme_with_class_style(theme, ClassStyle::Spaced).unwrap();
80 let css = Box::leak(
81 format!(r#"@media (prefers-color-scheme: light){{{css}}}"#)
82 .into_boxed_str()
83 .into_boxed_bytes(),
84 );
85
86 let dark_theme = bat_assets.get_theme("TwoDark");
87 let dark_css =
88 syntect::html::css_for_theme_with_class_style(dark_theme, ClassStyle::Spaced).unwrap();
89 let dark_css = Box::leak(
90 format!(r#"@media (prefers-color-scheme: dark){{{dark_css}}}"#)
91 .into_boxed_str()
92 .into_boxed_bytes(),
93 );
94
95 let static_favicon = |content: &'static [u8]| {
96 move || async move {
97 let mut resp = Response::new(Body::from(content));
98 resp.headers_mut().insert(
99 http::header::CONTENT_TYPE,
100 HeaderValue::from_static("image/x-icon"),
101 );
102 resp
103 }
104 };
105
106 let static_css = |content: &'static [u8]| {
107 move || async move {
108 let mut resp = Response::new(Body::from(content));
109 resp.headers_mut().insert(
110 http::header::CONTENT_TYPE,
111 HeaderValue::from_static("text/css"),
112 );
113 resp
114 }
115 };
116
117 let app = Router::new()
118 .route("/", get(methods::index::handle))
119 .route(
120 "/style.css",
121 get(static_css(include_bytes!(concat!(
122 env!("OUT_DIR"),
123 "/statics/css/style.css"
124 )))),
125 )
126 .route("/highlight.css", get(static_css(css)))
127 .route("/highlight-dark.css", get(static_css(dark_css)))
128 .route(
129 "/favicon.ico",
130 get(static_favicon(include_bytes!("../statics/favicon.ico"))),
131 )
132 .fallback(methods::repo::service)
133 .layer(layer_fn(LoggingMiddleware))
134 .layer(Extension(Arc::new(Git::new(syntax_set))))
135 .layer(Extension(db))
136 .layer(Extension(Arc::new(args.scan_path)))
137 .layer(CorsLayer::new());
138
139 axum::Server::bind(&args.bind_address)
140 .serve(app.into_make_service_with_connect_info::<std::net::SocketAddr>())
141 .await
142 .unwrap();
143}
144
145#[instrument(skip(t))]
146pub fn into_response<T: Template>(t: &T) -> Response {
147 match t.render() {
148 Ok(body) => {
149 let headers = [(
150 http::header::CONTENT_TYPE,
151 HeaderValue::from_static(T::MIME_TYPE),
152 )];
153
154 (headers, body).into_response()
155 }
156 Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
157 }
158}