datatrash/src/upload.rs

167 lines
5.5 KiB
Rust

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<Config>) -> Result<NamedFile, Error> {
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<PgPool>,
expiry_watch_sender: web::Data<Sender<()>>,
config: web::Data<Config>,
) -> Result<HttpResponse, Error> {
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<sqlx::Pool<sqlx::Postgres>>,
) -> 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<Config>,
) -> Result<(String, PathBuf), std::io::Error> {
loop {
let file_id = gen_file_id();
let mut file_path = config.files_dir.clone();
file_path.push(&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<HttpResponse, Error> {
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))
}