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#"No description"#) + return format!( + r#"No description"# + ); } 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 }}