diff --git a/README.md b/README.md
index 85e29d4..b770a6b 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,9 @@ Early development: not usable yet.
* [x] Repo caching
* [x] Submission form
* [x] Config
-* [ ] Repo serving
+* [x] Repo serving
+* [x] Allow download
+* [x] Markdown rendering
* [ ] Replace words
* [ ] Abuse report
* [ ] Admin tools
@@ -21,9 +23,11 @@ Early development: not usable yet.
* [ ] Expiration
* [ ] Manual removal
* [ ] Security tests (zip bomb)
-* [ ] Allow download
-* [ ] Markdown rendering
+* [ ] Security tests (URL encoding in links, HTML, HTTP headers)
* [ ] Error pages
+* [ ] Webdesign
+* [ ] Compatibility with other forges
+* [ ] Better logo (contribution welcome)
## Design choices
diff --git a/src/render.rs b/src/render.rs
index f9a130c..e3f9762 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -12,13 +12,23 @@ impl Renderer {
hl_registry.link_grammars();
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) {
- return format!(r#"
"#)
+ return format!(
+ r#"
"#
+ );
} else if config.video_extensions.contains_key(filetype) {
- return format!(r#""#)
+ return format!(r#""#);
} else if config.audio_extensions.contains_key(filetype) {
- return format!(r#""#)
+ 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 1e61bb1..e9a7bc6 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -161,16 +161,25 @@ pub fn make_router(config: &'static Config) -> impl Handler {
.to_string();
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 {
// TODO something?
}
let template = crate::templates::Repo {
+ current_entry: served_entry.into(),
content: html.clone(),
root,
path: conn.path().split('/').map(|s| s.to_string()).collect(),
title: repo_metadata.title,
+ repo_hash: repo_hash_str.into(),
};
template.render().unwrap()
}
@@ -189,12 +198,21 @@ pub fn make_router(config: &'static Config) -> impl Handler {
.to_string();
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 {
+ current_entry: served_entry.into(),
content: html.clone(),
root,
path: conn.path().split('/').map(|s| s.to_string()).collect(),
title: repo_metadata.title,
+ repo_hash: repo_hash_str.into(),
};
template.render().unwrap()
}
@@ -228,9 +246,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
};
match served_entry {
- templates::EntryRef::Directory(_) => {
- conn.with_status(404)
- }
+ 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);
@@ -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 {
conn.ok(crate::templates::Home {}.render().unwrap())
}),
diff --git a/src/templates.rs b/src/templates.rs
index 40378ee..2fb7a14 100644
--- a/src/templates.rs
+++ b/src/templates.rs
@@ -19,6 +19,10 @@ pub struct Repo {
pub path: Vec,
/// Repository title
pub title: String,
+ /// Repository id
+ pub repo_hash: String,
+ /// Served entry
+ pub current_entry: Entry,
}
#[derive(Clone, Template)]
@@ -53,6 +57,15 @@ pub enum EntryRef<'a> {
File(&'a File),
}
+impl<'a> From> 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)]
pub struct File {
pub name: String,
diff --git a/templates/repo.html b/templates/repo.html
index 743889a..8733337 100644
--- a/templates/repo.html
+++ b/templates/repo.html
@@ -39,6 +39,11 @@ html, body {
{% for element in path %}/{{ element }}{% endfor %}
+ {% match current_entry %}
+ {% when Entry::Directory(dir) %}
+ {% when Entry::File(file) %}
+ Download file
+ {% endmatch %}
{{ content|safe }}