This commit is contained in:
Pascal Engélibert 2026-07-31 10:14:40 +02:00
commit 5bcc40d573
5 changed files with 105 additions and 12 deletions

View file

@ -12,7 +12,9 @@ Early development: not usable yet.
* [x] Repo caching * [x] Repo caching
* [x] Submission form * [x] Submission form
* [x] Config * [x] Config
* [ ] Repo serving * [x] Repo serving
* [x] Allow download
* [x] Markdown rendering
* [ ] Replace words * [ ] Replace words
* [ ] Abuse report * [ ] Abuse report
* [ ] Admin tools * [ ] Admin tools
@ -21,9 +23,11 @@ Early development: not usable yet.
* [ ] Expiration * [ ] Expiration
* [ ] Manual removal * [ ] Manual removal
* [ ] Security tests (zip bomb) * [ ] Security tests (zip bomb)
* [ ] Allow download * [ ] Security tests (URL encoding in links, HTML, HTTP headers)
* [ ] Markdown rendering
* [ ] Error pages * [ ] Error pages
* [ ] Webdesign
* [ ] Compatibility with other forges
* [ ] Better logo (contribution welcome)
## Design choices ## Design choices

View file

@ -12,13 +12,23 @@ impl Renderer {
hl_registry.link_grammars(); hl_registry.link_grammars();
Self { hl_registry } Self { hl_registry }
} }
pub fn render(&self, config: &Config, filetype: &str, content: &[u8], pretty: bool, repo_hash_str: &str, file_path: &str) -> 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) { if config.image_extensions.contains_key(filetype) {
return format!(r#"<img alt="No description" src="/inline/{repo_hash_str}/{file_path}"/>"#) return format!(
r#"<img alt="No description" src="/inline/{repo_hash_str}/{file_path}"/>"#
);
} else if config.video_extensions.contains_key(filetype) { } else if config.video_extensions.contains_key(filetype) {
return format!(r#"<video src="/inline/{repo_hash_str}/{file_path}"></video>"#) return format!(r#"<video src="/inline/{repo_hash_str}/{file_path}"></video>"#);
} else if config.audio_extensions.contains_key(filetype) { } else if config.audio_extensions.contains_key(filetype) {
return format!(r#"<audio src="/inline/{repo_hash_str}/{file_path}"></audio>"#) 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.");

View file

@ -161,16 +161,25 @@ 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(config, &file_lang, &file_content, true, repo_hash_str, &served_file.path); html = renderer.render(
config,
&file_lang,
&file_content,
true,
repo_hash_str,
&served_file.path,
);
} else { } else {
// TODO something? // TODO something?
} }
let template = crate::templates::Repo { let template = crate::templates::Repo {
current_entry: served_entry.into(),
content: html.clone(), content: html.clone(),
root, root,
path: conn.path().split('/').map(|s| s.to_string()).collect(), path: conn.path().split('/').map(|s| s.to_string()).collect(),
title: repo_metadata.title, title: repo_metadata.title,
repo_hash: repo_hash_str.into(),
}; };
template.render().unwrap() template.render().unwrap()
} }
@ -189,12 +198,21 @@ 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(config, &file_lang, &file_content, true, repo_hash_str, &served_file.path); 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 {
current_entry: served_entry.into(),
content: html.clone(), content: html.clone(),
root, root,
path: conn.path().split('/').map(|s| s.to_string()).collect(), path: conn.path().split('/').map(|s| s.to_string()).collect(),
title: repo_metadata.title, title: repo_metadata.title,
repo_hash: repo_hash_str.into(),
}; };
template.render().unwrap() template.render().unwrap()
} }
@ -228,9 +246,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
}; };
match served_entry { match served_entry {
templates::EntryRef::Directory(_) => { templates::EntryRef::Directory(_) => conn.with_status(404),
conn.with_status(404)
}
templates::EntryRef::File(served_file) => { templates::EntryRef::File(served_file) => {
let Some(extension) = served_file.name.rsplit('.').next() else { let Some(extension) = served_file.name.rsplit('.').next() else {
return conn.with_status(404); return conn.with_status(404);
@ -255,6 +271,51 @@ pub fn make_router(config: &'static Config) -> impl Handler {
} }
} }
}) })
.get("/download/: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(file_content) =
fetch_file(config, repo_hash_str, &served_file.hash)
else {
return conn.with_status(500);
};
let disposition = if served_file.name.is_ascii() {
format!(
r#"attachment; filename="{}""#,
askama::filters::urlencode(&served_file.name).unwrap().0
)
} else {
format!(
"attachment; filename*=UTF-8''{}",
askama::filters::urlencode(&served_file.name).unwrap().0
)
};
conn.with_response_header(
"Access-Control-Allow-Origin",
config.origin.as_str(),
)
.with_response_header("Content-Disposition", disposition)
.ok(file_content)
}
}
})
.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())
}), }),

View file

@ -19,6 +19,10 @@ pub struct Repo {
pub path: Vec<String>, pub path: Vec<String>,
/// Repository title /// Repository title
pub title: String, pub title: String,
/// Repository id
pub repo_hash: String,
/// Served entry
pub current_entry: Entry,
} }
#[derive(Clone, Template)] #[derive(Clone, Template)]
@ -53,6 +57,15 @@ pub enum EntryRef<'a> {
File(&'a File), File(&'a File),
} }
impl<'a> From<EntryRef<'a>> for Entry {
fn from(value: EntryRef<'a>) -> Self {
match value {
EntryRef::Directory(dir) => Entry::Directory(dir.clone()),
EntryRef::File(file) => Entry::File(file.clone()),
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct File { pub struct File {
pub name: String, pub name: String,

View file

@ -39,6 +39,11 @@ html, body {
<span> <span>
{% for element in path %}/{{ element }}{% endfor %} {% for element in path %}/{{ element }}{% endfor %}
</span> </span>
{% match current_entry %}
{% when Entry::Directory(dir) %}
{% when Entry::File(file) %}
<a href="/download/{{ repo_hash|safe }}/{{ file.path }}">Download file</a>
{% endmatch %}
<div id="content"> <div id="content">
{{ content|safe }} {{ content|safe }}
</div> </div>