2020-07-09 17:27:24 +00:00
|
|
|
mod deleter;
|
|
|
|
mod file_kind;
|
2020-07-08 19:26:46 +00:00
|
|
|
mod multipart;
|
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
use actix_files::{Files, NamedFile};
|
2020-07-08 19:26:46 +00:00
|
|
|
use actix_multipart::Multipart;
|
2020-08-02 23:12:42 +00:00
|
|
|
use actix_web::{
|
|
|
|
error,
|
|
|
|
http::header::{ContentDisposition, DispositionParam, DispositionType},
|
|
|
|
middleware,
|
|
|
|
web::{self, Bytes},
|
|
|
|
App, Error, FromRequest, HttpRequest, HttpResponse, HttpServer,
|
|
|
|
};
|
2020-07-13 13:29:40 +00:00
|
|
|
use async_std::{
|
|
|
|
fs,
|
|
|
|
path::PathBuf,
|
|
|
|
sync::{channel, Sender},
|
|
|
|
task,
|
|
|
|
};
|
2020-07-09 17:27:24 +00:00
|
|
|
use file_kind::FileKind;
|
2020-07-12 00:26:11 +00:00
|
|
|
use sqlx::{
|
|
|
|
postgres::{PgPool, PgRow},
|
|
|
|
Cursor, Row,
|
|
|
|
};
|
2020-07-08 19:26:46 +00:00
|
|
|
use std::env;
|
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
const UPLOAD_HTML: &str = include_str!("../template/upload.html");
|
|
|
|
const VIEW_HTML: &str = include_str!("../template/view.html");
|
2020-07-08 19:26:46 +00:00
|
|
|
|
2020-07-14 11:45:14 +00:00
|
|
|
async fn index() -> Result<NamedFile, Error> {
|
2020-07-14 15:53:43 +00:00
|
|
|
Ok(NamedFile::open("./static/index.html")
|
2020-07-14 14:03:34 +00:00
|
|
|
.map_err(|_| error::ErrorNotFound(""))?
|
|
|
|
.disable_content_disposition())
|
2020-07-08 19:26:46 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
async fn upload(
|
|
|
|
payload: Multipart,
|
|
|
|
db: web::Data<PgPool>,
|
|
|
|
sender: web::Data<Sender<()>>,
|
2020-07-11 21:27:15 +00:00
|
|
|
config: web::Data<Config>,
|
2020-07-09 17:27:24 +00:00
|
|
|
) -> Result<HttpResponse, Error> {
|
|
|
|
let file_id = format!("{:x?}", rand::random::<u32>());
|
2020-07-11 21:27:15 +00:00
|
|
|
let mut filename = config.files_dir.clone();
|
|
|
|
filename.push(&file_id);
|
2020-07-09 17:27:24 +00:00
|
|
|
|
|
|
|
let (original_name, valid_till, kind) =
|
|
|
|
match multipart::parse_multipart(payload, &file_id, &filename).await {
|
|
|
|
Ok(data) => data,
|
|
|
|
Err(err) => {
|
|
|
|
if filename.exists().await {
|
|
|
|
fs::remove_file(filename)
|
|
|
|
.await
|
|
|
|
.map_err(|_| error::ErrorInternalServerError("could not remove file"))?;
|
2020-07-08 19:26:46 +00:00
|
|
|
}
|
2020-07-09 17:27:24 +00:00
|
|
|
return Err(err);
|
2020-07-08 19:26:46 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-07-12 00:26:11 +00:00
|
|
|
sqlx::query("INSERT INTO Files (file_id, file_name, valid_till, kind) VALUES ($1, $2, $3, $4)")
|
|
|
|
.bind(&file_id)
|
2020-08-03 00:42:27 +00:00
|
|
|
.bind(original_name.as_ref().unwrap_or_else(|| &file_id))
|
2020-07-12 00:26:11 +00:00
|
|
|
.bind(valid_till.naive_local())
|
|
|
|
.bind(kind.to_string())
|
|
|
|
.execute(db.as_ref())
|
|
|
|
.await
|
|
|
|
.map_err(|_| error::ErrorInternalServerError("could not insert file into database"))?;
|
2020-07-08 19:26:46 +00:00
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
log::info!(
|
|
|
|
"create new file {} (valid_till: {}, kind: {})",
|
|
|
|
file_id,
|
|
|
|
valid_till,
|
|
|
|
kind
|
|
|
|
);
|
2020-07-08 19:26:46 +00:00
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
sender.send(()).await;
|
2020-07-08 19:26:46 +00:00
|
|
|
|
2020-08-03 00:42:27 +00:00
|
|
|
let redirect = if kind == FileKind::BINARY && original_name.is_some() {
|
|
|
|
format!("/upload/{}/{}", file_id, original_name.unwrap())
|
|
|
|
} else {
|
|
|
|
format!("/upload/{}", file_id)
|
|
|
|
};
|
2020-07-13 14:08:55 +00:00
|
|
|
Ok(HttpResponse::SeeOther()
|
2020-08-03 00:42:27 +00:00
|
|
|
.header("location", redirect)
|
2020-07-08 19:26:46 +00:00
|
|
|
.finish())
|
|
|
|
}
|
|
|
|
|
2020-08-03 00:42:27 +00:00
|
|
|
async fn uploaded(req: web::HttpRequest) -> Result<HttpResponse, Error> {
|
|
|
|
let id = req.match_info().query("id");
|
|
|
|
let name = req.match_info().get("name");
|
|
|
|
let conn = req.connection_info();
|
|
|
|
let url = if let Some(name) = name {
|
|
|
|
format!("{}://{}/file/{}/{}", conn.scheme(), conn.host(), id, name)
|
|
|
|
} else {
|
|
|
|
format!("{}://{}/file/{}", conn.scheme(), conn.host(), id)
|
|
|
|
};
|
2020-08-02 23:12:42 +00:00
|
|
|
let upload_html = UPLOAD_HTML.replace("{url}", url.as_str());
|
2020-07-08 19:26:46 +00:00
|
|
|
Ok(HttpResponse::Ok()
|
|
|
|
.content_type("text/html")
|
|
|
|
.body(upload_html))
|
|
|
|
}
|
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
async fn download(
|
|
|
|
req: HttpRequest,
|
|
|
|
db: web::Data<PgPool>,
|
2020-07-11 21:27:15 +00:00
|
|
|
config: web::Data<Config>,
|
2020-07-09 17:27:24 +00:00
|
|
|
) -> Result<HttpResponse, Error> {
|
2020-08-03 00:42:27 +00:00
|
|
|
let id = req.match_info().query("id");
|
2020-07-12 00:26:11 +00:00
|
|
|
let mut cursor = sqlx::query("SELECT file_id, file_name, kind from files WHERE file_id = $1")
|
2020-08-03 00:42:27 +00:00
|
|
|
.bind(id)
|
2020-07-12 00:26:11 +00:00
|
|
|
.fetch(db.as_ref());
|
|
|
|
let row: PgRow = cursor
|
|
|
|
.next()
|
|
|
|
.await
|
|
|
|
.map_err(|_| error::ErrorInternalServerError("could not run select statement"))?
|
2020-07-13 13:22:33 +00:00
|
|
|
.ok_or_else(|| error::ErrorNotFound("file does not exist or has expired"))?;
|
|
|
|
|
2020-07-12 00:26:11 +00:00
|
|
|
let file_id: String = row.get("file_id");
|
|
|
|
let file_name: String = row.get("file_name");
|
|
|
|
let kind: String = row.get("kind");
|
2020-07-11 21:27:15 +00:00
|
|
|
let mut path = config.files_dir.clone();
|
2020-07-12 00:26:11 +00:00
|
|
|
path.push(&file_id);
|
2020-07-09 17:27:24 +00:00
|
|
|
|
2020-07-14 11:21:57 +00:00
|
|
|
if kind == FileKind::TEXT.to_string() && !req.query_string().contains("raw") {
|
2020-07-13 13:22:33 +00:00
|
|
|
let content = fs::read_to_string(path).await.map_err(|_| {
|
|
|
|
error::ErrorInternalServerError("this file should be here but could not be found")
|
|
|
|
})?;
|
|
|
|
let encoded = htmlescape::encode_minimal(&content);
|
|
|
|
let view_html = VIEW_HTML.replace("{text}", &encoded);
|
2020-07-09 17:27:24 +00:00
|
|
|
let response = HttpResponse::Ok().content_type("text/html").body(view_html);
|
|
|
|
Ok(response)
|
|
|
|
} else {
|
2020-07-13 13:22:33 +00:00
|
|
|
let file = NamedFile::open(path)
|
|
|
|
.map_err(|_| {
|
|
|
|
error::ErrorInternalServerError("this file should be here but could not be found")
|
|
|
|
})?
|
|
|
|
.set_content_disposition(ContentDisposition {
|
|
|
|
disposition: DispositionType::Attachment,
|
|
|
|
parameters: vec![DispositionParam::Filename(file_name)],
|
|
|
|
});
|
2020-07-09 17:27:24 +00:00
|
|
|
file.into_response(&req)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-19 14:24:42 +00:00
|
|
|
async fn not_found() -> Result<HttpResponse, Error> {
|
|
|
|
Ok(HttpResponse::NotFound()
|
|
|
|
.content_type("text/plain")
|
|
|
|
.body("not found"))
|
|
|
|
}
|
|
|
|
|
2020-08-02 01:08:07 +00:00
|
|
|
fn get_db_url() -> String {
|
2020-08-02 23:28:42 +00:00
|
|
|
if let Ok(database_url) = env::var("DATABASE_URL") {
|
|
|
|
return database_url;
|
|
|
|
}
|
2020-08-02 01:08:07 +00:00
|
|
|
|
2020-08-02 23:28:42 +00:00
|
|
|
let auth = if let Ok(user) = env::var("DATABASE_USER") {
|
|
|
|
if let Ok(pass) = env::var("DATABASE_PASS") {
|
|
|
|
format!("{}:{}@", user, pass)
|
|
|
|
} else {
|
|
|
|
format!("{}@", user)
|
2020-08-02 01:08:07 +00:00
|
|
|
}
|
2020-08-02 23:28:42 +00:00
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
};
|
|
|
|
|
|
|
|
format!(
|
2020-08-02 23:30:45 +00:00
|
|
|
"postgresql://{auth}{host}/{name}",
|
|
|
|
auth = auth,
|
|
|
|
host = env::var("DATABASE_HOST").unwrap_or_else(|_| "localhost".to_string()),
|
|
|
|
name = env::var("DATABASE_NAME").unwrap_or_else(|_| "datatrash".to_string())
|
2020-08-02 23:28:42 +00:00
|
|
|
)
|
2020-08-02 01:08:07 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
async fn setup_db() -> PgPool {
|
2020-08-02 01:08:07 +00:00
|
|
|
let conn_url = &get_db_url();
|
|
|
|
log::info!("Using Connection string {}", conn_url);
|
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
let pool = PgPool::builder()
|
|
|
|
.max_size(5)
|
2020-07-13 13:22:33 +00:00
|
|
|
.connect_timeout(std::time::Duration::from_secs(5))
|
2020-08-02 01:08:07 +00:00
|
|
|
.build(conn_url)
|
2020-07-09 17:27:24 +00:00
|
|
|
.await
|
|
|
|
.expect("could not create db pool");
|
|
|
|
|
2020-07-12 00:26:11 +00:00
|
|
|
sqlx::query(include_str!("../init-db.sql"))
|
2020-07-09 20:01:25 +00:00
|
|
|
.execute(&pool)
|
|
|
|
.await
|
|
|
|
.expect("could not create table Files");
|
2020-07-09 17:27:24 +00:00
|
|
|
|
|
|
|
pool
|
|
|
|
}
|
|
|
|
|
2020-07-11 21:27:15 +00:00
|
|
|
#[derive(Clone)]
|
|
|
|
struct Config {
|
|
|
|
files_dir: PathBuf,
|
|
|
|
}
|
|
|
|
|
2020-07-08 19:26:46 +00:00
|
|
|
#[actix_rt::main]
|
|
|
|
async fn main() -> std::io::Result<()> {
|
2020-07-14 15:53:43 +00:00
|
|
|
if env::var("RUST_LOG").is_err() {
|
|
|
|
env::set_var("RUST_LOG", "info");
|
|
|
|
}
|
2020-07-08 19:26:46 +00:00
|
|
|
env_logger::init();
|
|
|
|
|
2020-07-09 17:27:24 +00:00
|
|
|
let pool: PgPool = setup_db().await;
|
2020-07-11 21:27:15 +00:00
|
|
|
let config = Config {
|
|
|
|
files_dir: PathBuf::from(env::var("FILES_DIR").unwrap_or_else(|_| "./files".to_owned())),
|
|
|
|
};
|
2020-07-13 13:22:33 +00:00
|
|
|
fs::create_dir_all(&config.files_dir)
|
|
|
|
.await
|
|
|
|
.expect("could not create directory for storing files");
|
2020-07-14 15:53:43 +00:00
|
|
|
let (sender, receiver) = channel(8);
|
2020-07-13 13:22:33 +00:00
|
|
|
|
2020-07-13 13:29:40 +00:00
|
|
|
log::info!("omnomnom");
|
|
|
|
|
2020-07-11 21:27:15 +00:00
|
|
|
task::spawn(deleter::delete_old_files(
|
2020-07-14 15:53:43 +00:00
|
|
|
receiver,
|
2020-07-11 21:27:15 +00:00
|
|
|
pool.clone(),
|
|
|
|
config.files_dir.clone(),
|
|
|
|
));
|
2020-07-09 17:27:24 +00:00
|
|
|
|
2020-07-08 19:26:46 +00:00
|
|
|
let db = web::Data::new(pool);
|
2020-07-14 15:53:43 +00:00
|
|
|
let sender = web::Data::new(sender);
|
|
|
|
let upload_max_bytes: usize = env::var("UPLOAD_MAX_BYTES")
|
2020-07-11 21:27:15 +00:00
|
|
|
.ok()
|
|
|
|
.and_then(|variable| variable.parse().ok())
|
2020-07-24 17:56:36 +00:00
|
|
|
.unwrap_or(8 * 1024 * 1024);
|
2020-07-11 21:27:15 +00:00
|
|
|
let bind_address = env::var("BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0:8000".to_owned());
|
|
|
|
|
|
|
|
HttpServer::new({
|
|
|
|
move || {
|
|
|
|
App::new()
|
|
|
|
.wrap(middleware::Logger::default())
|
|
|
|
.app_data(db.clone())
|
2020-07-14 15:53:43 +00:00
|
|
|
.app_data(sender.clone())
|
|
|
|
.app_data(Bytes::configure(|cfg| cfg.limit(upload_max_bytes)))
|
2020-07-11 21:27:15 +00:00
|
|
|
.data(config.clone())
|
|
|
|
.service(web::resource("/").route(web::get().to(index)))
|
|
|
|
.service(web::resource("/upload").route(web::post().to(upload)))
|
2020-08-02 23:12:42 +00:00
|
|
|
.service(
|
2020-08-03 00:42:27 +00:00
|
|
|
web::resource(["/upload/{id}", "/upload/{id}/{name}"])
|
|
|
|
.route(web::get().to(uploaded)),
|
|
|
|
)
|
|
|
|
.service(
|
|
|
|
web::resource(["/file/{id}", "/file/{id}/{name}"])
|
2020-08-02 23:12:42 +00:00
|
|
|
.route(web::get().to(download)),
|
|
|
|
)
|
2020-07-11 21:27:15 +00:00
|
|
|
.service(Files::new("/static", "static").disable_content_disposition())
|
2020-08-19 14:24:42 +00:00
|
|
|
.default_service(web::route().to(not_found))
|
2020-07-11 21:27:15 +00:00
|
|
|
}
|
2020-07-08 19:26:46 +00:00
|
|
|
})
|
2020-07-11 21:27:15 +00:00
|
|
|
.bind(bind_address)?
|
2020-07-08 19:26:46 +00:00
|
|
|
.run()
|
|
|
|
.await
|
|
|
|
}
|