From fbf3d7640a4f9fcab9bbcf60b27b9f201b8a1c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= Date: Mon, 27 Jul 2026 21:35:40 +0200 Subject: [PATCH] Title --- FORMATS.md | 24 ++++++++++++++++++ src/api_client.rs | 5 ++-- src/cache.rs | 11 ++++++++- src/repo.rs | 19 ++++++++++++++- src/server.rs | 59 ++++++++++++++++++++++++--------------------- src/templates.rs | 10 +++++++- templates/home.html | 12 ++++++--- templates/repo.html | 8 +++--- 8 files changed, 110 insertions(+), 38 deletions(-) create mode 100644 FORMATS.md diff --git a/FORMATS.md b/FORMATS.md new file mode 100644 index 0000000..db8dc0d --- /dev/null +++ b/FORMATS.md @@ -0,0 +1,24 @@ +# Internal storage formats + +This file describes the internal storage formats used for persistent data. + +## Repository metadata + +`meta.bin` + +File structure: + +* Version (1 byte): `0x00` +* Time (8 bytes, big endian) +* Title length (4 bytes, big endian) (may be zero) +* Title +* Repository URL length (4 bytes, big endian) +* Repository URL +* Commit hash length (4 bytes, big endian) +* Commit hash +* Content + +Content is a concatenation of entries. Entry structure: +* File path length (4 bytes, big endian) +* File path +* Hash (44 bytes) diff --git a/src/api_client.rs b/src/api_client.rs index b19039e..62d74e5 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -211,6 +211,7 @@ pub async fn fetch_repo_tree_index_at_commit( repo_index, RepoMetadata { date: std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(), + title: String::new(), repo_url: repo_url.to_string(), commit_hash: commit_hash.to_string(), content: Vec::new(), @@ -235,8 +236,8 @@ pub async fn fetch_repo_files( let repo_key: [u8; 32] = rand::rng().random(); hasher.update(repo_key); let repo_id: [u8; 32] = hasher.finalize_reset().into(); - let mut repo_id_str = [0; 24]; - base64_turbo::URL_SAFE + let mut repo_id_str = [0; 22]; + base64_turbo::URL_SAFE_NO_PAD .encode_into(&repo_id[0..16], &mut repo_id_str) .expect("unreachable"); let repo_id_str = str::from_utf8(&repo_id_str).expect("unreachable"); diff --git a/src/cache.rs b/src/cache.rs index 9e1d10f..31eb036 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -10,13 +10,22 @@ pub struct CacheEntry { time: AtomicU64, } -#[derive(Default)] pub struct Cache { ttl: u64, size: usize, content: DashMap>, } +impl Default for Cache { + fn default() -> Self { + Self { + ttl: 0, + size: 0, + content: DashMap::new(), + } + } +} + impl Cache { pub fn sweep(&self) { let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); diff --git a/src/repo.rs b/src/repo.rs index edbaf88..0851eac 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -11,9 +11,10 @@ use std::{ const REPO_METADATA_FILE_NAME: &str = "meta.bin"; const VERSION: [u8; 1] = [0]; -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct RepoMetadata { pub date: u64, + pub title: String, pub repo_url: String, pub commit_hash: String, pub content: Vec, @@ -75,6 +76,10 @@ impl RepoMetadata { let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); file.write_all(&now.to_be_bytes())?; + file.write_all(&(self.title.len() as u32).to_be_bytes())?; + + file.write_all(self.title.as_bytes())?; + file.write_all(&(self.repo_url.len() as u32).to_be_bytes())?; file.write_all(self.repo_url.as_bytes())?; @@ -104,6 +109,17 @@ impl RepoMetadata { file.read_exact(&mut date)?; let date = u64::from_be_bytes(date); + let mut title_len = [0u8; 4]; + file.read_exact(&mut title_len)?; + let title_len = u32::from_be_bytes(title_len) as usize; + if title_len > MAX_URL_SIZE { + return Err(ReadRepoMetadataError::InvalidFormat); + } + + let mut title = vec![0; title_len]; + file.read_exact(&mut title)?; + let title = String::from_utf8(title)?; + let mut repo_url_len = [0u8; 4]; file.read_exact(&mut repo_url_len)?; let repo_url_len = u32::from_be_bytes(repo_url_len) as usize; @@ -131,6 +147,7 @@ impl RepoMetadata { Ok(Self { date, + title, repo_url, commit_hash, content, diff --git a/src/server.rs b/src/server.rs index ad838f8..f5b7f26 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,4 @@ -use crate::{cache, config::Config, repo::ReadRepoMetadataError, templates}; +use crate::{cache, config::Config, repo, templates}; use askama::Template; use log::error; @@ -16,7 +16,7 @@ pub fn make_router(config: &'static Config) -> impl Handler { let metadata_cache: &'static _ = Box::leak(Box::new(cache::Cache::< String, - templates::Directory, + (repo::RepoMetadata, templates::Directory), >::default())); let file_cache: &'static _ = Box::leak(Box::new(cache::Cache::<(String, String), String>::default())); @@ -26,7 +26,6 @@ pub fn make_router(config: &'static Config) -> impl Handler { ( trillium_caching_headers::CachingHeaders::new(), - //trillium_static_compiled::static_compiled!("./static").with_index_file("index.html"), Router::new() .get("/", |conn: Conn| async move { conn.ok(crate::templates::Home {}.render().unwrap()) @@ -34,10 +33,14 @@ pub fn make_router(config: &'static Config) -> impl Handler { .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); } @@ -61,6 +64,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( config, &mut client, @@ -71,7 +75,6 @@ pub fn make_router(config: &'static Config) -> impl Handler { .await .expect("todo handle error"); } - //let planet = conn.param("planet").unwrap(); conn.ok(crate::templates::Home {}.render().unwrap()) }) .get("/r/:hash/*", move |conn: Conn| { @@ -81,21 +84,14 @@ pub fn make_router(config: &'static Config) -> impl Handler { }; 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(key); - let repo_metadata = + let mut repo_metadata = match crate::repo::RepoMetadata::read_from_file(config, &repo_dir) { Ok(v) => v, Err(e) => { - if let ReadRepoMetadataError::CannotOpenFile(e) = &e { + if let repo::ReadRepoMetadataError::CannotOpenFile(e) = &e { if e.kind() == ErrorKind::NotFound { return None; } @@ -119,6 +115,10 @@ pub fn make_router(config: &'static Config) -> impl Handler { name: name.into(), path: dbg!(entry.file_path).into(), hash: entry.hash.into(), + link: format!( + "/r/{repo_hash_str}/{0}", + entry.file_path + ), }), path, ); @@ -128,9 +128,11 @@ pub fn make_router(config: &'static Config) -> impl Handler { } } } - Some(root) + // Save memory + repo_metadata.content.clear(); + Some((repo_metadata, root)) }; - let Some(root) = + let Some((repo_metadata, root)) = metadata_cache.fetch(repo_hash_str.to_string(), fetch_metadata) else { return conn.with_status(404); @@ -180,14 +182,16 @@ pub fn make_router(config: &'static Config) -> impl Handler { 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 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 { @@ -195,12 +199,13 @@ pub fn make_router(config: &'static Config) -> impl Handler { ..Default::default() }, ); - conn.ok(crate::templates::Repo { + let template = crate::templates::Repo { content: html.clone(), root, - } - .render() - .unwrap()) + path: conn.path().split('/').map(|s| s.to_string()).collect(), + title: repo_metadata.title, + }; + conn.ok(template.render().unwrap()) } }) .get("/e/:secret", |conn: Conn| async move { diff --git a/src/templates.rs b/src/templates.rs index d2ca631..4233a99 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -7,11 +7,18 @@ use trillium_askama::Template; #[template(path = "home.html")] pub struct Home {} +/// View of a page in a repository #[derive(Template)] #[template(path = "repo.html")] pub struct Repo { + /// HTML-rendered content of the file pub content: String, + /// Repository tree from the root pub root: Directory, + /// Current file path + pub path: Vec, + /// Repository title + pub title: String, } #[derive(Clone, Template)] @@ -30,7 +37,7 @@ pub struct Repo { {% when Entry::File(file) %} -
  • {{ file.name|escape }}
  • +
  • {{ file.name|escape }}
  • {% endmatch %} "#, ext = "html", @@ -46,6 +53,7 @@ pub struct File { pub name: String, pub path: String, pub hash: String, + pub link: String, } #[derive(Clone, Default)] diff --git a/templates/home.html b/templates/home.html index 4fbfa8d..4b923cc 100644 --- a/templates/home.html +++ b/templates/home.html @@ -8,12 +8,18 @@

    Import any repository from any Forgejo instance using its main URL. The full commit hash is also needed, so the right version is fetched.

    Token is optional. It is needed if the repository is private. In that case, create an API token with read access to the repository. It is revocable at any moment and only grants the selected permissions.

    + + + Title that will be displayed for this anonymous repository (can be modified later).
    -
    + + URL of the original repository.

    -
    + + Full commit hash at which the repository is fetched.

    -
    + + Access token, in case the repository is private.

    diff --git a/templates/repo.html b/templates/repo.html index fdc5fd3..743889a 100644 --- a/templates/repo.html +++ b/templates/repo.html @@ -25,7 +25,7 @@ html, body {
    -

    Titre

    +

    Blindforge

      @@ -35,8 +35,10 @@ html, body {
    -

    Fichier

    - Root / Dossier / Fichier +

    {{ title }}

    + + {% for element in path %}/{{ element }}{% endfor %} +
    {{ content|safe }}