2021-09-11 00:08:47 +00:00
|
|
|
use std::io::ErrorKind;
|
2021-08-18 21:22:50 +00:00
|
|
|
|
2021-04-04 12:30:31 +00:00
|
|
|
use crate::config::Config;
|
|
|
|
use crate::multipart::UploadConfig;
|
2022-10-07 13:51:38 +00:00
|
|
|
use crate::{mime_relations, multipart, template};
|
2021-12-08 17:54:55 +00:00
|
|
|
use actix_files::NamedFile;
|
2021-04-04 12:30:31 +00:00
|
|
|
use actix_multipart::Multipart;
|
2022-02-26 23:34:57 +00:00
|
|
|
use actix_web::http::header::LOCATION;
|
|
|
|
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
|
2021-04-04 12:30:31 +00:00
|
|
|
use rand::prelude::SliceRandom;
|
|
|
|
use sqlx::postgres::PgPool;
|
2022-02-26 23:34:57 +00:00
|
|
|
use std::path::PathBuf;
|
|
|
|
use tokio::fs::{self, OpenOptions};
|
|
|
|
use tokio::sync::mpsc::Sender;
|
2021-04-04 12:30:31 +00:00
|
|
|
|
|
|
|
const UPLOAD_HTML: &str = include_str!("../template/upload.html");
|
2021-12-20 00:06:28 +00:00
|
|
|
const UPLOAD_SHORT_HTML: &str = include_str!("../template/upload-short.html");
|
2021-04-04 12:30:31 +00:00
|
|
|
|
|
|
|
const ID_CHARS: &[char] = &[
|
2022-08-21 16:44:12 +00:00
|
|
|
'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',
|
2021-04-04 12:30:31 +00:00
|
|
|
];
|
|
|
|
|
2021-12-08 17:54:55 +00:00
|
|
|
pub async fn index(config: web::Data<Config>) -> Result<NamedFile, Error> {
|
2022-01-29 11:50:44 +00:00
|
|
|
let file = NamedFile::open(config.static_dir.join("index.html")).map_err(|file_err| {
|
2021-12-08 17:54:55 +00:00
|
|
|
log::error!("index.html could not be read {:?}", file_err);
|
|
|
|
error::ErrorInternalServerError("this file should be here but could not be found")
|
2022-01-29 11:50:44 +00:00
|
|
|
})?;
|
|
|
|
Ok(file.disable_content_disposition())
|
2021-04-04 12:30:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn upload(
|
2022-02-26 23:34:57 +00:00
|
|
|
req: HttpRequest,
|
2021-04-04 12:30:31 +00:00
|
|
|
payload: Multipart,
|
|
|
|
db: web::Data<PgPool>,
|
|
|
|
expiry_watch_sender: web::Data<Sender<()>>,
|
|
|
|
config: web::Data<Config>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
2021-09-11 00:08:47 +00:00
|
|
|
let (file_id, file_name) = create_unique_file(&config).await.map_err(|file_err| {
|
|
|
|
log::error!("could not create file {:?}", file_err);
|
|
|
|
error::ErrorInternalServerError("could not create file")
|
|
|
|
})?;
|
2021-04-04 12:30:31 +00:00
|
|
|
|
2022-08-18 21:20:56 +00:00
|
|
|
let parsed_multipart = multipart::parse_multipart(payload, &file_name, &config).await;
|
2021-04-04 12:30:31 +00:00
|
|
|
let UploadConfig {
|
|
|
|
original_name,
|
2022-08-18 21:20:56 +00:00
|
|
|
content_type,
|
2021-04-04 12:30:31 +00:00
|
|
|
valid_till,
|
|
|
|
delete_on_download,
|
|
|
|
} = match parsed_multipart {
|
|
|
|
Ok(data) => data,
|
|
|
|
Err(err) => {
|
2022-02-26 23:34:57 +00:00
|
|
|
match fs::remove_file(file_name).await {
|
|
|
|
Ok(()) => {}
|
|
|
|
Err(err) if err.kind() == ErrorKind::NotFound => {}
|
|
|
|
Err(err) => {
|
|
|
|
log::error!("could not remove file {:?}", err);
|
|
|
|
return Err(error::ErrorInternalServerError(
|
2021-04-04 12:30:31 +00:00
|
|
|
"could not parse multipart; could not remove file",
|
2022-02-26 23:34:57 +00:00
|
|
|
));
|
|
|
|
}
|
2021-04-04 12:30:31 +00:00
|
|
|
}
|
|
|
|
return Err(err);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-10-07 13:51:38 +00:00
|
|
|
let file_name = original_name.clone().unwrap_or_else(|| {
|
|
|
|
format!(
|
2022-10-07 13:52:12 +00:00
|
|
|
"{file_id}.{}",
|
2022-10-07 13:51:38 +00:00
|
|
|
mime_relations::get_extension(&content_type).unwrap_or("txt")
|
|
|
|
)
|
|
|
|
});
|
2021-04-04 12:30:31 +00:00
|
|
|
let db_insert = sqlx::query(
|
2022-08-18 21:20:56 +00:00
|
|
|
"INSERT INTO Files (file_id, file_name, content_type, valid_till, delete_on_download) \
|
2021-04-04 12:30:31 +00:00
|
|
|
VALUES ($1, $2, $3, $4, $5)",
|
|
|
|
)
|
|
|
|
.bind(&file_id)
|
2022-08-18 21:20:56 +00:00
|
|
|
.bind(&file_name)
|
|
|
|
.bind(&content_type.to_string())
|
2022-02-27 00:50:29 +00:00
|
|
|
.bind(valid_till)
|
2021-04-04 12:30:31 +00:00
|
|
|
.bind(delete_on_download)
|
|
|
|
.execute(db.as_ref())
|
|
|
|
.await;
|
2021-04-07 22:33:22 +00:00
|
|
|
if let Err(db_err) = db_insert {
|
|
|
|
log::error!("could not insert into datebase {:?}", db_err);
|
2021-09-11 00:08:47 +00:00
|
|
|
fs::remove_file(file_name).await.map_err(|file_err| {
|
2021-04-07 22:33:22 +00:00
|
|
|
log::error!("could not remove file {:?}", file_err);
|
2021-04-04 12:30:31 +00:00
|
|
|
error::ErrorInternalServerError(
|
|
|
|
"could not insert file into database; could not remove file",
|
|
|
|
)
|
|
|
|
})?;
|
|
|
|
return Err(error::ErrorInternalServerError(
|
|
|
|
"could not insert file into database",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
log::info!(
|
2022-08-18 21:20:56 +00:00
|
|
|
"{} create new file {} (valid_till: {}, content_type: {}, delete_on_download: {})",
|
2021-04-04 12:30:31 +00:00
|
|
|
req.connection_info().realip_remote_addr().unwrap_or("-"),
|
|
|
|
file_id,
|
|
|
|
valid_till,
|
2022-08-18 21:20:56 +00:00
|
|
|
content_type,
|
2021-04-04 12:30:31 +00:00
|
|
|
delete_on_download
|
|
|
|
);
|
|
|
|
|
|
|
|
expiry_watch_sender.send(()).await.unwrap();
|
|
|
|
|
2022-08-18 21:20:56 +00:00
|
|
|
let redirect = if let Some(original_name) = original_name.as_ref() {
|
|
|
|
let encoded_name = urlencoding::encode(original_name);
|
2022-10-07 13:52:12 +00:00
|
|
|
format!("/upload/{file_id}/{encoded_name}")
|
2021-04-04 12:30:31 +00:00
|
|
|
} else {
|
2022-10-07 13:52:12 +00:00
|
|
|
format!("/upload/{file_id}")
|
2021-04-04 12:30:31 +00:00
|
|
|
};
|
|
|
|
|
2022-08-18 21:20:56 +00:00
|
|
|
let url = get_file_url(&req, &file_id, original_name.as_deref());
|
2021-04-04 12:30:31 +00:00
|
|
|
Ok(HttpResponse::SeeOther()
|
2022-02-26 23:34:57 +00:00
|
|
|
.insert_header((LOCATION, redirect))
|
2022-10-07 13:52:12 +00:00
|
|
|
.body(format!("{url}\n")))
|
2021-04-04 12:30:31 +00:00
|
|
|
}
|
|
|
|
|
2021-09-11 00:08:47 +00:00
|
|
|
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_name = config.files_dir.clone();
|
|
|
|
file_name.push(&file_id);
|
|
|
|
match OpenOptions::new()
|
|
|
|
.write(true)
|
|
|
|
.create_new(true)
|
|
|
|
.open(&file_name)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
Ok(_) => return Ok((file_id, file_name)),
|
|
|
|
Err(error) if error.kind() == ErrorKind::AlreadyExists => continue,
|
|
|
|
Err(error) => return Err(error),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-04 12:30:31 +00:00
|
|
|
fn gen_file_id() -> String {
|
|
|
|
let mut rng = rand::thread_rng();
|
|
|
|
let mut id = String::with_capacity(5);
|
|
|
|
for _ in 0..5 {
|
|
|
|
id.push(*ID_CHARS.choose(&mut rng).expect("ID_CHARS is not empty"));
|
|
|
|
}
|
|
|
|
id
|
|
|
|
}
|
|
|
|
|
2022-02-26 23:34:57 +00:00
|
|
|
fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
|
2022-10-07 13:52:12 +00:00
|
|
|
let host = template::get_host_url(req);
|
2021-04-04 12:30:31 +00:00
|
|
|
if let Some(name) = name {
|
|
|
|
let encoded_name = urlencoding::encode(name);
|
2022-10-07 13:52:12 +00:00
|
|
|
format!("{host}/{id}/{encoded_name}")
|
2021-04-04 12:30:31 +00:00
|
|
|
} else {
|
2022-10-07 13:52:12 +00:00
|
|
|
format!("{host}/{id}")
|
2021-04-04 12:30:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-26 23:34:57 +00:00
|
|
|
pub async fn uploaded(req: HttpRequest) -> Result<HttpResponse, Error> {
|
2021-04-04 12:30:31 +00:00
|
|
|
let id = req.match_info().query("id");
|
|
|
|
let name = req.match_info().get("name");
|
2021-12-20 00:06:28 +00:00
|
|
|
let upload_html = if name.is_some() {
|
|
|
|
UPLOAD_SHORT_HTML
|
|
|
|
.replace("{link}", &get_file_url(&req, id, name))
|
|
|
|
.replace("{shortlink}", &get_file_url(&req, id, None))
|
|
|
|
} else {
|
|
|
|
UPLOAD_HTML.replace("{link}", &get_file_url(&req, id, name))
|
|
|
|
};
|
2021-04-04 12:30:31 +00:00
|
|
|
Ok(HttpResponse::Ok()
|
|
|
|
.content_type("text/html")
|
|
|
|
.body(upload_html))
|
|
|
|
}
|