From dc13ef503c32478e1734452dd5dfbd336ac1a990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= Date: Thu, 30 Jul 2026 23:52:20 +0200 Subject: [PATCH] Inline files --- example_config.toml | 2 + src/config.rs | 110 +++++++++++++++++++++++++++++ src/render.rs | 11 ++- src/server.rs | 165 ++++++++++++++++++++++++++++++-------------- 4 files changed, 234 insertions(+), 54 deletions(-) diff --git a/example_config.toml b/example_config.toml index 47f7b60..963165f 100644 --- a/example_config.toml +++ b/example_config.toml @@ -29,3 +29,5 @@ data_dir = "/dev/shm/blindforge" # Maximum length of an URL for the client. (characters) #api_client_max_url_len = 256 + +#origin = "https://blindforge.zoai.re" diff --git a/src/config.rs b/src/config.rs index 06cbb69..ba334c6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + pub struct Config { /// Address or host to listen to pub listen_host: String, @@ -23,6 +25,14 @@ pub struct Config { pub page_cache_size: usize, /// Maximum number of repository trees to keep in cache pub repo_cache_size: usize, + /// File extensions that will be served as images (and associated MIME type) + pub image_extensions: HashMap, + /// File extensions that will be served as videos (and associated MIME type) + pub video_extensions: HashMap, + /// File extensions that will be served as audio (and associated MIME type) + pub audio_extensions: HashMap, + /// Authorized origin for serving inline files (CORS) + pub origin: String, } impl Default for Config { @@ -42,6 +52,58 @@ impl Default for Config { ), page_cache_size: 200, repo_cache_size: 20, + image_extensions: [ + ("ani", "image/x-icon"), + ("apng", "image/apng"), + ("bmp", "image/bmp"), + ("cur", "image/x-icon"), + ("gif", "image/gif"), + ("ico", "image/x-icon"), + ("jpeg", "image/jpeg"), + ("jpg", "image/jpeg"), + ("jxl", "image/jxl"), + ("png", "image/png"), + ("qoi", "image/qoi"), + ("tif", "image/tiff"), + ("tiff", "image/tiff"), + ("webp", "image/webp"), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + video_extensions: [ + ("3gp", "video/3gp"), + ("avi", "video/x-msvideo"), + ("mkv", "application/x-matroska"), + ("mp4", "video/mp4"), + ("mpeg", "video/mpeg"), + ("ogv", "video/ogg"), + ("webm", "video/webm"), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + audio_extensions: [ + ("aac", "audio/aac"), + ("flac", "audio/flac"), + ("m4a", "audio/mp4"), + ("m4b", "audio/mp4"), + ("m4p", "audio/mp4"), + ("m4r", "audio/mp4"), + ("m4v", "audio/mp4"), + ("mp3", "audio/mp3"), + ("oga", "audio/ogg"), + ("ogg", "audio/ogg"), + ("opus", "audio/opus"), + ("spx", "audio/x-speex"), + ("wav", "audio/x-wav"), + ("wave", "audio/x-wav"), + ("wma", "audio/x-ms-wma"), + ] + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + origin: String::from("*"), } } } @@ -125,6 +187,54 @@ impl Config { .try_into() .expect("Config: invalid repo_cache_size"); } + if let Some(v) = toml.get("image_extensions") { + config.image_extensions = v + .as_table() + .expect("Config: invalid image_extensions") + .iter() + .map(|(k, v)| { + ( + k.as_str().to_string(), + v.as_string() + .expect("Config: invalid image_extensions") + .to_string(), + ) + }) + .collect() + } + if let Some(v) = toml.get("video_extensions") { + config.image_extensions = v + .as_table() + .expect("Config: invalid video_extensions") + .iter() + .map(|(k, v)| { + ( + k.as_str().to_string(), + v.as_string() + .expect("Config: invalid video_extensions") + .to_string(), + ) + }) + .collect() + } + if let Some(v) = toml.get("audio_extensions") { + config.image_extensions = v + .as_table() + .expect("Config: invalid audio_extensions") + .iter() + .map(|(k, v)| { + ( + k.as_str().to_string(), + v.as_string() + .expect("Config: invalid audio_extensions") + .to_string(), + ) + }) + .collect() + } + if let Some(v) = toml.get("origin") { + config.origin = v.as_string().expect("Config: invalid origin").into() + } config } } diff --git a/src/render.rs b/src/render.rs index 20ba175..f9a130c 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,3 +1,5 @@ +use crate::config::Config; + use linemd::Parser; pub struct Renderer { @@ -10,7 +12,14 @@ impl Renderer { hl_registry.link_grammars(); Self { hl_registry } } - pub fn render(&self, filetype: &str, content: &[u8], pretty: bool) -> String { + pub fn render(&self, config: &Config, filetype: &str, content: &[u8], pretty: bool, repo_hash_str: &str, file_path: &str) -> String { + if config.image_extensions.contains_key(filetype) { + return format!(r#"No description"#) + } else if config.video_extensions.contains_key(filetype) { + return format!(r#""#) + } else if config.audio_extensions.contains_key(filetype) { + return format!(r#""#) + } let Ok(content) = str::from_utf8(content) else { return String::from("Cannot render file as it is not valid UTF-8."); }; diff --git a/src/server.rs b/src/server.rs index efe5c79..1e61bb1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,6 +3,7 @@ use crate::{cache, config::Config, render, repo, templates}; use askama::Template; use log::error; use std::{ + collections::HashMap, io::{ErrorKind, Read}, path::PathBuf, }; @@ -30,6 +31,17 @@ pub fn make_router(config: &'static Config) -> impl Handler { crate::api_client::make_client(), ))); + let mut inline_extensions = HashMap::<&str, &str>::new(); + inline_extensions.extend( + config + .image_extensions + .iter() + .chain(config.video_extensions.iter()) + .chain(config.audio_extensions.iter()) + .map(|(k, v)| (k.as_str(), v.as_str())), + ); + let inline_extensions: &'static _ = Box::leak(Box::new(inline_extensions)); + ( trillium_caching_headers::CachingHeaders::new(), Router::new() @@ -96,57 +108,10 @@ pub fn make_router(config: &'static Config) -> impl Handler { return conn.with_status(401); }; - let fetch_metadata = |key| { - let repo_dir = PathBuf::from(&config.data_dir) - .join(crate::SUBDIR_REPOS) - .join(key); - let mut repo_metadata = - match crate::repo::RepoMetadata::read_from_file(&repo_dir) { - Ok(v) => v, - Err(e) => { - if let repo::ReadRepoMetadataError::CannotOpenFile(e) = &e { - if e.kind() == ErrorKind::NotFound { - return None; - } - } - error!("Reading repo metadata: {e:?}"); - return None; - } - }; - let mut root = templates::Directory::default(); - for entry in repo_metadata.iter_files() { - match entry { - Ok(entry) => { - let Some(name) = entry.file_path.rsplit('/').next() else { - error!("Entry has no name"); - continue; - }; - let path = entry.file_path.split('/').peekable(); - - root.insert( - templates::Entry::File(templates::File { - name: name.into(), - path: entry.file_path.into(), - hash: entry.hash.into(), - link: format!( - "/r/{repo_hash_str}/{0}", - entry.file_path - ), - }), - path, - ); - } - Err(e) => { - error!("Reading repo metadata file index: {e:?}") - } - } - } - // Save memory - repo_metadata.content.clear(); - Some((repo_metadata, root)) - }; - let Some((repo_metadata, root)) = - metadata_cache.fetch(repo_hash_str.to_string(), fetch_metadata) + let Some((repo_metadata, root)) = metadata_cache + .fetch(repo_hash_str.to_string(), |key| { + fetch_metadata(config, &key) + }) else { return conn.with_status(404); }; @@ -196,7 +161,7 @@ pub fn make_router(config: &'static Config) -> impl Handler { .to_string(); file_lang.make_ascii_lowercase(); - html = renderer.render(&file_lang, &file_content, true); + html = renderer.render(config, &file_lang, &file_content, true, repo_hash_str, &served_file.path); } else { // TODO something? } @@ -224,7 +189,7 @@ pub fn make_router(config: &'static Config) -> impl Handler { .to_string(); file_lang.make_ascii_lowercase(); - let html = renderer.render(&file_lang, &file_content, true); + let html = renderer.render(config, &file_lang, &file_content, true, repo_hash_str, &served_file.path); let template = crate::templates::Repo { content: html.clone(), root, @@ -245,6 +210,51 @@ pub fn make_router(config: &'static Config) -> impl Handler { } } }) + .get("/inline/:hash/*", move |conn: Conn| async move { + let Some(repo_hash_str) = conn.param("hash") else { + return conn.with_status(401); + }; + + let Some((_repo_metadata, root)) = metadata_cache + .fetch(repo_hash_str.to_string(), |key| { + fetch_metadata(config, &key) + }) + else { + return conn.with_status(404); + }; + + let Some(served_entry) = root.find(conn.path().split('/')) else { + return conn.with_status(404); + }; + + match served_entry { + templates::EntryRef::Directory(_) => { + conn.with_status(404) + } + templates::EntryRef::File(served_file) => { + let Some(extension) = served_file.name.rsplit('.').next() else { + return conn.with_status(404); + }; + let mut extension = extension.to_string(); + extension.make_ascii_lowercase(); + if let Some(&mime_type) = inline_extensions.get(extension.as_str()) { + let Some(file_content) = + fetch_file(config, repo_hash_str, &served_file.hash) + else { + return conn.with_status(500); + }; + conn.with_response_header( + "Access-Control-Allow-Origin", + config.origin.as_str(), + ) + .with_response_header("Content-Type", mime_type) + .ok(file_content) + } else { + conn.with_status(404) + } + } + } + }) .get("/e/:secret", |conn: Conn| async move { conn.ok(crate::templates::Home {}.render().unwrap()) }), @@ -273,3 +283,52 @@ fn fetch_file(config: &Config, repo_hash: &str, file_hash: &str) -> Option Option<(repo::RepoMetadata, templates::Directory)> { + let repo_dir = PathBuf::from(&config.data_dir) + .join(crate::SUBDIR_REPOS) + .join(repo_hash_str); + let mut repo_metadata = match crate::repo::RepoMetadata::read_from_file(&repo_dir) { + Ok(v) => v, + Err(e) => { + if let repo::ReadRepoMetadataError::CannotOpenFile(e) = &e { + if e.kind() == ErrorKind::NotFound { + return None; + } + } + error!("Reading repo metadata: {e:?}"); + return None; + } + }; + let mut root = templates::Directory::default(); + for entry in repo_metadata.iter_files() { + match entry { + Ok(entry) => { + let Some(name) = entry.file_path.rsplit('/').next() else { + error!("Entry has no name"); + continue; + }; + let path = entry.file_path.split('/').peekable(); + + root.insert( + templates::Entry::File(templates::File { + name: name.into(), + path: entry.file_path.into(), + hash: entry.hash.into(), + link: format!("/r/{repo_hash_str}/{0}", entry.file_path), + }), + path, + ); + } + Err(e) => { + error!("Reading repo metadata file index: {e:?}") + } + } + } + // Save memory + repo_metadata.content.clear(); + Some((repo_metadata, root)) +}