Inline files
This commit is contained in:
parent
ed769102de
commit
dc13ef503c
4 changed files with 234 additions and 54 deletions
|
|
@ -29,3 +29,5 @@ data_dir = "/dev/shm/blindforge"
|
||||||
|
|
||||||
# Maximum length of an URL for the client. (characters)
|
# Maximum length of an URL for the client. (characters)
|
||||||
#api_client_max_url_len = 256
|
#api_client_max_url_len = 256
|
||||||
|
|
||||||
|
#origin = "https://blindforge.zoai.re"
|
||||||
|
|
|
||||||
110
src/config.rs
110
src/config.rs
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Address or host to listen to
|
/// Address or host to listen to
|
||||||
pub listen_host: String,
|
pub listen_host: String,
|
||||||
|
|
@ -23,6 +25,14 @@ pub struct Config {
|
||||||
pub page_cache_size: usize,
|
pub page_cache_size: usize,
|
||||||
/// Maximum number of repository trees to keep in cache
|
/// Maximum number of repository trees to keep in cache
|
||||||
pub repo_cache_size: usize,
|
pub repo_cache_size: usize,
|
||||||
|
/// File extensions that will be served as images (and associated MIME type)
|
||||||
|
pub image_extensions: HashMap<String, String>,
|
||||||
|
/// File extensions that will be served as videos (and associated MIME type)
|
||||||
|
pub video_extensions: HashMap<String, String>,
|
||||||
|
/// File extensions that will be served as audio (and associated MIME type)
|
||||||
|
pub audio_extensions: HashMap<String, String>,
|
||||||
|
/// Authorized origin for serving inline files (CORS)
|
||||||
|
pub origin: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
|
@ -42,6 +52,58 @@ impl Default for Config {
|
||||||
),
|
),
|
||||||
page_cache_size: 200,
|
page_cache_size: 200,
|
||||||
repo_cache_size: 20,
|
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()
|
.try_into()
|
||||||
.expect("Config: invalid repo_cache_size");
|
.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
|
config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
use linemd::Parser;
|
use linemd::Parser;
|
||||||
|
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
|
|
@ -10,7 +12,14 @@ impl Renderer {
|
||||||
hl_registry.link_grammars();
|
hl_registry.link_grammars();
|
||||||
Self { hl_registry }
|
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#"<img alt="No description" src="/inline/{repo_hash_str}/{file_path}"/>"#)
|
||||||
|
} else if config.video_extensions.contains_key(filetype) {
|
||||||
|
return format!(r#"<video src="/inline/{repo_hash_str}/{file_path}"></video>"#)
|
||||||
|
} else if config.audio_extensions.contains_key(filetype) {
|
||||||
|
return format!(r#"<audio src="/inline/{repo_hash_str}/{file_path}"></audio>"#)
|
||||||
|
}
|
||||||
let Ok(content) = str::from_utf8(content) else {
|
let Ok(content) = str::from_utf8(content) else {
|
||||||
return String::from("Cannot render file as it is not valid UTF-8.");
|
return String::from("Cannot render file as it is not valid UTF-8.");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
165
src/server.rs
165
src/server.rs
|
|
@ -3,6 +3,7 @@ use crate::{cache, config::Config, render, repo, templates};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
io::{ErrorKind, Read},
|
io::{ErrorKind, Read},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
@ -30,6 +31,17 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
||||||
crate::api_client::make_client(),
|
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(),
|
trillium_caching_headers::CachingHeaders::new(),
|
||||||
Router::new()
|
Router::new()
|
||||||
|
|
@ -96,57 +108,10 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
||||||
return conn.with_status(401);
|
return conn.with_status(401);
|
||||||
};
|
};
|
||||||
|
|
||||||
let fetch_metadata = |key| {
|
let Some((repo_metadata, root)) = metadata_cache
|
||||||
let repo_dir = PathBuf::from(&config.data_dir)
|
.fetch(repo_hash_str.to_string(), |key| {
|
||||||
.join(crate::SUBDIR_REPOS)
|
fetch_metadata(config, &key)
|
||||||
.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)
|
|
||||||
else {
|
else {
|
||||||
return conn.with_status(404);
|
return conn.with_status(404);
|
||||||
};
|
};
|
||||||
|
|
@ -196,7 +161,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
||||||
.to_string();
|
.to_string();
|
||||||
file_lang.make_ascii_lowercase();
|
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 {
|
} else {
|
||||||
// TODO something?
|
// TODO something?
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +189,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
|
||||||
.to_string();
|
.to_string();
|
||||||
file_lang.make_ascii_lowercase();
|
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 {
|
let template = crate::templates::Repo {
|
||||||
content: html.clone(),
|
content: html.clone(),
|
||||||
root,
|
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 {
|
.get("/e/:secret", |conn: Conn| async move {
|
||||||
conn.ok(crate::templates::Home {}.render().unwrap())
|
conn.ok(crate::templates::Home {}.render().unwrap())
|
||||||
}),
|
}),
|
||||||
|
|
@ -273,3 +283,52 @@ fn fetch_file(config: &Config, repo_hash: &str, file_hash: &str) -> Option<Vec<u
|
||||||
|
|
||||||
Some(buf)
|
Some(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fetch_metadata(
|
||||||
|
config: &Config,
|
||||||
|
repo_hash_str: &str,
|
||||||
|
) -> 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))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue