2021-08-18 21:22:50 +00:00
|
|
|
use std::{cmp, vec};
|
|
|
|
|
2021-04-04 12:30:31 +00:00
|
|
|
use crate::config::Config;
|
|
|
|
use crate::file_kind::FileKind;
|
|
|
|
use crate::multipart;
|
|
|
|
use crate::multipart::UploadConfig;
|
|
|
|
use actix_multipart::Multipart;
|
|
|
|
use actix_web::{error, web, Error, HttpResponse};
|
|
|
|
use async_std::{channel::Sender, fs};
|
2021-08-18 21:22:50 +00:00
|
|
|
use chrono::Duration;
|
2021-04-04 12:30:31 +00:00
|
|
|
use rand::prelude::SliceRandom;
|
|
|
|
use sqlx::postgres::PgPool;
|
|
|
|
|
|
|
|
const INDEX_HTML: &str = include_str!("../template/index.html");
|
2021-04-07 22:03:02 +00:00
|
|
|
const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js");
|
2021-04-04 12:30:31 +00:00
|
|
|
const UPLOAD_HTML: &str = include_str!("../template/upload.html");
|
2021-08-18 21:22:50 +00:00
|
|
|
const AUTH_SNIPPET_HTML: &str = include_str!("../snippet/auth.html.snippet");
|
|
|
|
const MAX_SIZE_SNIPPET_HTML: &str = include_str!("../snippet/max_size.html.snippet");
|
2021-04-04 12:30:31 +00:00
|
|
|
|
|
|
|
const ID_CHARS: &[char] = &[
|
|
|
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', '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-07 22:03:02 +00:00
|
|
|
pub async fn index(
|
|
|
|
req: web::HttpRequest,
|
|
|
|
config: web::Data<Config>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
2021-08-18 21:22:50 +00:00
|
|
|
let filled_index_html = fill_index_html(req, config);
|
2021-04-04 12:30:31 +00:00
|
|
|
Ok(HttpResponse::Ok()
|
|
|
|
.content_type("text/html")
|
2021-04-07 22:03:02 +00:00
|
|
|
.body(filled_index_html))
|
|
|
|
}
|
|
|
|
|
2021-08-18 21:22:50 +00:00
|
|
|
fn fill_index_html(req: web::HttpRequest, config: web::Data<Config>) -> String {
|
|
|
|
let upload_url = format!("{}/upload", get_host_url(&req));
|
|
|
|
let auth_snippet = config
|
|
|
|
.no_auth_limits
|
|
|
|
.as_ref()
|
|
|
|
.map_or("", |_| AUTH_SNIPPET_HTML);
|
|
|
|
let max_size_snippet = config
|
|
|
|
.max_file_size
|
|
|
|
.as_ref()
|
|
|
|
.map_or("", |_| MAX_SIZE_SNIPPET_HTML);
|
|
|
|
INDEX_HTML
|
|
|
|
.replace("{max_size_snippet}", max_size_snippet)
|
|
|
|
.replace(
|
|
|
|
"{max_size}",
|
|
|
|
&render_file_size(config.max_file_size.unwrap_or(0)),
|
|
|
|
)
|
|
|
|
.replace("{auth_snippet}", auth_snippet)
|
|
|
|
.replace(
|
|
|
|
"{auth_time}",
|
|
|
|
&config
|
|
|
|
.no_auth_limits
|
|
|
|
.as_ref()
|
|
|
|
.map(|limit| limit.max_time)
|
|
|
|
.map_or("".into(), render_duration),
|
|
|
|
)
|
|
|
|
.replace(
|
|
|
|
"{auth_large_time}",
|
|
|
|
&config
|
|
|
|
.no_auth_limits
|
|
|
|
.as_ref()
|
|
|
|
.map(|limit| limit.large_file_max_time)
|
|
|
|
.map_or("".into(), render_duration),
|
|
|
|
)
|
|
|
|
.replace(
|
|
|
|
"{auth_large_size}",
|
|
|
|
&config
|
|
|
|
.no_auth_limits
|
|
|
|
.as_ref()
|
|
|
|
.map(|limit| limit.large_file_size)
|
|
|
|
.map_or("".into(), render_file_size),
|
|
|
|
)
|
|
|
|
.replace("{upload_url}", upload_url.as_str())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render_file_size(size: u64) -> String {
|
|
|
|
let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5);
|
|
|
|
let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize];
|
|
|
|
let value = size / (1024_u64.pow(magnitude));
|
|
|
|
format!("{}{}B", value, prefix)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render_duration(duration: Duration) -> String {
|
|
|
|
let days = duration.num_days();
|
|
|
|
let hours = duration.num_hours() % 24;
|
|
|
|
let minutes = duration.num_minutes() % 60;
|
|
|
|
let seconds = duration.num_seconds() % 60;
|
|
|
|
let mut elements = vec![];
|
|
|
|
if let Some(name) = pluralize(days, "tag", "e") {
|
|
|
|
elements.push(name);
|
|
|
|
}
|
|
|
|
if let Some(name) = pluralize(hours, "stunde", "n") {
|
|
|
|
elements.push(name);
|
|
|
|
}
|
|
|
|
if let Some(name) = pluralize(minutes, "minute", "n") {
|
|
|
|
elements.push(name);
|
|
|
|
}
|
|
|
|
if let Some(name) = pluralize(seconds, "sekunde", "n") {
|
|
|
|
elements.push(name);
|
|
|
|
}
|
|
|
|
elements.join("+")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn pluralize(number: i64, word: &str, suffix: &str) -> Option<String> {
|
|
|
|
match number {
|
|
|
|
0 => None,
|
|
|
|
1 => Some(format!("{} {}", number, word)),
|
|
|
|
_ => Some(format!("{} {}{}", number, word, suffix)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-07 22:03:02 +00:00
|
|
|
pub async fn auth_hide(config: web::Data<Config>) -> Result<HttpResponse, Error> {
|
|
|
|
if let Some(no_auth_limits) = &config.no_auth_limits {
|
|
|
|
let auth_hide_js = AUTH_HIDE_JS
|
2021-08-18 21:22:50 +00:00
|
|
|
.replace(
|
|
|
|
"{no_auth_max_time}",
|
|
|
|
&no_auth_limits.max_time.num_seconds().to_string(),
|
|
|
|
)
|
2021-04-07 22:03:02 +00:00
|
|
|
.replace(
|
|
|
|
"{no_auth_large_file_max_time}",
|
2021-08-18 21:22:50 +00:00
|
|
|
&no_auth_limits.large_file_max_time.num_seconds().to_string(),
|
2021-04-07 22:03:02 +00:00
|
|
|
)
|
|
|
|
.replace(
|
|
|
|
"{no_auth_large_file_size}",
|
|
|
|
&no_auth_limits.large_file_size.to_string(),
|
|
|
|
);
|
|
|
|
Ok(HttpResponse::Ok()
|
|
|
|
.content_type("application/javascript")
|
|
|
|
.body(auth_hide_js))
|
|
|
|
} else {
|
|
|
|
Err(error::ErrorNotFound("file not found"))
|
|
|
|
}
|
2021-04-04 12:30:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn upload(
|
|
|
|
req: web::HttpRequest,
|
|
|
|
payload: Multipart,
|
|
|
|
db: web::Data<PgPool>,
|
|
|
|
expiry_watch_sender: web::Data<Sender<()>>,
|
|
|
|
config: web::Data<Config>,
|
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
let file_id = gen_file_id();
|
|
|
|
let mut filename = config.files_dir.clone();
|
|
|
|
filename.push(&file_id);
|
|
|
|
|
2021-04-07 22:03:02 +00:00
|
|
|
let parsed_multipart = multipart::parse_multipart(payload, &file_id, &filename, &config).await;
|
2021-04-04 12:30:31 +00:00
|
|
|
let UploadConfig {
|
|
|
|
original_name,
|
|
|
|
valid_till,
|
|
|
|
kind,
|
|
|
|
delete_on_download,
|
|
|
|
} = match parsed_multipart {
|
|
|
|
Ok(data) => data,
|
|
|
|
Err(err) => {
|
|
|
|
if filename.exists().await {
|
2021-04-07 22:33:22 +00:00
|
|
|
fs::remove_file(filename).await.map_err(|file_err| {
|
|
|
|
log::error!("could not remove file {:?}", file_err);
|
2021-04-04 12:30:31 +00:00
|
|
|
error::ErrorInternalServerError(
|
|
|
|
"could not parse multipart; could not remove file",
|
|
|
|
)
|
|
|
|
})?;
|
|
|
|
}
|
|
|
|
return Err(err);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let db_insert = sqlx::query(
|
|
|
|
"INSERT INTO Files (file_id, file_name, valid_till, kind, delete_on_download) \
|
|
|
|
VALUES ($1, $2, $3, $4, $5)",
|
|
|
|
)
|
|
|
|
.bind(&file_id)
|
|
|
|
.bind(&original_name)
|
|
|
|
.bind(valid_till.naive_local())
|
|
|
|
.bind(kind.to_string())
|
|
|
|
.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);
|
|
|
|
fs::remove_file(filename).await.map_err(|file_err| {
|
|
|
|
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!(
|
|
|
|
"{} create new file {} (valid_till: {}, kind: {}, delete_on_download: {})",
|
|
|
|
req.connection_info().realip_remote_addr().unwrap_or("-"),
|
|
|
|
file_id,
|
|
|
|
valid_till,
|
|
|
|
kind,
|
|
|
|
delete_on_download
|
|
|
|
);
|
|
|
|
|
|
|
|
expiry_watch_sender.send(()).await.unwrap();
|
|
|
|
|
|
|
|
let redirect = if kind == FileKind::Binary {
|
|
|
|
let encoded_name = urlencoding::encode(&original_name);
|
|
|
|
format!("/upload/{}/{}", file_id, encoded_name)
|
|
|
|
} else {
|
|
|
|
format!("/upload/{}", file_id)
|
|
|
|
};
|
|
|
|
|
|
|
|
let url = get_file_url(&req, &file_id, Some(&original_name));
|
|
|
|
Ok(HttpResponse::SeeOther()
|
|
|
|
.header("location", redirect)
|
|
|
|
.body(format!("{}\n", url)))
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_host_url(req: &web::HttpRequest) -> String {
|
|
|
|
let conn = req.connection_info();
|
|
|
|
format!("{}://{}", conn.scheme(), conn.host())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_file_url(req: &web::HttpRequest, id: &str, name: Option<&str>) -> String {
|
|
|
|
if let Some(name) = name {
|
|
|
|
let encoded_name = urlencoding::encode(name);
|
|
|
|
format!("{}/{}/{}", get_host_url(req), id, encoded_name)
|
|
|
|
} else {
|
|
|
|
format!("{}/{}", get_host_url(req), id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn uploaded(req: web::HttpRequest) -> Result<HttpResponse, Error> {
|
|
|
|
let id = req.match_info().query("id");
|
|
|
|
let name = req.match_info().get("name");
|
|
|
|
let url = get_file_url(&req, id, name);
|
|
|
|
let upload_html = UPLOAD_HTML.replace("{url}", url.as_str());
|
|
|
|
Ok(HttpResponse::Ok()
|
|
|
|
.content_type("text/html")
|
|
|
|
.body(upload_html))
|
|
|
|
}
|