Serve empty directories, page cache

This commit is contained in:
Pascal Engélibert 2026-07-29 17:55:23 +02:00
commit ed769102de
12 changed files with 339 additions and 112 deletions

7
Cargo.lock generated
View file

@ -309,6 +309,7 @@ dependencies = [
"dashmap", "dashmap",
"form_urlencoded", "form_urlencoded",
"giallo", "giallo",
"linemd",
"log", "log",
"rand", "rand",
"serde", "serde",
@ -1048,6 +1049,12 @@ version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linemd"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e8d932ea5e37464982f193ac2c9a7263d9ffaf5a9622688bbf981cc5b0fc92"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.12.1" version = "0.12.1"

View file

@ -19,6 +19,8 @@ dashmap = "6.2.1"
form_urlencoded = "1.2.2" form_urlencoded = "1.2.2"
# Code highlight # Code highlight
giallo = { version = "0.5.0", features = ["dump"] } giallo = { version = "0.5.0", features = ["dump"] }
# Markdown rendering
linemd = "0.5.0"
log = "0.4.33" log = "0.4.33"
rand = "0.10.2" rand = "0.10.2"
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }

View file

@ -9,7 +9,8 @@ This file describes the internal storage formats used for persistent data.
File structure: File structure:
* Version (1 byte): `0x00` * Version (1 byte): `0x00`
* Time (8 bytes, big endian) * Creation time (at which the repo was mirrored) (8 bytes, big endian)
* Update time (8 bytes, big endian)
* Title length (4 bytes, big endian) (may be zero) * Title length (4 bytes, big endian) (may be zero)
* Title * Title
* Repository URL length (4 bytes, big endian) * Repository URL length (4 bytes, big endian)

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

View file

@ -207,10 +207,13 @@ pub async fn fetch_repo_tree_index_at_commit(
} }
} }
let time = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
Ok(( Ok((
repo_index, repo_index,
RepoMetadata { RepoMetadata {
date: std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(), time_created: time,
time_updated: time,
title: String::new(), title: String::new(),
repo_url: repo_url.to_string(), repo_url: repo_url.to_string(),
commit_hash: commit_hash.to_string(), commit_hash: commit_hash.to_string(),
@ -230,7 +233,7 @@ pub async fn fetch_repo_files(
repo_index: &RepoIndex, repo_index: &RepoIndex,
repo_metadata: &mut RepoMetadata, repo_metadata: &mut RepoMetadata,
token: Option<&str>, token: Option<&str>,
) -> Result<(), FetchRepoError> { ) -> Result<String, FetchRepoError> {
let mut hasher = sha2::Sha256::default(); let mut hasher = sha2::Sha256::default();
let repo_key: [u8; 32] = rand::rng().random(); let repo_key: [u8; 32] = rand::rng().random();
@ -288,7 +291,7 @@ pub async fn fetch_repo_files(
} }
} }
repo_metadata.write_to_file(config, &repo_dir)?; repo_metadata.write_to_file(&repo_dir)?;
Ok(()) Ok(repo_id_str.to_string())
} }

View file

@ -7,26 +7,38 @@ use std::{
pub struct CacheEntry<T> { pub struct CacheEntry<T> {
inner: T, inner: T,
/// Latest access
time: AtomicU64, time: AtomicU64,
} }
/// Index values V by key K
pub struct Cache<K: Eq + Hash, V> { pub struct Cache<K: Eq + Hash, V> {
/// Time to live for entries
ttl: u64, ttl: u64,
/// Maximum number of entries
size: usize, size: usize,
content: DashMap<K, CacheEntry<V>>, content: DashMap<K, CacheEntry<V>>,
} }
impl<K: Eq + Hash, V> Default for Cache<K, V> { //impl<K: Eq + Hash, V> Default for Cache<K, V> {
fn default() -> Self { // fn default() -> Self {
// Self {
// ttl: 0,
// size: 0,
// content: DashMap::new(),
// }
// }
//}
impl<K: Clone + Eq + Hash, V: Clone> Cache<K, V> {
pub fn new(size: usize) -> Self {
Self { Self {
ttl: 0, ttl: 0,
size: 0, size,
content: DashMap::new(), content: DashMap::new(),
} }
} }
}
impl<K: Clone + Eq + Hash, V: Clone> Cache<K, V> {
pub fn sweep(&self) { pub fn sweep(&self) {
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
self.content self.content
@ -50,13 +62,14 @@ impl<K: Clone + Eq + Hash, V: Clone> Cache<K, V> {
} }
} }
/// Get an existing entry at the given key, or insert a new one
pub fn fetch(&self, key: impl Borrow<K>, f: impl Fn(K) -> Option<V>) -> Option<V> { pub fn fetch(&self, key: impl Borrow<K>, f: impl Fn(K) -> Option<V>) -> Option<V> {
if let Some(entry) = self.content.get(key.borrow()) { if let Some(entry) = self.content.get(key.borrow()) {
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
entry.time.store(now, Relaxed); entry.time.store(now, Relaxed);
Some(entry.inner.clone()) Some(entry.inner.clone())
} else if let Some(inner) = (f)(key.borrow().clone()) { } else if let Some(inner) = (f)(key.borrow().clone()) {
if self.content.len() >= self.size { while self.content.len() >= self.size {
self.sweep_oldest(); self.sweep_oldest();
} }
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
@ -70,8 +83,67 @@ impl<K: Clone + Eq + Hash, V: Clone> Cache<K, V> {
None None
} }
} }
}
/*pub struct CacheRef { /// Same as fetch, but if the entry exists, condition is called on it.
cache: /// If condition returns true, then the entry is kept and returned.
}*/ /// Else, the entry is removed and potentially replaced by a fresh one.
pub fn fetch_or_renew(
&self,
key: impl Borrow<K>,
f: impl Fn(K) -> Option<V>,
condition: impl Fn(&V) -> bool,
) -> Option<V> {
if let Some(entry) = self.content.get(key.borrow()) {
if (condition)(&entry.value().inner) {
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
entry.time.store(now, Relaxed);
return Some(entry.inner.clone());
}
}
if let Some(inner) = (f)(key.borrow().clone()) {
while self.content.len() >= self.size {
self.sweep_oldest();
}
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let entry = CacheEntry {
time: AtomicU64::new(now),
inner: inner.clone(),
};
self.content.insert(key.borrow().clone(), entry);
Some(inner)
} else {
None
}
}
pub fn fetch_cond_only(
&self,
key: impl Borrow<K>,
condition: impl Fn(&V) -> bool,
) -> Option<V> {
if let Some(entry) = self.content.get(key.borrow()) {
if (condition)(&entry.value().inner) {
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
entry.time.store(now, Relaxed);
return Some(entry.inner.clone());
}
}
None
}
pub fn update(&self, key: impl Borrow<K>, value: Option<V>) {
if let Some(value) = value {
while self.content.len() >= self.size {
self.sweep_oldest();
}
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let entry = CacheEntry {
time: AtomicU64::new(now),
inner: value,
};
self.content.insert(key.borrow().clone(), entry);
} else {
self.content.remove(key.borrow());
}
}
}

View file

@ -17,6 +17,12 @@ pub struct Config {
pub max_file_path_len: usize, pub max_file_path_len: usize,
/// API URL maximum length /// API URL maximum length
pub api_client_max_url_len: usize, pub api_client_max_url_len: usize,
/// Files that are served when showing a directory (separated by colon `:`, in order of decreasing priority)
pub default_files: String,
/// Maximum number of pages to keep in cache
pub page_cache_size: usize,
/// Maximum number of repository trees to keep in cache
pub repo_cache_size: usize,
} }
impl Default for Config { impl Default for Config {
@ -31,6 +37,11 @@ impl Default for Config {
max_file_size: 8 * 1024 * 1024, max_file_size: 8 * 1024 * 1024,
max_file_path_len: 8192, max_file_path_len: 8192,
api_client_max_url_len: 256, api_client_max_url_len: 256,
default_files: String::from(
"README.md:README.MD:readme.md:README.txt:README.TXT:readme.txt:README:readme",
),
page_cache_size: 200,
repo_cache_size: 20,
} }
} }
} }
@ -97,6 +108,23 @@ impl Config {
.try_into() .try_into()
.expect("Config: invalid api_client_max_url_len"); .expect("Config: invalid api_client_max_url_len");
} }
if let Some(v) = toml.get("default_files") {
config.default_files = v.as_string().expect("Config: invalid default_files").into()
}
if let Some(v) = toml.get("page_cache_size") {
config.page_cache_size = v
.as_integer()
.expect("Config: invalid page_cache_size")
.try_into()
.expect("Config: invalid page_cache_size");
}
if let Some(v) = toml.get("repo_cache_size") {
config.repo_cache_size = v
.as_integer()
.expect("Config: invalid repo_cache_size")
.try_into()
.expect("Config: invalid repo_cache_size");
}
config config
} }
} }

View file

@ -1,11 +1,10 @@
#![feature(btree_set_entry)] use std::path::PathBuf;
use std::{path::PathBuf, sync::Arc};
mod api_client; mod api_client;
mod cache; mod cache;
mod config; mod config;
mod queue; mod queue;
mod render;
mod repo; mod repo;
mod server; mod server;
mod templates; mod templates;

44
src/render.rs Normal file
View file

@ -0,0 +1,44 @@
use linemd::Parser;
pub struct Renderer {
hl_registry: giallo::Registry,
}
impl Renderer {
pub fn new() -> Self {
let mut hl_registry = giallo::Registry::builtin().unwrap();
hl_registry.link_grammars();
Self { hl_registry }
}
pub fn render(&self, filetype: &str, content: &[u8], pretty: bool) -> String {
let Ok(content) = str::from_utf8(content) else {
return String::from("Cannot render file as it is not valid UTF-8.");
};
if pretty && filetype == "md" {
linemd::render_as_html(content.parse_md())
} else {
let hl_options = giallo::HighlightOptions::new(
filetype,
giallo::ThemeVariant::Single("catppuccin-frappe"),
);
let highlighted = self
.hl_registry
.highlight(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"),
);
self.hl_registry.highlight(content, &hl_options).unwrap()
});
giallo::HtmlRenderer::default().render(
&highlighted,
&giallo::RenderOptions {
show_line_numbers: true,
..Default::default()
},
)
}
}
}

View file

@ -1,7 +1,4 @@
use crate::{ use crate::api_client::{MAX_PATH_SIZE, MAX_URL_SIZE};
api_client::{MAX_PATH_SIZE, MAX_URL_SIZE},
config::Config,
};
use std::{ use std::{
io::{ErrorKind, Read, Write}, io::{ErrorKind, Read, Write},
@ -13,7 +10,8 @@ const VERSION: [u8; 1] = [0];
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RepoMetadata { pub struct RepoMetadata {
pub date: u64, pub time_created: u64,
pub time_updated: u64,
pub title: String, pub title: String,
pub repo_url: String, pub repo_url: String,
pub commit_hash: String, pub commit_hash: String,
@ -54,27 +52,24 @@ impl From<std::io::Error> for ReadRepoMetadataError {
} }
impl From<std::string::FromUtf8Error> for ReadRepoMetadataError { impl From<std::string::FromUtf8Error> for ReadRepoMetadataError {
fn from(value: std::string::FromUtf8Error) -> Self { fn from(_value: std::string::FromUtf8Error) -> Self {
Self::InvalidFormat Self::InvalidFormat
} }
} }
impl RepoMetadata { impl RepoMetadata {
pub fn write_to_file( pub fn write_to_file(&self, repo_dir: &Path) -> Result<(), WriteRepoMetadataError> {
&self,
config: &Config,
repo_dir: &Path,
) -> Result<(), WriteRepoMetadataError> {
let mut file = std::fs::OpenOptions::new() let mut file = std::fs::OpenOptions::new()
.write(true) .write(true)
.create_new(true) .create_new(true)
.open(repo_dir.join("index")) .open(repo_dir.join(REPO_METADATA_FILE_NAME))
.map_err(WriteRepoMetadataError::CannotOpenFile)?; .map_err(WriteRepoMetadataError::CannotOpenFile)?;
file.write_all(&VERSION)?; file.write_all(&VERSION)?;
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
file.write_all(&now.to_be_bytes())?; file.write_all(&now.to_be_bytes())?;
file.write_all(&now.to_be_bytes())?;
file.write_all(&(self.title.len() as u32).to_be_bytes())?; file.write_all(&(self.title.len() as u32).to_be_bytes())?;
@ -93,10 +88,10 @@ impl RepoMetadata {
Ok(()) Ok(())
} }
pub fn read_from_file(config: &Config, repo_dir: &Path) -> Result<Self, ReadRepoMetadataError> { pub fn read_from_file(repo_dir: &Path) -> Result<Self, ReadRepoMetadataError> {
let mut file = std::fs::OpenOptions::new() let mut file = std::fs::OpenOptions::new()
.read(true) .read(true)
.open(repo_dir.join("index")) .open(repo_dir.join(REPO_METADATA_FILE_NAME))
.map_err(ReadRepoMetadataError::CannotOpenFile)?; .map_err(ReadRepoMetadataError::CannotOpenFile)?;
let mut version = [0u8; 1]; let mut version = [0u8; 1];
@ -105,9 +100,13 @@ impl RepoMetadata {
return Err(ReadRepoMetadataError::UnsupportedVersion); return Err(ReadRepoMetadataError::UnsupportedVersion);
} }
let mut date = [0u8; 8]; let mut time_created = [0u8; 8];
file.read_exact(&mut date)?; file.read_exact(&mut time_created)?;
let date = u64::from_be_bytes(date); let time_created = u64::from_be_bytes(time_created);
let mut time_updated = [0u8; 8];
file.read_exact(&mut time_updated)?;
let time_updated = u64::from_be_bytes(time_updated);
let mut title_len = [0u8; 4]; let mut title_len = [0u8; 4];
file.read_exact(&mut title_len)?; file.read_exact(&mut title_len)?;
@ -146,7 +145,8 @@ impl RepoMetadata {
file.read_to_end(&mut content)?; file.read_to_end(&mut content)?;
Ok(Self { Ok(Self {
date, time_created,
time_updated,
title, title,
repo_url, repo_url,
commit_hash, commit_hash,

View file

@ -1,4 +1,4 @@
use crate::{cache, config::Config, repo, templates}; use crate::{cache, config::Config, render, repo, templates};
use askama::Template; use askama::Template;
use log::error; use log::error;
@ -9,17 +9,23 @@ use std::{
use trillium::{Conn, Handler}; use trillium::{Conn, Handler};
use trillium_router::{Router, RouterConnExt}; use trillium_router::{Router, RouterConnExt};
#[derive(Clone)]
struct PageCacheEntry {
time: u64,
content: String,
}
pub fn make_router(config: &'static Config) -> impl Handler { pub fn make_router(config: &'static Config) -> impl Handler {
let mut hl_registry = giallo::Registry::builtin().unwrap(); let renderer: &'static _ = Box::leak(Box::new(render::Renderer::new()));
hl_registry.link_grammars();
let hl_registry: &'static _ = Box::leak(Box::new(hl_registry));
let metadata_cache: &'static _ = Box::leak(Box::new(cache::Cache::< let metadata_cache: &'static _ = Box::leak(Box::new(cache::Cache::<
String, String,
(repo::RepoMetadata, templates::Directory), (repo::RepoMetadata, templates::Directory),
>::default())); >::new(config.repo_cache_size)));
let file_cache: &'static _ = let page_cache: &'static _ = Box::leak(Box::new(cache::Cache::<
Box::leak(Box::new(cache::Cache::<(String, String), String>::default())); (String, String),
PageCacheEntry,
>::new(config.page_cache_size)));
let client: &'static _ = Box::leak(Box::new(async_lock::Mutex::new( let client: &'static _ = Box::leak(Box::new(async_lock::Mutex::new(
crate::api_client::make_client(), crate::api_client::make_client(),
))); )));
@ -30,6 +36,10 @@ pub fn make_router(config: &'static Config) -> impl Handler {
.get("/", |conn: Conn| async move { .get("/", |conn: Conn| async move {
conn.ok(crate::templates::Home {}.render().unwrap()) 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 { .post("/fetch", move |mut conn: Conn| async move {
if let Ok(request_body) = conn.request_body().with_max_len(8192).read_bytes().await 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 .await
.expect("todo handle error"); .expect("todo handle error");
repo_metadata.title = title; repo_metadata.title = title;
crate::api_client::fetch_repo_files( let repo_id_str = crate::api_client::fetch_repo_files(
config, config,
&mut client, &mut client,
&repo_index, &repo_index,
@ -74,6 +84,9 @@ pub fn make_router(config: &'static Config) -> impl Handler {
) )
.await .await
.expect("todo handle error"); .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()) 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(crate::SUBDIR_REPOS)
.join(key); .join(key);
let mut repo_metadata = 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, Ok(v) => v,
Err(e) => { Err(e) => {
if let repo::ReadRepoMetadataError::CannotOpenFile(e) = &e { if let repo::ReadRepoMetadataError::CannotOpenFile(e) = &e {
@ -113,7 +126,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
root.insert( root.insert(
templates::Entry::File(templates::File { templates::Entry::File(templates::File {
name: name.into(), name: name.into(),
path: dbg!(entry.file_path).into(), path: entry.file_path.into(),
hash: entry.hash.into(), hash: entry.hash.into(),
link: format!( link: format!(
"/r/{repo_hash_str}/{0}", "/r/{repo_hash_str}/{0}",
@ -138,74 +151,98 @@ pub fn make_router(config: &'static Config) -> impl Handler {
return conn.with_status(404); return conn.with_status(404);
}; };
let Some(repo_file) = root.find(conn.path().split('/')) else { let invalidate_page_cache = |entry: &PageCacheEntry| {
return conn.with_status(404); // 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(
let fetch_file = |(repo_hash, file_hash)| { (repo_hash_str.to_string(), conn.path().to_string()),
let file_path = PathBuf::from(&config.data_dir) invalidate_page_cache,
.join(crate::SUBDIR_REPOS) ) {
.join(repo_hash) conn.ok(rendered_page.content)
.join(file_hash); } else {
let Some(served_entry) = root.find(conn.path().split('/')) else {
let mut file = match std::fs::OpenOptions::new().read(true).open(&file_path) page_cache
{ .update((repo_hash_str.to_string(), conn.path().to_string()), None);
Ok(file) => file, return conn.with_status(404);
Err(e) => {
error!("Cannot open file `{file_path:?}`: {e:?}");
return None;
}
}; };
let mut buf = String::new(); let rendered_page = match served_entry {
if let Err(e) = file.read_to_string(&mut buf) { templates::EntryRef::Directory(served_dir) => {
error!("Error reading file `{file_path:?}`: {e:?}"); let mut served_file = None;
return 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 mut file_lang = served_file
}; .name
let Some(file_content) = file_cache.fetch( .rsplit('.')
(repo_hash_str.to_string(), repo_file.hash.clone()), .next()
fetch_file, .unwrap_or(giallo::PLAIN_GRAMMAR_NAME)
) else { .to_string();
return conn.with_status(500); file_lang.make_ascii_lowercase();
};
let file_lang = conn html = renderer.render(&file_lang, &file_content, true);
.path() } else {
.rsplit('.') // TODO something?
.next() }
.unwrap_or(giallo::PLAIN_GRAMMAR_NAME);
let hl_options = giallo::HighlightOptions::new( let template = crate::templates::Repo {
file_lang, content: html.clone(),
giallo::ThemeVariant::Single("catppuccin-frappe"), root,
); path: conn.path().split('/').map(|s| s.to_string()).collect(),
let highlighted = hl_registry title: repo_metadata.title,
.highlight(&file_content, &hl_options) };
.unwrap_or_else(|_e| { template.render().unwrap()
// If extension is unknown }
let hl_options = giallo::HighlightOptions::new( templates::EntryRef::File(served_file) => {
giallo::PLAIN_GRAMMAR_NAME, let Some(file_content) =
giallo::ThemeVariant::Single("catppuccin-frappe"), fetch_file(config, repo_hash_str, &served_file.hash)
); else {
hl_registry.highlight(&file_content, &hl_options).unwrap() return conn.with_status(500);
}); };
let html = giallo::HtmlRenderer::default().render(
&highlighted, let mut file_lang = served_file
&giallo::RenderOptions { .name
show_line_numbers: true, .rsplit('.')
..Default::default() .next()
}, .unwrap_or(giallo::PLAIN_GRAMMAR_NAME)
); .to_string();
let template = crate::templates::Repo { file_lang.make_ascii_lowercase();
content: html.clone(),
root, let html = renderer.render(&file_lang, &file_content, true);
path: conn.path().split('/').map(|s| s.to_string()).collect(), let template = crate::templates::Repo {
title: repo_metadata.title, content: html.clone(),
}; root,
conn.ok(template.render().unwrap()) 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 { .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)
}

View file

@ -48,6 +48,11 @@ pub enum Entry {
File(File), File(File),
} }
pub enum EntryRef<'a> {
Directory(&'a Directory),
File(&'a File),
}
#[derive(Clone)] #[derive(Clone)]
pub struct File { pub struct File {
pub name: String, pub name: String,
@ -121,10 +126,16 @@ impl Directory {
} }
} }
pub fn find<'a>(&self, mut path: impl Iterator<Item = &'a str>) -> Option<&File> { pub fn find<'a, 'b>(&'a self, mut path: impl Iterator<Item = &'b str>) -> Option<EntryRef<'a>> {
match self.entries.get(path.next()?)? { let Some(next_path) = path.next() else {
return Some(EntryRef::Directory(self));
};
if next_path.is_empty() {
return Some(EntryRef::Directory(self));
}
match self.entries.get(next_path)? {
Entry::Directory(directory) => directory.find(path), Entry::Directory(directory) => directory.find(path),
Entry::File(file) => Some(file), Entry::File(file) => Some(EntryRef::File(file)),
} }
} }
} }