char/sorcery
static-files based git repo viewer
git clone https://git.t4t.associates/char/sorcery
3665650
main
1use std:: fs; 2use std:: path::{ Component , Path , PathBuf }; 3 4use anyhow:: Result ; 5use axum:: body:: Body ; 6use axum:: extract::{ OriginalUri , Path as AxumPath , State }; 7use axum:: http::{ StatusCode , header}; 8use axum:: response::{ IntoResponse , Redirect , Response }; 9use include_dir::{ Dir , include_dir}; 10 11use crate :: generate::{ MANIFEST_FILE , STATE_FILE , STYLESHEET }; 12use crate :: render:: encode_path; 13 14use super ::{ AppState , WebResult , internal, x_accel}; 15 16/// Bundle first: `cd web && deno task build`. 17static WEB_DIST : Dir < ' _ > =include_dir! ( "$CARGO_MANIFEST_DIR/web/dist" ); 18static FONTS : Dir < ' _ > =include_dir! ( "$CARGO_MANIFEST_DIR/assets/fonts" ); 19 20pub ( super ) fn stage_assets ( cache : & Path ) ->Result <()> { 21// Embedded assets are served via X-Accel like everything else, so nginx 22// provides ETag/Last-Modified/304s. Write only on change to keep the 23// mtime-derived ETags stable across restarts. 24write_asset ( & cache. join ( "css/style.css" ), STYLESHEET . as_bytes ()) ?; 25write_embedded_dir ( & WEB_DIST , & cache. join ( "js" )) ?; 26write_embedded_dir ( & FONTS , & cache. join ( "fonts" )) 27} 28 29fn write_embedded_dir ( dir : & Dir < ' _ >, out : & Path ) ->Result <()> { 30for filein dir. files () { 31write_asset ( & out. join ( file. path ()), file. contents ()) ?; 32} 33for childin dir. dirs () { 34write_embedded_dir ( child, out) ?; 35} 36Ok (()) 37} 38 39fn write_asset ( path : & Path , content : & [ u8 ]) ->Result <()> { 40if fs:: read ( path). ok (). as_deref () !=Some ( content) { 41 fs:: create_dir_all ( path. parent (). expect ( "asset path has a parent" )) ?; 42 fs:: write ( path, content) ?; 43} 44Ok (()) 45} 46 47pub ( super ) async fn index ( State ( state): State < AppState >) ->Response { 48let html = state. snapshot . load (). catalog_html . clone (); 49( 50[( header:: CONTENT_TYPE , "text/html; charset=utf-8" )], 51Body :: from ( html), 52) 53. into_response () 54} 55 56pub ( super ) async fn stylesheet ( State ( state): State < AppState >) ->WebResult < Response > { 57serve_cache_path ( & state, "css/style.css" ) 58} 59 60pub ( super ) async fn js_asset ( 61State ( state): State < AppState >, 62AxumPath ( path): AxumPath < String >, 63) ->WebResult < Response > { 64embedded_asset ( & state, & WEB_DIST , "js" , & path) 65} 66 67pub ( super ) async fn font_asset ( 68State ( state): State < AppState >, 69AxumPath ( path): AxumPath < String >, 70) ->WebResult < Response > { 71embedded_asset ( & state, & FONTS , "fonts" , & path) 72} 73 74fn embedded_asset ( 75state : & AppState , 76dir : & Dir < ' _ >, 77prefix : & str , 78path : & str , 79) ->WebResult < Response > { 80if dir. get_file ( path). is_none () { 81return Err (( StatusCode :: NOT_FOUND , "not found" . into ())); 82} 83serve_cache_path ( state, & format! ( "{prefix}/{}" , encode_path ( path))) 84} 85 86pub ( super ) async fn add_trailing_slash ( OriginalUri ( uri): OriginalUri ) ->Redirect { 87Redirect :: permanent ( & format! ( "{}/" , uri. path ())) 88} 89 90pub ( super ) async fn repo_root ( 91State ( state): State < AppState >, 92OriginalUri ( uri): OriginalUri , 93AxumPath (( user, repo)): AxumPath <( String , String )>, 94) ->WebResult < Response > { 95serve_repo ( state, user, repo, String :: new (), uri. path (). to_owned ()). await 96} 97 98pub ( super ) async fn repo_path ( 99State ( state): State < AppState >, 100OriginalUri ( uri): OriginalUri , 101AxumPath (( user, repo, path)): AxumPath <( String , String , String )>, 102) ->WebResult < Response > { 103serve_repo ( state, user, repo, path, uri. path (). to_owned ()). await 104} 105 106async fn serve_repo ( 107state : AppState , 108user : String , 109name : String , 110path : String , 111request_path : String , 112) ->WebResult < Response > { 113let entry = state. resolve ( & user, & name) ?; 114let relative =requested_file ( & path) ?; 115let output = state. config . repo_cache ( & entry. repository ); 116if !output. join ( STATE_FILE ). is_file () { 117 state. build ( & entry). await . map_err ( internal) ?; 118} 119 120let target = output. join ( & relative); 121if target. is_dir () { 122return Ok ( Redirect :: permanent ( & format! ( "{request_path}/" )). into_response ()); 123} 124if !target. is_file () { 125return Err (( StatusCode :: NOT_FOUND , "page not found" . into ())); 126} 127 128serve_cache_path ( 129& state, 130& format! ( 131"{}/{}/{}" , 132encode_path ( & entry. repository . user ), 133encode_path ( & entry. repository . name ), 134encode_path ( & relative. to_string_lossy ()), 135), 136) 137} 138 139fn serve_cache_path ( state : & AppState , encoded_path : & str ) ->WebResult < Response > { 140x_accel ( & state. config . internal_prefix , encoded_path) 141} 142 143fn requested_file ( path : & str ) ->WebResult < PathBuf > { 144if path. is_empty () { 145return Ok ( PathBuf :: from ( "index.html" )); 146} 147if path ==STATE_FILE || path ==MANIFEST_FILE { 148return Err (( StatusCode :: NOT_FOUND , "page not found" . into ())); 149} 150let directory = path. ends_with ( '/' ); 151let path =Path :: new ( path); 152if path 153. components () 154. any ( |component| !matches! ( component, Component :: Normal ( _))) 155{ 156return Err (( StatusCode :: BAD_REQUEST , "invalid path" . into ())); 157} 158Ok ( if directory{ 159 path. join ( "index.html" ) 160} else { 161 path. to_owned () 162}) 163} 164 165# [ cfg ( test )] 166mod tests{ 167use crate :: generate::{ MANIFEST_FILE , STATE_FILE }; 168 169use super :: requested_file; 170 171# [ test ] 172fn generator_metadata_is_private () { 173assert! ( requested_file ( STATE_FILE ). is_err ()); 174assert! ( requested_file ( MANIFEST_FILE ). is_err ()); 175} 176}