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 actix_multipart::Multipart; use actix_web::{error, web, Error, HttpResponse}; use async_std::{ channel::Sender, 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 upload( req: web::HttpRequest, payload: Multipart, db: web::Data, expiry_watch_sender: web::Data>, config: web::Data, ) -> Result { let (file_id, file_name) = create_unique_file(&config).await.map_err(|file_err| { log::error!("could not create file {:?}", file_err); error::ErrorInternalServerError("could not create file") })?; let parsed_multipart = multipart::parse_multipart(payload, &file_id, &file_name, &config).await; let UploadConfig { original_name, valid_till, kind, delete_on_download, } = match parsed_multipart { Ok(data) => data, Err(err) => { if file_name.exists().await { fs::remove_file(file_name).await.map_err(|file_err| { log::error!("could not remove file {:?}", file_err); error::ErrorInternalServerError( "could not parse multipart; could not remove file", ) })?; } return Err(err); } }; let db_insert = sqlx::query( "INSERT INTO Files (file_id, file_name, valid_till, kind, delete_on_download) \ VALUES ($1, $2, $3, $4, $5)", ) .bind(&file_id) .bind(&original_name) .bind(valid_till.naive_local()) .bind(kind.to_string()) .bind(delete_on_download) .execute(db.as_ref()) .await; if let Err(db_err) = db_insert { log::error!("could not insert into datebase {:?}", db_err); fs::remove_file(file_name).await.map_err(|file_err| { log::error!("could not remove file {:?}", file_err); error::ErrorInternalServerError( "could not insert file into database; could not remove file", ) })?; return Err(error::ErrorInternalServerError( "could not insert file into database", )); } log::info!( "{} create new file {} (valid_till: {}, kind: {}, delete_on_download: {})", req.connection_info().realip_remote_addr().unwrap_or("-"), file_id, valid_till, kind, delete_on_download ); expiry_watch_sender.send(()).await.unwrap(); let redirect = if kind == FileKind::Binary { let encoded_name = urlencoding::encode(&original_name); format!("/upload/{}/{}", file_id, encoded_name) } else { format!("/upload/{}", file_id) }; let url = get_file_url(&req, &file_id, Some(&original_name)); Ok(HttpResponse::SeeOther() .header("location", redirect) .body(format!("{}\n", url))) } async fn create_unique_file( config: &web::Data, ) -> Result<(String, PathBuf), std::io::Error> { loop { let file_id = gen_file_id(); let mut file_name = config.files_dir.clone(); file_name.push(&file_id); match OpenOptions::new() .write(true) .create_new(true) .open(&file_name) .await { Ok(_) => return Ok((file_id, file_name)), Err(error) if error.kind() == ErrorKind::AlreadyExists => continue, Err(error) => return Err(error), } } } fn gen_file_id() -> String { let mut rng = rand::thread_rng(); let mut id = String::with_capacity(5); for _ in 0..5 { id.push(*ID_CHARS.choose(&mut rng).expect("ID_CHARS is not empty")); } 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) } else { format!("{}/{}", get_host_url(req), id) } } pub async fn uploaded(req: web::HttpRequest) -> Result { let id = req.match_info().query("id"); let name = req.match_info().get("name"); let url = get_file_url(&req, id, name); let upload_html = UPLOAD_HTML.replace("{url}", url.as_str()); Ok(HttpResponse::Ok() .content_type("text/html") .body(upload_html)) }