Serve empty directories, page cache
This commit is contained in:
parent
fbf3d7640a
commit
ed769102de
12 changed files with 339 additions and 112 deletions
204
src/server.rs
204
src/server.rs
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{cache, config::Config, repo, templates};
|
||||
use crate::{cache, config::Config, render, repo, templates};
|
||||
|
||||
use askama::Template;
|
||||
use log::error;
|
||||
|
|
@ -9,17 +9,23 @@ use std::{
|
|||
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 mut hl_registry = giallo::Registry::builtin().unwrap();
|
||||
hl_registry.link_grammars();
|
||||
let hl_registry: &'static _ = Box::leak(Box::new(hl_registry));
|
||||
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),
|
||||
>::default()));
|
||||
let file_cache: &'static _ =
|
||||
Box::leak(Box::new(cache::Cache::<(String, String), String>::default()));
|
||||
>::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(),
|
||||
)));
|
||||
|
|
@ -30,6 +36,10 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
.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
|
||||
{
|
||||
|
|
@ -65,7 +75,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
.await
|
||||
.expect("todo handle error");
|
||||
repo_metadata.title = title;
|
||||
crate::api_client::fetch_repo_files(
|
||||
let repo_id_str = crate::api_client::fetch_repo_files(
|
||||
config,
|
||||
&mut client,
|
||||
&repo_index,
|
||||
|
|
@ -74,6 +84,9 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
)
|
||||
.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())
|
||||
})
|
||||
|
|
@ -88,7 +101,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
.join(crate::SUBDIR_REPOS)
|
||||
.join(key);
|
||||
let mut repo_metadata =
|
||||
match crate::repo::RepoMetadata::read_from_file(config, &repo_dir) {
|
||||
match crate::repo::RepoMetadata::read_from_file(&repo_dir) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
if let repo::ReadRepoMetadataError::CannotOpenFile(e) = &e {
|
||||
|
|
@ -113,7 +126,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
root.insert(
|
||||
templates::Entry::File(templates::File {
|
||||
name: name.into(),
|
||||
path: dbg!(entry.file_path).into(),
|
||||
path: entry.file_path.into(),
|
||||
hash: entry.hash.into(),
|
||||
link: format!(
|
||||
"/r/{repo_hash_str}/{0}",
|
||||
|
|
@ -138,74 +151,98 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
return conn.with_status(404);
|
||||
};
|
||||
|
||||
let Some(repo_file) = root.find(conn.path().split('/')) 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
|
||||
};
|
||||
|
||||
let fetch_file = |(repo_hash, file_hash)| {
|
||||
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;
|
||||
}
|
||||
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 mut buf = String::new();
|
||||
if let Err(e) = file.read_to_string(&mut buf) {
|
||||
error!("Error reading file `{file_path:?}`: {e:?}");
|
||||
return None;
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
Some(buf)
|
||||
};
|
||||
let Some(file_content) = file_cache.fetch(
|
||||
(repo_hash_str.to_string(), repo_file.hash.clone()),
|
||||
fetch_file,
|
||||
) 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 file_lang = conn
|
||||
.path()
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.unwrap_or(giallo::PLAIN_GRAMMAR_NAME);
|
||||
html = renderer.render(&file_lang, &file_content, true);
|
||||
} else {
|
||||
// TODO something?
|
||||
}
|
||||
|
||||
let hl_options = giallo::HighlightOptions::new(
|
||||
file_lang,
|
||||
giallo::ThemeVariant::Single("catppuccin-frappe"),
|
||||
);
|
||||
let highlighted = hl_registry
|
||||
.highlight(&file_content, &hl_options)
|
||||
.unwrap_or_else(|_e| {
|
||||
// If extension is unknown
|
||||
let hl_options = giallo::HighlightOptions::new(
|
||||
giallo::PLAIN_GRAMMAR_NAME,
|
||||
giallo::ThemeVariant::Single("catppuccin-frappe"),
|
||||
);
|
||||
hl_registry.highlight(&file_content, &hl_options).unwrap()
|
||||
});
|
||||
let html = giallo::HtmlRenderer::default().render(
|
||||
&highlighted,
|
||||
&giallo::RenderOptions {
|
||||
show_line_numbers: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let template = crate::templates::Repo {
|
||||
content: html.clone(),
|
||||
root,
|
||||
path: conn.path().split('/').map(|s| s.to_string()).collect(),
|
||||
title: repo_metadata.title,
|
||||
};
|
||||
conn.ok(template.render().unwrap())
|
||||
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 {
|
||||
|
|
@ -213,3 +250,26 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
|||
}),
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue