blindforge/src/server.rs

275 lines
7.8 KiB
Rust

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<Vec<u8>> {
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)
}