2022-02-26 23:34:57 +00:00
|
|
|
use std::{path::PathBuf, str::FromStr};
|
2021-09-09 20:18:42 +00:00
|
|
|
|
2021-04-04 12:30:31 +00:00
|
|
|
use actix_files::NamedFile;
|
|
|
|
use actix_web::{
|
|
|
|
error,
|
2021-12-20 14:00:13 +00:00
|
|
|
http::header::{
|
|
|
|
Accept, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
|
|
|
Header,
|
|
|
|
},
|
2021-04-04 12:30:31 +00:00
|
|
|
web, Error, HttpRequest, HttpResponse,
|
|
|
|
};
|
2021-12-20 14:00:13 +00:00
|
|
|
use mime::{Mime, TEXT_HTML};
|
2021-09-10 15:44:37 +00:00
|
|
|
use sqlx::postgres::PgPool;
|
2022-02-26 23:34:57 +00:00
|
|
|
use std::path::Path;
|
|
|
|
use tokio::fs;
|
2021-09-09 20:18:42 +00:00
|
|
|
use url::Url;
|
2021-04-04 12:30:31 +00:00
|
|
|
|
|
|
|
use crate::deleter;
|
2021-04-04 12:33:37 +00:00
|
|
|
use crate::{config::Config, file_kind::FileKind};
|
2021-04-04 12:30:31 +00:00
|
|
|
|
2021-09-09 20:18:42 +00:00
|
|
|
const TEXT_VIEW_HTML: &str = include_str!("../template/text-view.html");
|
|
|
|
const URL_VIEW_HTML: &str = include_str!("../template/url-view.html");
|
2021-04-04 12:30:31 +00:00
|
|
|
|
2021-09-12 16:51:14 +00:00
|
|
|
const TEXT_VIEW_SIZE_LIMIT: u64 = 512 * 1024; // 512KiB
|
|
|
|
|
2021-12-20 14:00:13 +00:00
|
|
|
enum ViewType {
|
|
|
|
Raw,
|
|
|
|
Download,
|
|
|
|
Html,
|
|
|
|
}
|
|
|
|
|
2021-04-04 12:30:31 +00:00
|
|
|
pub async fn download(
|
|
|
|
req: HttpRequest,
|
|
|
|
db: web::Data<PgPool>,
|
|
|
|
config: web::Data<Config>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
let id = req.match_info().query("id");
|
2021-12-20 14:00:13 +00:00
|
|
|
let (file_id, file_name, file_kind, delete) = load_file_info(id, &db).await?;
|
2021-04-04 12:30:31 +00:00
|
|
|
let mut path = config.files_dir.clone();
|
|
|
|
path.push(&file_id);
|
|
|
|
|
2021-12-20 14:00:13 +00:00
|
|
|
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,
|
2021-04-04 12:30:31 +00:00
|
|
|
};
|
2021-12-20 14:00:13 +00:00
|
|
|
if delete {
|
2021-04-04 12:30:31 +00:00
|
|
|
deleter::delete_by_id(&db, &file_id, &config.files_dir)
|
|
|
|
.await
|
2021-04-07 22:33:22 +00:00
|
|
|
.map_err(|db_err| {
|
|
|
|
log::error!("could not delete file {:?}", db_err);
|
|
|
|
error::ErrorInternalServerError("could not delete file")
|
|
|
|
})?;
|
2021-04-04 12:30:31 +00:00
|
|
|
}
|
|
|
|
response
|
|
|
|
}
|
|
|
|
|
2021-09-09 20:18:42 +00:00
|
|
|
async fn load_file_info(
|
|
|
|
id: &str,
|
|
|
|
db: &web::Data<sqlx::Pool<sqlx::Postgres>>,
|
|
|
|
) -> Result<(String, String, String, bool), Error> {
|
2021-09-10 15:44:37 +00:00
|
|
|
sqlx::query_as(
|
2021-09-09 20:18:42 +00:00
|
|
|
"SELECT file_id, file_name, kind, delete_on_download from files WHERE file_id = $1",
|
|
|
|
)
|
|
|
|
.bind(id)
|
2021-09-10 15:44:37 +00:00
|
|
|
.fetch_optional(db.as_ref())
|
|
|
|
.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"))
|
2021-09-09 20:18:42 +00:00
|
|
|
}
|
|
|
|
|
2021-08-15 21:23:03 +00:00
|
|
|
fn get_content_type(path: &Path) -> Mime {
|
2021-04-04 12:30:31 +00:00
|
|
|
let std_path = std::path::Path::new(path.as_os_str());
|
2021-08-15 21:23:03 +00:00
|
|
|
tree_magic_mini::from_filepath(std_path)
|
2021-04-04 12:30:31 +00:00
|
|
|
.unwrap_or("application/octet-stream")
|
|
|
|
.parse::<Mime>()
|
2021-08-15 21:23:03 +00:00
|
|
|
.expect("tree_magic_mini should not produce invalid mime")
|
2021-04-04 12:30:31 +00:00
|
|
|
}
|
|
|
|
|
2021-12-20 14:00:13 +00:00
|
|
|
async fn get_view_type(
|
|
|
|
req: &HttpRequest,
|
2021-09-12 16:51:14 +00:00
|
|
|
file_kind: &str,
|
2021-12-20 14:00:13 +00:00
|
|
|
file_mime: &Mime,
|
2021-09-12 16:51:14 +00:00
|
|
|
file_path: &Path,
|
2021-12-20 14:00:13 +00:00
|
|
|
delete_on_download: bool,
|
|
|
|
) -> ViewType {
|
|
|
|
if delete_on_download || req.query_string().contains("dl") {
|
|
|
|
return ViewType::Download;
|
|
|
|
}
|
2022-04-23 21:40:35 +00:00
|
|
|
if req.query_string().contains("raw") {
|
|
|
|
return ViewType::Raw;
|
|
|
|
}
|
2021-09-12 16:51:14 +00:00
|
|
|
let is_text =
|
2021-12-20 14:00:13 +00:00
|
|
|
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) {
|
2022-02-26 23:34:57 +00:00
|
|
|
for accept_mime in accept.ranked() {
|
2021-12-20 14:00:13 +00:00
|
|
|
if accept_mime == TEXT_HTML {
|
2022-04-23 21:40:35 +00:00
|
|
|
return ViewType::Html;
|
|
|
|
}
|
|
|
|
if mime_matches(&accept_mime, file_mime) {
|
2021-12-20 14:00:13 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-23 21:40:35 +00:00
|
|
|
ViewType::Raw
|
2021-12-20 14:00:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2021-09-12 16:51:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async fn get_file_size(file_path: &Path) -> u64 {
|
|
|
|
fs::metadata(file_path)
|
|
|
|
.await
|
|
|
|
.map(|metadata| metadata.len())
|
|
|
|
.unwrap_or(0)
|
|
|
|
}
|
|
|
|
|
2021-09-09 20:18:42 +00:00
|
|
|
async fn build_text_response(path: &Path) -> Result<HttpResponse, Error> {
|
|
|
|
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);
|
2021-09-09 23:45:12 +00:00
|
|
|
let html = if !content.contains(&['\n', '\r'][..]) && Url::from_str(&content).is_ok() {
|
2021-09-09 20:18:42 +00:00
|
|
|
let attribute_encoded = htmlescape::encode_attribute(&content);
|
|
|
|
URL_VIEW_HTML
|
|
|
|
.replace("{link_content}", &encoded)
|
|
|
|
.replace("{link_attribute}", &attribute_encoded)
|
|
|
|
} else {
|
|
|
|
TEXT_VIEW_HTML.replace("{text}", &encoded)
|
|
|
|
};
|
2021-12-20 14:40:49 +00:00
|
|
|
Ok(HttpResponse::Ok()
|
|
|
|
.content_type(TEXT_HTML.to_string())
|
|
|
|
.body(html))
|
2021-09-09 20:18:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn build_file_response(
|
|
|
|
download: bool,
|
|
|
|
file_name: &str,
|
2022-02-26 23:34:57 +00:00
|
|
|
path: PathBuf,
|
2021-09-09 20:18:42 +00:00
|
|
|
content_type: Mime,
|
2021-12-20 14:00:13 +00:00
|
|
|
req: &HttpRequest,
|
2021-09-09 20:18:42 +00:00
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
let content_disposition = ContentDisposition {
|
|
|
|
disposition: if download {
|
|
|
|
DispositionType::Attachment
|
|
|
|
} else {
|
|
|
|
DispositionType::Inline
|
|
|
|
},
|
2021-09-11 00:08:47 +00:00
|
|
|
parameters: get_disposition_params(file_name),
|
2021-09-09 20:18:42 +00:00
|
|
|
};
|
|
|
|
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);
|
2022-02-26 23:34:57 +00:00
|
|
|
Ok(file.into_response(req))
|
2021-09-09 20:18:42 +00:00
|
|
|
}
|
|
|
|
|
2021-04-04 12:30:31 +00:00
|
|
|
fn get_disposition_params(filename: &str) -> Vec<DispositionParam> {
|
|
|
|
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
|
|
|
|
}
|