forks/sorcery
git clone https://git.gae.moe/forks/sorcery
2688dec
typst_support
1use std:: fs; 2use std:: path::{ Component , Path , PathBuf }; 3 4use anyhow:: Result ; 5use axum:: body::{ Body , Bytes }; 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 { 48html_response ( state. snapshot . load (). catalog_html . clone ()) 49} 50 51pub ( super ) async fn user_root ( 52State ( state): State < AppState >, 53AxumPath ( user): AxumPath < String >, 54) ->WebResult < Response > { 55let html = state. snapshot . load (). user_html . get ( & user). cloned (); 56 html. map ( html_response) 57. ok_or_else ( ||( StatusCode :: NOT_FOUND , "not found" . into ())) 58} 59 60/// Unlike repos, only redirect known users: this route also catches 61/// top-level strays like `/favicon.ico`. 62pub ( super ) async fn user_redirect ( 63State ( state): State < AppState >, 64OriginalUri ( uri): OriginalUri , 65AxumPath ( user): AxumPath < String >, 66) ->WebResult < Redirect > { 67if !state. snapshot . load (). user_html . contains_key ( & user) { 68return Err (( StatusCode :: NOT_FOUND , "not found" . into ())); 69} 70Ok ( add_trailing_slash ( OriginalUri ( uri)). await ) 71} 72 73fn html_response ( html : Bytes ) ->Response { 74( 75[( header:: CONTENT_TYPE , "text/html; charset=utf-8" )], 76Body :: from ( html), 77) 78. into_response () 79} 80 81pub ( super ) async fn stylesheet ( State ( state): State < AppState >) ->WebResult < Response > { 82serve_cache_path ( & state, "css/style.css" ) 83} 84 85pub ( super ) async fn js_asset ( 86State ( state): State < AppState >, 87AxumPath ( path): AxumPath < String >, 88) ->WebResult < Response > { 89embedded_asset ( & state, & WEB_DIST , "js" , & path) 90} 91 92pub ( super ) async fn font_asset ( 93State ( state): State < AppState >, 94AxumPath ( path): AxumPath < String >, 95) ->WebResult < Response > { 96embedded_asset ( & state, & FONTS , "fonts" , & path) 97} 98 99fn embedded_asset ( 100state : & AppState , 101dir : & Dir < ' _ >, 102prefix : & str , 103path : & str , 104) ->WebResult < Response > { 105if dir. get_file ( path). is_none () { 106return Err (( StatusCode :: NOT_FOUND , "not found" . into ())); 107} 108serve_cache_path ( state, & format! ( "{prefix}/{}" , encode_path ( path))) 109} 110 111pub ( super ) async fn add_trailing_slash ( OriginalUri ( uri): OriginalUri ) ->Redirect { 112Redirect :: permanent ( & format! ( "{}/" , uri. path ())) 113} 114 115pub ( super ) async fn repo_root ( 116State ( state): State < AppState >, 117OriginalUri ( uri): OriginalUri , 118AxumPath (( user, repo)): AxumPath <( String , String )>, 119) ->WebResult < Response > { 120serve_repo ( state, user, repo, String :: new (), uri. path (). to_owned ()). await 121} 122 123pub ( super ) async fn repo_path ( 124State ( state): State < AppState >, 125OriginalUri ( uri): OriginalUri , 126AxumPath (( user, repo, path)): AxumPath <( String , String , String )>, 127) ->WebResult < Response > { 128serve_repo ( state, user, repo, path, uri. path (). to_owned ()). await 129} 130 131async fn serve_repo ( 132state : AppState , 133user : String , 134name : String , 135path : String , 136request_path : String , 137) ->WebResult < Response > { 138let entry = state. resolve ( & user, & name) ?; 139let relative =requested_file ( & path) ?; 140let output = state. config . repo_cache ( & entry. repository ); 141if !output. join ( STATE_FILE ). is_file () { 142 state. build ( & entry, true ). await . map_err ( internal) ?; 143} 144 145let target = output. join ( & relative); 146if target. is_dir () { 147return Ok ( Redirect :: permanent ( & format! ( "{request_path}/" )). into_response ()); 148} 149if !target. is_file () { 150return Err (( StatusCode :: NOT_FOUND , "page not found" . into ())); 151} 152 153serve_cache_path ( 154& state, 155& format! ( 156"{}/{}/{}" , 157encode_path ( & entry. repository . user ), 158encode_path ( & entry. repository . name ), 159encode_path ( & relative. to_string_lossy ()), 160), 161) 162} 163 164fn serve_cache_path ( state : & AppState , encoded_path : & str ) ->WebResult < Response > { 165x_accel ( & state. config . internal_prefix , encoded_path) 166} 167 168fn requested_file ( path : & str ) ->WebResult < PathBuf > { 169if path. is_empty () { 170return Ok ( PathBuf :: from ( "index.html" )); 171} 172if path ==STATE_FILE || path ==MANIFEST_FILE { 173return Err (( StatusCode :: NOT_FOUND , "page not found" . into ())); 174} 175let directory = path. ends_with ( '/' ); 176let path =Path :: new ( path); 177if path 178. components () 179. any ( |component| !matches! ( component, Component :: Normal ( _))) 180{ 181return Err (( StatusCode :: BAD_REQUEST , "invalid path" . into ())); 182} 183Ok ( if directory{ 184 path. join ( "index.html" ) 185} else { 186 path. to_owned () 187}) 188} 189 190# [ cfg ( test )] 191mod tests{ 192use crate :: generate::{ MANIFEST_FILE , STATE_FILE }; 193 194use super :: requested_file; 195 196# [ test ] 197fn generator_metadata_is_private () { 198assert! ( requested_file ( STATE_FILE ). is_err ()); 199assert! ( requested_file ( MANIFEST_FILE ). is_err ()); 200} 201}