use actix_files::NamedFile; use actix_web::{ error, http::header::{Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue}, web, Error, HttpRequest, HttpResponse, }; use async_std::{fs, path::Path}; use futures::TryStreamExt; use mime::Mime; use sqlx::{ postgres::{PgPool, PgRow}, Row, }; use crate::deleter; use crate::{config::Config, file_kind::FileKind}; const VIEW_HTML: &str = include_str!("../template/view.html"); pub async fn download( req: HttpRequest, db: web::Data, config: web::Data, ) -> Result { let id = req.match_info().query("id"); let mut rows = sqlx::query( "SELECT file_id, file_name, kind, delete_on_download from files WHERE file_id = $1", ) .bind(id) .fetch(db.as_ref()); let row: PgRow = rows .try_next() .await .map_err(|db_err| { log::error!("could not run select statement {:?}", db_err); error::ErrorInternalServerError("could not run select statement") })? .ok_or_else(|| error::ErrorNotFound("file does not exist or has expired"))?; let file_id: String = row.get("file_id"); let file_name: String = row.get("file_name"); let file_kind: String = row.get("kind"); let delete_on_download: bool = row.get("delete_on_download"); let mut path = config.files_dir.clone(); path.push(&file_id); let download = delete_on_download || req.query_string().contains("dl"); let content_type = get_content_type(&path); let is_text = file_kind == FileKind::Text.to_string() || content_type.type_() == mime::TEXT; let response = if is_text && !download { let content = fs::read_to_string(path).await.map_err(|file_err| { log::error!("file could not be read {:?}", file_err); error::ErrorInternalServerError("this file should be here but could not be found") })?; let encoded = htmlescape::encode_minimal(&content); let view_html = VIEW_HTML.replace("{text}", &encoded); let response = HttpResponse::Ok().content_type("text/html").body(view_html); Ok(response) } else { let content_disposition = ContentDisposition { disposition: if download { DispositionType::Attachment } else { DispositionType::Inline }, parameters: get_disposition_params(&file_name), }; let file = NamedFile::open(path) .map_err(|file_err| { log::error!("file could not be read {:?}", file_err); error::ErrorInternalServerError("this file should be here but could not be found") })? .set_content_type(content_type) .set_content_disposition(content_disposition); file.into_response(&req) }; if delete_on_download { deleter::delete_by_id(&db, &file_id, &config.files_dir) .await .map_err(|db_err| { log::error!("could not delete file {:?}", db_err); error::ErrorInternalServerError("could not delete file") })?; } response } fn get_content_type(path: &Path) -> Mime { let std_path = std::path::Path::new(path.as_os_str()); tree_magic_mini::from_filepath(std_path) .unwrap_or("application/octet-stream") .parse::() .expect("tree_magic_mini should not produce invalid mime") } fn get_disposition_params(filename: &str) -> Vec { let mut parameters = vec![DispositionParam::Filename(filename.to_owned())]; if !filename.is_ascii() { parameters.push(DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, value: filename.to_owned().into_bytes(), })) } parameters }