diff --git a/README.md b/README.md index d9ff565..45c9f8c 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,16 @@ docker-compose up -d --build ## running & config -- The static files directory needs to be next to the binary. +- The `static` files directory needs to be next to the binary. +- The `static` directory needs to be writable + - On startup the `index.html` will be generated based on the config - The maximum filename length is 255 ### General configuration | environment variable | default value | | -------------------- | -------------- | +| STATIC_DIR | ./static | | FILES_DIR | ./files | | UPLOAD_MAX_BYTES | 8388608 (8MiB) | | BIND_ADDRESS | 0.0.0.0:8000 | diff --git a/snippet/auth.html.snippet b/snippet/auth.html.snippet index 8712041..3d5d2ab 100644 --- a/snippet/auth.html.snippet +++ b/snippet/auth.html.snippet @@ -6,4 +6,4 @@
- + diff --git a/src/config.rs b/src/config.rs index 4891950..a44a266 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use chrono::Duration; #[derive(Clone)] pub struct Config { + pub static_dir: PathBuf, pub files_dir: PathBuf, pub max_file_size: Option, pub no_auth_limits: Option, @@ -25,6 +26,8 @@ pub async fn get_config() -> Config { .unwrap_or(8 * 1024 * 1024); let max_file_size = (max_file_size != 0).then(|| max_file_size); + let static_dir = + PathBuf::from(env::var("STATIC_DIR").unwrap_or_else(|_| "./static".to_owned())); let files_dir = PathBuf::from(env::var("FILES_DIR").unwrap_or_else(|_| "./files".to_owned())); fs::create_dir_all(&files_dir) .await @@ -33,6 +36,7 @@ pub async fn get_config() -> Config { let no_auth_limits = get_no_auth_limits(); Config { + static_dir, files_dir, max_file_size, no_auth_limits, @@ -55,7 +59,9 @@ fn get_no_auth_limits() -> Option { }) } (None, None, None, None) => None, - _ => panic!("Incomplete NO_AUTH configuration: All environment variables must be specified") + _ => { + panic!("Incomplete NO_AUTH configuration: All environment variables must be specified") + } } } diff --git a/src/main.rs b/src/main.rs index 20c23d4..5ff1b4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod deleter; mod download; mod file_kind; mod multipart; +mod template; mod upload; use actix_files::Files; @@ -39,6 +40,8 @@ async fn main() -> std::io::Result<()> { config.files_dir.clone(), )); + template::write_prefillable_templates(&config).await; + HttpServer::new({ move || { App::new() @@ -52,9 +55,6 @@ async fn main() -> std::io::Result<()> { web::resource(["/upload/{id}", "/upload/{id}/{name}"]) .route(web::get().to(upload::uploaded)), ) - .service( - web::resource("/assets/auth-hide.js").route(web::get().to(upload::auth_hide)), - ) .service(Files::new("/static", "static").disable_content_disposition()) .service( web::resource([ diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..bee1d0e --- /dev/null +++ b/src/template.rs @@ -0,0 +1,139 @@ +use std::cmp; + +use actix_web::web; +use chrono::Duration; + +use crate::config::Config; + +const INDEX_HTML: &str = include_str!("../template/index.html"); +const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js"); +const AUTH_SNIPPET_HTML: &str = include_str!("../snippet/auth.html.snippet"); +const MAX_SIZE_SNIPPET_HTML: &str = include_str!("../snippet/max_size.html.snippet"); + +pub async fn write_prefillable_templates(config: &Config) { + let index_html = build_index_html(config); + let auth_hide_js = build_auth_hide_js(config); + let index_path = config.static_dir.join("index.html"); + let auth_hide_path = config.static_dir.join("auth-hide.js"); + + async_std::fs::write(index_path, index_html) + .await + .expect("could not write index.html to static folder"); + if let Some(auth_hide_js) = auth_hide_js { + async_std::fs::write(auth_hide_path, auth_hide_js) + .await + .expect("could not write auth-hide.js to static folder"); + } else if auth_hide_path.exists().await { + async_std::fs::remove_file(auth_hide_path) + .await + .expect("could not delete auth-hide.js from static folder"); + } +} + +fn build_index_html(config: &Config) -> String { + let auth_snippet = config + .no_auth_limits + .as_ref() + .map_or("", |_| AUTH_SNIPPET_HTML); + let max_size_snippet = config + .max_file_size + .as_ref() + .map_or("", |_| MAX_SIZE_SNIPPET_HTML); + INDEX_HTML + .replace("{max_size_snippet}", max_size_snippet) + .replace( + "{max_size}", + &render_file_size(config.max_file_size.unwrap_or(0)), + ) + .replace("{auth_snippet}", auth_snippet) + .replace( + "{auth_time}", + &config + .no_auth_limits + .as_ref() + .map(|limit| limit.max_time) + .map_or("".into(), render_duration), + ) + .replace( + "{auth_large_time}", + &config + .no_auth_limits + .as_ref() + .map(|limit| limit.large_file_max_time) + .map_or("".into(), render_duration), + ) + .replace( + "{auth_large_size}", + &config + .no_auth_limits + .as_ref() + .map(|limit| limit.large_file_size) + .map_or("".into(), render_file_size), + ) +} + +fn render_file_size(size: u64) -> String { + let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5); + let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize]; + let value = size / (1024_u64.pow(magnitude)); + format!("{}{}B", value, prefix) +} + +fn render_duration(duration: Duration) -> String { + let days = duration.num_days(); + let hours = duration.num_hours() % 24; + let minutes = duration.num_minutes() % 60; + let seconds = duration.num_seconds() % 60; + let mut elements = vec![]; + if let Some(name) = pluralize(days, "tag", "e") { + elements.push(name); + } + if let Some(name) = pluralize(hours, "stunde", "n") { + elements.push(name); + } + if let Some(name) = pluralize(minutes, "minute", "n") { + elements.push(name); + } + if let Some(name) = pluralize(seconds, "sekunde", "n") { + elements.push(name); + } + elements.join("+") +} + +fn pluralize(number: i64, word: &str, suffix: &str) -> Option { + match number { + 0 => None, + 1 => Some(format!("{} {}", number, word)), + _ => Some(format!("{} {}{}", number, word, suffix)), + } +} + +fn build_auth_hide_js(config: &Config) -> Option { + if let Some(no_auth_limits) = &config.no_auth_limits { + let auth_hide_js = AUTH_HIDE_JS + .replace( + "{no_auth_max_time}", + &no_auth_limits.max_time.num_seconds().to_string(), + ) + .replace( + "{no_auth_large_file_max_time}", + &no_auth_limits.large_file_max_time.num_seconds().to_string(), + ) + .replace( + "{no_auth_large_file_size}", + &no_auth_limits.large_file_size.to_string(), + ); + Some(auth_hide_js) + // Ok(HttpResponse::Ok() + // .content_type("application/javascript") + // .body(auth_hide_js)) + } else { + None + // Err(error::ErrorNotFound("file not found")) + } +} + +pub fn get_host_url(req: &web::HttpRequest) -> String { + let conn = req.connection_info(); + format!("{}://{}", conn.scheme(), conn.host()) +} diff --git a/src/upload.rs b/src/upload.rs index d96790c..077c222 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -1,10 +1,10 @@ use std::io::ErrorKind; -use std::{cmp, vec}; use crate::config::Config; use crate::file_kind::FileKind; -use crate::multipart; use crate::multipart::UploadConfig; +use crate::{multipart, template}; +use actix_files::NamedFile; use actix_multipart::Multipart; use actix_web::{error, web, Error, HttpResponse}; use async_std::{ @@ -12,132 +12,21 @@ use async_std::{ fs::{self, OpenOptions}, path::PathBuf, }; -use chrono::Duration; use rand::prelude::SliceRandom; use sqlx::postgres::PgPool; -const INDEX_HTML: &str = include_str!("../template/index.html"); -const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js"); const UPLOAD_HTML: &str = include_str!("../template/upload.html"); -const AUTH_SNIPPET_HTML: &str = include_str!("../snippet/auth.html.snippet"); -const MAX_SIZE_SNIPPET_HTML: &str = include_str!("../snippet/max_size.html.snippet"); const ID_CHARS: &[char] = &[ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', ]; -pub async fn index( - req: web::HttpRequest, - config: web::Data, -) -> Result { - let filled_index_html = fill_index_html(req, config); - Ok(HttpResponse::Ok() - .content_type("text/html") - .body(filled_index_html)) -} - -fn fill_index_html(req: web::HttpRequest, config: web::Data) -> String { - let upload_url = format!("{}/upload", get_host_url(&req)); - let auth_snippet = config - .no_auth_limits - .as_ref() - .map_or("", |_| AUTH_SNIPPET_HTML); - let max_size_snippet = config - .max_file_size - .as_ref() - .map_or("", |_| MAX_SIZE_SNIPPET_HTML); - INDEX_HTML - .replace("{max_size_snippet}", max_size_snippet) - .replace( - "{max_size}", - &render_file_size(config.max_file_size.unwrap_or(0)), - ) - .replace("{auth_snippet}", auth_snippet) - .replace( - "{auth_time}", - &config - .no_auth_limits - .as_ref() - .map(|limit| limit.max_time) - .map_or("".into(), render_duration), - ) - .replace( - "{auth_large_time}", - &config - .no_auth_limits - .as_ref() - .map(|limit| limit.large_file_max_time) - .map_or("".into(), render_duration), - ) - .replace( - "{auth_large_size}", - &config - .no_auth_limits - .as_ref() - .map(|limit| limit.large_file_size) - .map_or("".into(), render_file_size), - ) - .replace("{upload_url}", upload_url.as_str()) -} - -fn render_file_size(size: u64) -> String { - let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5); - let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize]; - let value = size / (1024_u64.pow(magnitude)); - format!("{}{}B", value, prefix) -} - -fn render_duration(duration: Duration) -> String { - let days = duration.num_days(); - let hours = duration.num_hours() % 24; - let minutes = duration.num_minutes() % 60; - let seconds = duration.num_seconds() % 60; - let mut elements = vec![]; - if let Some(name) = pluralize(days, "tag", "e") { - elements.push(name); - } - if let Some(name) = pluralize(hours, "stunde", "n") { - elements.push(name); - } - if let Some(name) = pluralize(minutes, "minute", "n") { - elements.push(name); - } - if let Some(name) = pluralize(seconds, "sekunde", "n") { - elements.push(name); - } - elements.join("+") -} - -fn pluralize(number: i64, word: &str, suffix: &str) -> Option { - match number { - 0 => None, - 1 => Some(format!("{} {}", number, word)), - _ => Some(format!("{} {}{}", number, word, suffix)), - } -} - -pub async fn auth_hide(config: web::Data) -> Result { - if let Some(no_auth_limits) = &config.no_auth_limits { - let auth_hide_js = AUTH_HIDE_JS - .replace( - "{no_auth_max_time}", - &no_auth_limits.max_time.num_seconds().to_string(), - ) - .replace( - "{no_auth_large_file_max_time}", - &no_auth_limits.large_file_max_time.num_seconds().to_string(), - ) - .replace( - "{no_auth_large_file_size}", - &no_auth_limits.large_file_size.to_string(), - ); - Ok(HttpResponse::Ok() - .content_type("application/javascript") - .body(auth_hide_js)) - } else { - Err(error::ErrorNotFound("file not found")) - } +pub async fn index(config: web::Data) -> Result { + NamedFile::open(config.static_dir.join("index.html")).map_err(|file_err| { + log::error!("index.html could not be read {:?}", file_err); + error::ErrorInternalServerError("this file should be here but could not be found") + }) } pub async fn upload( @@ -250,17 +139,12 @@ fn gen_file_id() -> String { id } -fn get_host_url(req: &web::HttpRequest) -> String { - let conn = req.connection_info(); - format!("{}://{}", conn.scheme(), conn.host()) -} - fn get_file_url(req: &web::HttpRequest, id: &str, name: Option<&str>) -> String { if let Some(name) = name { let encoded_name = urlencoding::encode(name); - format!("{}/{}/{}", get_host_url(req), id, encoded_name) + format!("{}/{}/{}", template::get_host_url(req), id, encoded_name) } else { - format!("{}/{}", get_host_url(req), id) + format!("{}/{}", template::get_host_url(req), id) } } diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..9c9d930 --- /dev/null +++ b/static/index.html @@ -0,0 +1,73 @@ + + + + datatrash + + + + + + +
+

datatrash

+
+ +
+ +
+ +
+ +
+ + +
+ + +
+ +
+
+ nutzung als api +
+datei hochladen
+  curl -F 'file=@yourfile.rs' <domain>/upload
+
+text hochladen
+  curl -F 'text=your text' <domain>/upload
+
+zeitbegrenzung setzen
+  curl -F 'text=your text' -F 'keep_for=1800' <domain>/upload
+
+nach einem download löschen
+  curl -F 'text=your text' -F 'delete_on_download=true' <domain>/upload
+
+authentifizieren
+  curl -F 'text=your text' -F 'password=…' <domain>/upload
+
+
+ + + diff --git a/template/index.html b/template/index.html index 85a3665..041a8be 100644 --- a/template/index.html +++ b/template/index.html @@ -42,19 +42,19 @@ nutzung als api
 datei hochladen
-  curl -F 'file=@yourfile.rs' {upload_url}
+  curl -F 'file=@yourfile.rs' <domain>/upload
 
 text hochladen
-  curl -F 'text=your text' {upload_url}
+  curl -F 'text=your text' <domain>/upload
 
 zeitbegrenzung setzen
-  curl -F 'text=your text' -F 'keep_for=1800' {upload_url}
+  curl -F 'text=your text' -F 'keep_for=1800' <domain>/upload
 
 nach einem download löschen
-  curl -F 'text=your text' -F 'delete_on_download=true' {upload_url}
+  curl -F 'text=your text' -F 'delete_on_download=true' <domain>/upload
 
 authentifizieren
-  curl -F 'text=your text' -F 'password=…' {upload_url}