use std::io::ErrorKind; use crate::config::Config; use crate::multipart::UploadConfig; use crate::{mime_relations, multipart, template}; use actix_files::NamedFile; use actix_multipart::Multipart; use actix_web::http::header::LOCATION; use actix_web::{error, web, Error, HttpRequest, HttpResponse}; use rand::{distributions::Slice, Rng}; use sqlx::postgres::PgPool; use std::path::PathBuf; use tokio::fs::{self, OpenOptions}; use tokio::sync::mpsc::Sender; const UPLOAD_HTML: &str = include_str!("../template/upload.html"); const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html"); const ID_CHARS: &[char] = &[ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', '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(config: web::Data) -> Result { let file = 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") })?; Ok(file.disable_content_disposition()) } pub async fn upload( req: HttpRequest, payload: Multipart, db: web::Data, expiry_watch_sender: web::Data>, config: web::Data, ) -> Result { let (file_id, file_path) = create_unique_file(&config).await.map_err(|file_err| { log::error!("could not create file {:?}", file_err); error::ErrorInternalServerError("could not create file") })?; let upload_config = multipart::parse_multipart(payload, &file_path, &config).await?; let file_name = upload_config.original_name.clone().unwrap_or_else(|| { format!( "{file_id}.{}", mime_relations::get_extension(&upload_config.content_type).unwrap_or("txt") ) }); insert_file_metadata(&file_id, file_name, &upload_config, db).await?; log::info!( "{} create new file {} (valid_till: {}, content_type: {}, delete_on_download: {})", req.connection_info().realip_remote_addr().unwrap_or("-"), file_id, upload_config.valid_till, upload_config.content_type, upload_config.delete_on_download ); expiry_watch_sender.send(()).await.unwrap(); let redirect = get_redirect_url(&file_id, upload_config.original_name.as_deref()); let url = get_file_url(&req, &file_id, upload_config.original_name.as_deref()); Ok(HttpResponse::SeeOther() .insert_header((LOCATION, redirect)) .body(format!("{url}\n"))) } async fn insert_file_metadata( file_id: &String, file_name: String, upload_config: &UploadConfig, db: web::Data>, ) -> Result<(), Error> { let db_insert = sqlx::query( "INSERT INTO Files (file_id, file_name, content_type, valid_till, delete_on_download) \ VALUES ($1, $2, $3, $4, $5)", ) .bind(file_id) .bind(&file_name) .bind(&upload_config.content_type.to_string()) .bind(upload_config.valid_till) .bind(upload_config.delete_on_download) .execute(db.as_ref()) .await; if let Err(db_err) = db_insert { log::error!("could not insert into datebase {:?}", db_err); if let Err(file_err) = fs::remove_file(file_name).await { log::error!("could not remove file {:?}", file_err); } return Err(error::ErrorInternalServerError( "could not insert file into database", )); } Ok(()) } async fn create_unique_file( config: &web::Data, ) -> Result<(String, PathBuf), std::io::Error> { loop { let file_id = gen_file_id(); let file_path = config.files_dir.join(&file_id); match OpenOptions::new() .write(true) .create_new(true) .open(&file_path) .await { Ok(_) => return Ok((file_id, file_path)), Err(error) if error.kind() == ErrorKind::AlreadyExists => continue, Err(error) => return Err(error), } } } fn gen_file_id() -> String { let distribution = Slice::new(ID_CHARS).expect("ID_CHARS is not empty"); rand::thread_rng() .sample_iter(distribution) .take(5) .collect() } fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String { let host = template::get_host_url(req); if let Some(name) = name { let encoded_name = urlencoding::encode(name); format!("{host}/{id}/{encoded_name}") } else { format!("{host}/{id}") } } fn get_redirect_url(id: &str, name: Option<&str>) -> String { if let Some(name) = name { let encoded_name = urlencoding::encode(name); format!("/upload/{id}/{encoded_name}") } else { format!("/upload/{id}") } } pub async fn uploaded(req: HttpRequest) -> Result { let id = req.match_info().query("id"); let name = req .match_info() .get("name") .map(urlencoding::decode) .transpose() .map_err(|_| error::ErrorBadRequest("name is invalid utf-8"))?; let upload_html = if name.is_some() { UPLOAD_SHORT_HTML .replace("{link}", &get_file_url(&req, id, name.as_deref())) .replace("{shortlink}", &get_file_url(&req, id, None)) } else { UPLOAD_HTML.replace("{link}", &get_file_url(&req, id, name.as_deref())) }; Ok(HttpResponse::Ok() .content_type("text/html") .body(upload_html)) }