Serve files

This commit is contained in:
Pascal Engélibert 2026-07-25 19:57:43 +02:00
commit ca52201880
4 changed files with 125 additions and 41 deletions

View file

@ -188,6 +188,7 @@ impl<'a> Iterator for RepoMetadataIter<'a> {
return Some(Err(ReadRepoMetadataError::InvalidFormat));
};
self.offset += 48 + file_path_len;
Some(Ok(RepoMetadataEntry { file_path, hash }))
}
}

View file

@ -1,9 +1,18 @@
use crate::{cache, config::Config, repo::ReadRepoMetadataError, templates};
use crate::{
cache,
config::Config,
repo::{ReadRepoMetadataError, RepoMetadataEntry},
templates,
};
use askama::Template;
use async_lock::Mutex;
use log::error;
use std::{collections::{BTreeMap, BTreeSet, HashMap}, io::ErrorKind, path::PathBuf};
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
io::{ErrorKind, Read},
path::PathBuf,
};
use trillium::{Conn, Handler};
use trillium_router::{Router, RouterConnExt};
@ -18,7 +27,11 @@ pub fn make_router(config: &'static Config) -> impl Handler {
let metadata_cache: &'static _ = Box::leak(Box::new(Mutex::new(cache::Cache::<
String,
HashMap<String, String>,
templates::Directory,
>::default())));
let file_cache: &'static _ = Box::leak(Box::new(Mutex::new(cache::Cache::<
(String, String),
String,
>::default())));
let client: &'static _ = Box::leak(Box::new(async_lock::Mutex::new(
crate::api_client::make_client(),
@ -51,7 +64,8 @@ pub fn make_router(config: &'static Config) -> impl Handler {
};
let mut client = client.lock().await;
let (repo_index, mut repo_metadata) = crate::api_client::fetch_repo_tree_index_at_commit(
let (repo_index, mut repo_metadata) =
crate::api_client::fetch_repo_tree_index_at_commit(
&mut client,
&repo_url,
&commit_hash,
@ -59,7 +73,13 @@ pub fn make_router(config: &'static Config) -> impl Handler {
)
.await
.expect("todo handle error");
crate::api_client::fetch_repo_files(config, &mut client, &repo_index, &mut repo_metadata, None)
crate::api_client::fetch_repo_files(
config,
&mut client,
&repo_index,
&mut repo_metadata,
None,
)
.await
.expect("todo handle error");
}
@ -72,16 +92,17 @@ pub fn make_router(config: &'static Config) -> impl Handler {
return conn.with_status(401);
};
let cache_fetch = |key| {
let mut repo_hash = [0; 32];
if base64_turbo::URL_SAFE.decode_into(key, &mut repo_hash)
!= Ok(32)
{
return None;
}
let fetch_metadata = |key| {
//let mut repo_hash = [0; 24];
//if dbg!(base64_turbo::URL_SAFE.decode_into(key, &mut repo_hash))
// != Ok(16)
//{
// return None;
//}
//let repo_hash = &repo_hash[0..16];
let repo_dir = PathBuf::from(&config.data_dir)
.join(crate::SUBDIR_REPOS)
.join(repo_hash_str);
.join(key);
let repo_metadata =
match crate::repo::RepoMetadata::read_from_file(config, &repo_dir) {
Ok(v) => v,
@ -95,7 +116,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
return None;
}
};
let mut entries = templates::Directory::default();
let mut root = templates::Directory::default();
for entry in repo_metadata.iter_files() {
match entry {
Ok(entry) => {
@ -104,33 +125,71 @@ pub fn make_router(config: &'static Config) -> impl Handler {
continue;
};
let path = entry.file_path.split('/').peekable();
let template_entry = match entry {
}
entries.insert(, path);
root.insert(
templates::Entry::File(templates::File {
name: name.into(),
path: dbg!(entry.file_path).into(),
hash: entry.hash.into(),
}),
path,
);
}
Err(e) => {
error!("Reading repo metadata file index: {e:?}")
}
}
}
Some(files)
Some(root)
};
// TODO replace mutex with better thing (less contention or async mutex)
let Some(metadata) = metadata_cache
let Some(root) = metadata_cache
.lock()
.await
.fetch(repo_hash_str.to_string(), cache_fetch) else {
.fetch(repo_hash_str.to_string(), fetch_metadata)
else {
return conn.with_status(404);
};
let Some(file_hash) = root.find(conn.path().split('/')) else {
return conn.with_status(404);
};
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;
}
};
let mut buf = String::new();
if let Err(e) = file.read_to_string(&mut buf) {
error!("Error reading file `{file_path:?}`: {e:?}");
return None;
}
Some(buf)
};
let Some(file_content) = file_cache.lock().await.fetch(
(repo_hash_str.to_string(), file_hash.to_string()),
fetch_file,
) else {
return conn.with_status(500);
};
let hl_options = giallo::HighlightOptions::new(
"py",
giallo::ThemeVariant::Single("catppuccin-frappe"),
);
let highlighted = hl_registry
.highlight("def foo():\n\tpass", &hl_options)
.unwrap();
let highlighted = hl_registry.highlight(&file_content, &hl_options).unwrap();
let html = giallo::HtmlRenderer::default().render(
&highlighted,
&giallo::RenderOptions {
@ -140,7 +199,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
);
conn.ok(crate::templates::Repo {
content: html.clone(),
entries: Vec::new(),
root,
}
.render()
.unwrap())

View file

@ -14,10 +14,10 @@ pub struct Home {}
#[template(path = "repo.html")]
pub struct Repo {
pub content: String,
pub entries: BTreeSet<Entry>,
pub root: Directory,
}
#[derive(Template)]
#[derive(Clone, Template)]
#[template(
source = r#"
{% match self %}
@ -44,16 +44,18 @@ pub enum Entry {
File(File),
}
#[derive(Clone)]
pub struct File {
name: String,
path: String,
pub name: String,
pub path: String,
pub hash: String,
}
#[derive(Default)]
#[derive(Clone, Default)]
pub struct Directory {
name: String,
path: String,
entries: BTreeMap<String, Entry>,
pub name: String,
pub path: String,
pub entries: BTreeMap<String, Entry>,
}
impl Entry {
@ -78,13 +80,16 @@ impl PartialOrd<Entry> for Entry {
}
impl Directory {
/// Insert
pub fn insert<'a>(&mut self, entry: Entry, mut path: Peekable<impl Iterator<Item = &'a str>>) {
let Some(next_path_level) = path.next() else {
warn!("Cannot insert entry: no more path");
return;
};
if path.peek().is_some() {
// Directory
if let Some(parent) = self.entries.get_mut(next_path_level) {
// Directory exists
match parent {
Entry::Directory(dir) => dir.insert(entry, path),
Entry::File(_file) => {
@ -92,10 +97,29 @@ impl Directory {
}
}
} else {
warn!("Cannot insert entry: not found");
//warn!("Cannot insert entry: not found");
// Need to create directory
let mut new_dir_path = self.path.clone();
new_dir_path.push_str(next_path_level);
let mut parent = Directory {
name: next_path_level.into(),
path: new_dir_path,
entries: BTreeMap::new(),
};
parent.insert(entry, path);
self.entries
.insert(next_path_level.into(), Entry::Directory(parent));
}
} else {
// File
self.entries.insert(entry.name().to_string(), entry);
}
}
pub fn find<'a>(&self, mut path: impl Iterator<Item = &'a str>) -> Option<&str> {
match self.entries.get(path.next()?)? {
Entry::Directory(directory) => directory.find(path),
Entry::File(file) => Some(&file.hash),
}
}
}

View file

@ -29,8 +29,8 @@ html, body {
</header>
<div id="tree">
<ul class="tree">
{% for entry in entries %}
{{ entry.render()? }}
{% for (_, entry) in root.entries %}
{{ entry.render()?|safe }}
{% endfor %}
</ul>
</div>