use crate::{cache, config::Config, render, repo, templates}; use askama::Template; use log::error; use std::{ io::{ErrorKind, Read}, path::PathBuf, }; use trillium::{Conn, Handler}; use trillium_router::{Router, RouterConnExt}; #[derive(Clone)] struct PageCacheEntry { time: u64, content: String, } pub fn make_router(config: &'static Config) -> impl Handler { let renderer: &'static _ = Box::leak(Box::new(render::Renderer::new())); let metadata_cache: &'static _ = Box::leak(Box::new(cache::Cache::< String, (repo::RepoMetadata, templates::Directory), >::new(config.repo_cache_size))); let page_cache: &'static _ = Box::leak(Box::new(cache::Cache::< (String, String), PageCacheEntry, >::new(config.page_cache_size))); let client: &'static _ = Box::leak(Box::new(async_lock::Mutex::new( crate::api_client::make_client(), ))); ( trillium_caching_headers::CachingHeaders::new(), Router::new() .get("/", |conn: Conn| async move { conn.ok(crate::templates::Home {}.render().unwrap()) }) .get("/favicon.ico", |conn: Conn| async move { conn.ok(&include_bytes!("../favicon.ico")[..]) .with_response_header("Content-type", "image/x-icon") }) .post("/fetch", move |mut conn: Conn| async move { if let Ok(request_body) = conn.request_body().with_max_len(8192).read_bytes().await { let mut title = String::new(); let mut repo_url = None; let mut commit_hash = None; for (key, val) in form_urlencoded::parse(&request_body) { match key.as_ref() { "title" => { title = val.to_string(); } "repo-url" => { repo_url = Some(val); } "commit" => { commit_hash = Some(val); } _ => {} } } let (Some(repo_url), Some(commit_hash)) = (repo_url, commit_hash) else { return conn.ok("Missing arg"); }; let mut client = client.lock().await; let (repo_index, mut repo_metadata) = crate::api_client::fetch_repo_tree_index_at_commit( &mut client, &repo_url, &commit_hash, None, ) .await .expect("todo handle error"); repo_metadata.title = title; let repo_id_str = crate::api_client::fetch_repo_files( config, &mut client, &repo_index, &mut repo_metadata, None, ) .await .expect("todo handle error"); return conn .with_status(303) .with_response_header("Location", format!("/r/{repo_id_str}")); } conn.ok(crate::templates::Home {}.render().unwrap()) }) .get("/r/:hash/*", move |conn: Conn| { async move { let Some(repo_hash_str) = conn.param("hash") else { return conn.with_status(401); }; let fetch_metadata = |key| { let repo_dir = PathBuf::from(&config.data_dir) .join(crate::SUBDIR_REPOS) .join(key); let mut repo_metadata = match crate::repo::RepoMetadata::read_from_file(&repo_dir) { Ok(v) => v, Err(e) => { if let repo::ReadRepoMetadataError::CannotOpenFile(e) = &e { if e.kind() == ErrorKind::NotFound { return None; } } error!("Reading repo metadata: {e:?}"); return None; } }; let mut root = templates::Directory::default(); for entry in repo_metadata.iter_files() { match entry { Ok(entry) => { let Some(name) = entry.file_path.rsplit('/').next() else { error!("Entry has no name"); continue; }; let path = entry.file_path.split('/').peekable(); root.insert( templates::Entry::File(templates::File { name: name.into(), path: entry.file_path.into(), hash: entry.hash.into(), link: format!( "/r/{repo_hash_str}/{0}", entry.file_path ), }), path, ); } Err(e) => { error!("Reading repo metadata file index: {e:?}") } } } // Save memory repo_metadata.content.clear(); Some((repo_metadata, root)) }; let Some((repo_metadata, root)) = metadata_cache.fetch(repo_hash_str.to_string(), fetch_metadata) else { return conn.with_status(404); }; let invalidate_page_cache = |entry: &PageCacheEntry| { // strict cmp because it's better to spend more on useless invalidations // than to serve outdated pages // (e.g. privacy issues if the author thinks sensible data are removed but we still serve them) entry.time > repo_metadata.time_updated }; if let Some(rendered_page) = page_cache.fetch_cond_only( (repo_hash_str.to_string(), conn.path().to_string()), invalidate_page_cache, ) { conn.ok(rendered_page.content) } else { let Some(served_entry) = root.find(conn.path().split('/')) else { page_cache .update((repo_hash_str.to_string(), conn.path().to_string()), None); return conn.with_status(404); }; let rendered_page = match served_entry { templates::EntryRef::Directory(served_dir) => { let mut served_file = None; for default_filename in config.default_files.split(':') { if let Some(templates::Entry::File(f)) = served_dir.entries.get(default_filename) { served_file = Some(f); break; } } let mut html = String::new(); if let Some(served_file) = served_file { let Some(file_content) = fetch_file(config, repo_hash_str, &served_file.hash) else { return conn.with_status(500); }; let mut file_lang = served_file .name .rsplit('.') .next() .unwrap_or(giallo::PLAIN_GRAMMAR_NAME) .to_string(); file_lang.make_ascii_lowercase(); html = renderer.render(&file_lang, &file_content, true); } else { // TODO something? } let template = crate::templates::Repo { content: html.clone(), root, path: conn.path().split('/').map(|s| s.to_string()).collect(), title: repo_metadata.title, }; template.render().unwrap() } templates::EntryRef::File(served_file) => { let Some(file_content) = fetch_file(config, repo_hash_str, &served_file.hash) else { return conn.with_status(500); }; let mut file_lang = served_file .name .rsplit('.') .next() .unwrap_or(giallo::PLAIN_GRAMMAR_NAME) .to_string(); file_lang.make_ascii_lowercase(); let html = renderer.render(&file_lang, &file_content, true); let template = crate::templates::Repo { content: html.clone(), root, path: conn.path().split('/').map(|s| s.to_string()).collect(), title: repo_metadata.title, }; template.render().unwrap() } }; page_cache.update( (repo_hash_str.to_string(), conn.path().to_string()), Some(PageCacheEntry { time: std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(), content: rendered_page.clone(), }), ); conn.ok(rendered_page) } } }) .get("/e/:secret", |conn: Conn| async move { conn.ok(crate::templates::Home {}.render().unwrap()) }), ) } fn fetch_file(config: &Config, repo_hash: &str, file_hash: &str) -> Option> { let file_path = PathBuf::from(&config.data_dir) .join(crate::SUBDIR_REPOS) .join(repo_hash) .join(file_hash); let mut file = match std::fs::OpenOptions::new().read(true).open(&file_path) { Ok(file) => file, Err(e) => { error!("Cannot open file `{file_path:?}`: {e:?}"); return None; } }; let mut buf = Vec::new(); if let Err(e) = file.read_to_end(&mut buf) { error!("Error reading file `{file_path:?}`: {e:?}"); return None; } Some(buf) }