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

View file

@ -14,10 +14,10 @@ pub struct Home {}
#[template(path = "repo.html")] #[template(path = "repo.html")]
pub struct Repo { pub struct Repo {
pub content: String, pub content: String,
pub entries: BTreeSet<Entry>, pub root: Directory,
} }
#[derive(Template)] #[derive(Clone, Template)]
#[template( #[template(
source = r#" source = r#"
{% match self %} {% match self %}
@ -44,16 +44,18 @@ pub enum Entry {
File(File), File(File),
} }
#[derive(Clone)]
pub struct File { pub struct File {
name: String, pub name: String,
path: String, pub path: String,
pub hash: String,
} }
#[derive(Default)] #[derive(Clone, Default)]
pub struct Directory { pub struct Directory {
name: String, pub name: String,
path: String, pub path: String,
entries: BTreeMap<String, Entry>, pub entries: BTreeMap<String, Entry>,
} }
impl Entry { impl Entry {
@ -78,13 +80,16 @@ impl PartialOrd<Entry> for Entry {
} }
impl Directory { impl Directory {
/// Insert
pub fn insert<'a>(&mut self, entry: Entry, mut path: Peekable<impl Iterator<Item = &'a str>>) { pub fn insert<'a>(&mut self, entry: Entry, mut path: Peekable<impl Iterator<Item = &'a str>>) {
let Some(next_path_level) = path.next() else { let Some(next_path_level) = path.next() else {
warn!("Cannot insert entry: no more path"); warn!("Cannot insert entry: no more path");
return; return;
}; };
if path.peek().is_some() { if path.peek().is_some() {
// Directory
if let Some(parent) = self.entries.get_mut(next_path_level) { if let Some(parent) = self.entries.get_mut(next_path_level) {
// Directory exists
match parent { match parent {
Entry::Directory(dir) => dir.insert(entry, path), Entry::Directory(dir) => dir.insert(entry, path),
Entry::File(_file) => { Entry::File(_file) => {
@ -92,10 +97,29 @@ impl Directory {
} }
} }
} else { } 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 { } else {
// File
self.entries.insert(entry.name().to_string(), entry); 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> </header>
<div id="tree"> <div id="tree">
<ul class="tree"> <ul class="tree">
{% for entry in entries %} {% for (_, entry) in root.entries %}
{{ entry.render()? }} {{ entry.render()?|safe }}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>