repos
/ repos-rust master

repos-rust

mirror archived upstream

A minimal self-hosted git browser on Rust axum: bare repos rendered as a website with commits, diffs, syntax-highlighted blobs, atom feeds, and clone over HTTPS.

axumdockergitgit-browsergitoxidegixrustself-hosted

4.5 KB · 120 lines · Rust Raw History
  1use axum::{
  2    extract::{Path, State},
  3    http::header,
  4    response::{IntoResponse, Response},
  5    routing::get,
  6    Router,
  7};
  8
  9use crate::render::{not_found, render};
 10use crate::AppState;
 11
 12pub fn router() -> Router<AppState> {
 13    Router::new()
 14        .route("/{name}/blob/{rev}/{*path}", get(blob_page))
 15        .route("/{name}/raw/{rev}/{*path}", get(blob_raw))
 16}
 17
 18async fn blob_page(
 19    Path((name, rev, path)): Path<(String, String, String)>,
 20    State(state): State<AppState>,
 21) -> Response {
 22    let repo_root = state.config.repo_root.clone();
 23    let clone_base = state.config.clone_base.clone();
 24    let name_for_blocking = name.clone();
 25    let rev_for_blocking = rev.clone();
 26    let path_for_blocking = path.clone();
 27    let result = tokio::task::spawn_blocking(move || -> anyhow::Result<_> {
 28        let repo = crate::git::open(&repo_root, &name_for_blocking)?;
 29        let summary = crate::git::repo_summary(
 30            &crate::git::resolve_path(&repo_root, &name_for_blocking)?,
 31            &clone_base,
 32        )?;
 33        let oid = crate::git::resolve_rev(&repo, &rev_for_blocking)?;
 34        let blob = crate::git::read_blob(&repo, oid, &path_for_blocking)?;
 35        Ok((summary, blob))
 36    })
 37    .await;
 38    match result {
 39        Ok(Ok((summary, blob))) => {
 40            let basename = path.rsplit('/').next().unwrap_or(&path).to_string();
 41            // Syntect on multi-MB sources is seconds of CPU per request;
 42            // past this cap the page offers the raw link instead.
 43            const HIGHLIGHT_CAP: usize = 1024 * 1024;
 44            let too_large = !blob.is_binary && blob.data.len() > HIGHLIGHT_CAP;
 45            let highlighted = if blob.is_binary || too_large {
 46                None
 47            } else {
 48                let text = String::from_utf8_lossy(&blob.data).into_owned();
 49                Some(crate::highlight::highlight(&text, &basename))
 50            };
 51            render(
 52                &state,
 53                "blob.html",
 54                &format!("/{name}/blob/{rev}/{path}"),
 55                minijinja::context! {
 56                    repo => &summary,
 57                    rev => &rev,
 58                    path => &path,
 59                    basename => basename,
 60                    size => blob.size,
 61                    is_binary => blob.is_binary,
 62                    too_large => too_large,
 63                    highlighted => highlighted.map(minijinja::Value::from_safe_string),
 64                },
 65            )
 66        }
 67        Ok(Err(e)) => not_found(&state, &name, e),
 68        Err(e) => {
 69            tracing::error!("blob_page join: {e}");
 70            (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "render error").into_response()
 71        }
 72    }
 73}
 74
 75async fn blob_raw(
 76    Path((name, rev, path)): Path<(String, String, String)>,
 77    State(state): State<AppState>,
 78) -> Response {
 79    let repo_root = state.config.repo_root.clone();
 80    let name_for_blocking = name.clone();
 81    let result = tokio::task::spawn_blocking(move || -> anyhow::Result<_> {
 82        let repo = crate::git::open(&repo_root, &name_for_blocking)?;
 83        let oid = crate::git::resolve_rev(&repo, &rev)?;
 84        let blob = crate::git::read_blob(&repo, oid, &path)?;
 85        let mime = mime_guess::from_path(&path)
 86            .first_or_octet_stream()
 87            .essence_str()
 88            .to_string();
 89        Ok((blob.data, mime))
 90    })
 91    .await;
 92    match result {
 93        Ok(Ok((data, mime))) => {
 94            // Never serve repo content as an active type: an HTML or SVG
 95            // blob rendered inline would run its scripts on this origin
 96            // (stored XSS from any pushed file).
 97            let mime = match mime.as_str() {
 98                "text/html" | "application/xhtml+xml" | "image/svg+xml" | "text/xml"
 99                | "application/xml" => "text/plain".to_string(),
100                _ => mime,
101            };
102            let mut resp = data.into_response();
103            let value = mime
104                .parse()
105                .unwrap_or_else(|_| header::HeaderValue::from_static("application/octet-stream"));
106            resp.headers_mut().insert(header::CONTENT_TYPE, value);
107            resp.headers_mut().insert(
108                header::X_CONTENT_TYPE_OPTIONS,
109                header::HeaderValue::from_static("nosniff"),
110            );
111            resp
112        }
113        Ok(Err(e)) => not_found(&state, &name, e),
114        Err(e) => {
115            tracing::error!("blob_raw join: {e}");
116            (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "error").into_response()
117        }
118    }
119}