diff --git a/src/download.rs b/src/download.rs index 002b35d..9bbf937 100644 --- a/src/download.rs +++ b/src/download.rs @@ -3,11 +3,15 @@ use std::str::FromStr; use actix_files::NamedFile; use actix_web::{ error, - http::header::{Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue}, - web, Error, HttpRequest, HttpResponse, + http::header::{ + Accept, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, + Header, + }, + web::{self, delete}, + Error, HttpRequest, HttpResponse, }; use async_std::{fs, path::Path}; -use mime::Mime; +use mime::{Mime, TEXT_HTML}; use sqlx::postgres::PgPool; use url::Url; @@ -19,24 +23,29 @@ const URL_VIEW_HTML: &str = include_str!("../template/url-view.html"); const TEXT_VIEW_SIZE_LIMIT: u64 = 512 * 1024; // 512KiB +enum ViewType { + Raw, + Download, + Html, +} + pub async fn download( req: HttpRequest, db: web::Data, config: web::Data, ) -> Result { let id = req.match_info().query("id"); - let (file_id, file_name, file_kind, delete_on_download) = load_file_info(id, &db).await?; + let (file_id, file_name, file_kind, delete) = load_file_info(id, &db).await?; 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 response = if use_text_view(&file_kind, &content_type, &path, download).await { - build_text_response(&path).await - } else { - build_file_response(download, &file_name, path, content_type, req) + let file_mime = get_content_type(&path); + let response = match get_view_type(&req, &file_kind, &file_mime, &path, delete).await { + ViewType::Raw => build_file_response(false, &file_name, path, file_mime, &req), + ViewType::Download => build_file_response(true, &file_name, path, file_mime, &req), + ViewType::Html => build_text_response(&path).await, }; - if delete_on_download { + if delete { deleter::delete_by_id(&db, &file_id, &config.files_dir) .await .map_err(|db_err| { @@ -72,16 +81,44 @@ fn get_content_type(path: &Path) -> Mime { .expect("tree_magic_mini should not produce invalid mime") } -async fn use_text_view( +async fn get_view_type( + req: &HttpRequest, file_kind: &str, - content_type: &Mime, + file_mime: &Mime, file_path: &Path, - download: bool, -) -> bool { + delete_on_download: bool, +) -> ViewType { + if delete_on_download || req.query_string().contains("dl") { + return ViewType::Download; + } let is_text = - FileKind::from_str(file_kind) == Ok(FileKind::Text) || content_type.type_() == mime::TEXT; - let is_not_large = get_file_size(file_path).await < TEXT_VIEW_SIZE_LIMIT; - is_text && is_not_large && !download + FileKind::from_str(file_kind) == Ok(FileKind::Text) || file_mime.type_() == mime::TEXT; + if !is_text { + return ViewType::Raw; + } + if get_file_size(file_path).await >= TEXT_VIEW_SIZE_LIMIT { + return ViewType::Raw; + } + if let Ok(accept) = Accept::parse(req) { + for accept_mime in accept.mime_precedence() { + if mime_matches(&accept_mime, file_mime) { + return ViewType::Raw; + } + if accept_mime == TEXT_HTML { + break; + } + } + } else { + return ViewType::Raw; + } + + ViewType::Html +} + +fn mime_matches(accept: &Mime, content: &Mime) -> bool { + let type_matches = accept.type_() == content.type_() || accept.type_() == mime::STAR; + let subtype_matches = accept.subtype() == content.subtype() || accept.subtype() == mime::STAR; + type_matches && subtype_matches } async fn get_file_size(file_path: &Path) -> u64 { @@ -114,7 +151,7 @@ fn build_file_response( file_name: &str, path: async_std::path::PathBuf, content_type: Mime, - req: HttpRequest, + req: &HttpRequest, ) -> Result { let content_disposition = ContentDisposition { disposition: if download { @@ -131,7 +168,7 @@ fn build_file_response( })? .set_content_type(content_type) .set_content_disposition(content_disposition); - file.into_response(&req) + file.into_response(req) } fn get_disposition_params(filename: &str) -> Vec {