A website for temporary file- or text hosting
https://trash.ctdo.de/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
5.5 KiB
167 lines
5.5 KiB
use std::io::ErrorKind; |
|
|
|
use crate::config::Config; |
|
use crate::file_kind::FileKind; |
|
use crate::multipart::UploadConfig; |
|
use crate::{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::prelude::SliceRandom; |
|
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', '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_name) = create_unique_file(&config).await.map_err(|file_err| { |
|
log::error!("could not create file {:?}", file_err); |
|
error::ErrorInternalServerError("could not create file") |
|
})?; |
|
|
|
let parsed_multipart = multipart::parse_multipart(payload, &file_id, &file_name, &config).await; |
|
let UploadConfig { |
|
original_name, |
|
valid_till, |
|
kind, |
|
delete_on_download, |
|
} = match parsed_multipart { |
|
Ok(data) => data, |
|
Err(err) => { |
|
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( |
|
"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) |
|
.bind(kind.to_string()) |
|
.bind(delete_on_download) |
|
.execute(db.as_ref()) |
|
.await; |
|
if let Err(db_err) = db_insert { |
|
log::error!("could not insert into datebase {:?}", db_err); |
|
fs::remove_file(file_name).await.map_err(|file_err| { |
|
log::error!("could not remove file {:?}", file_err); |
|
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() |
|
.insert_header((LOCATION, redirect)) |
|
.body(format!("{}\n", url))) |
|
} |
|
|
|
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), |
|
} |
|
} |
|
} |
|
|
|
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_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String { |
|
if let Some(name) = name { |
|
let encoded_name = urlencoding::encode(name); |
|
format!("{}/{}/{}", template::get_host_url(req), id, encoded_name) |
|
} else { |
|
format!("{}/{}", template::get_host_url(req), id) |
|
} |
|
} |
|
|
|
pub async fn uploaded(req: HttpRequest) -> Result<HttpResponse, Error> { |
|
let id = req.match_info().query("id"); |
|
let name = req.match_info().get("name"); |
|
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)) |
|
}; |
|
Ok(HttpResponse::Ok() |
|
.content_type("text/html") |
|
.body(upload_html)) |
|
}
|
|
|