Always use multipart for files

This commit is contained in:
Raphaël Thériault 2020-01-15 22:34:55 -05:00
parent d6bc31d8ca
commit f1c40f4748
6 changed files with 87 additions and 163 deletions

View File

@ -2,7 +2,6 @@ PORT=8080
DATABASE_URL=target/database.db DATABASE_URL=target/database.db
POOL_SIZE=4 POOL_SIZE=4
FILES_DIR=target/static/ FILES_DIR=target/static/
MAX_FILESIZE=50000000
PASSWD=a1b2c3d4 PASSWD=a1b2c3d4

View File

@ -55,8 +55,6 @@ database_url = "database.db"
pool_size = 4 pool_size = 4
# Path to the directory where files will be stored, relative or absolute # Path to the directory where files will be stored, relative or absolute
files_dir = "files" files_dir = "files"
# Max allowed size for file uploads, in bytes
max_filesize = 10000000
# Highlight.js configuration # Highlight.js configuration
[highlight] [highlight]

View File

@ -359,35 +359,30 @@
return; return;
} }
let fileReader = new FileReader(); const fd = new FormData();
fileReader.onload = () => { fd.append("file", file);
const id = urlInput.value; const id = urlInput.value;
const url = `${baseUrl}f/${id}`; const url = `${baseUrl}f/${id}`;
const base64 = btoa(fileReader.result); let status;
const filename = file.name; fetch(url, {
let status; method: "PUT",
fetch(url, { body: fd,
method: "PUT", })
headers: { "Content-Type": "application/json" }, .then((response) => {
body: JSON.stringify({ base64, filename }), status = response.status;
return response.text();
}) })
.then((response) => { .then((text) => {
status = response.status; if (status !== 201) {
return response.text(); throw new Error(text);
}) } else {
.then((text) => { openModal(url);
if (status !== 201) { clearInputs();
throw new Error(text); fetchUsed();
} else { }
openModal(url); })
clearInputs(); .catch((error) => alert(error));
fetchUsed();
}
})
.catch((error) => alert(error));
};
fileReader.readAsBinaryString(file);
}); });
} else if (group === "links") { } else if (group === "links") {
submitButton.addEventListener("click", () => { submitButton.addEventListener("click", () => {
@ -398,7 +393,6 @@
let status; let status;
fetch(url, { fetch(url, {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ forward }), body: JSON.stringify({ forward }),
}) })
.then((response) => { .then((response) => {
@ -426,7 +420,6 @@
let status; let status;
fetch(url, { fetch(url, {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ contents, highlight }), body: JSON.stringify({ contents, highlight }),
}) })
.then((response) => { .then((response) => {

View File

@ -9,7 +9,7 @@ extern crate serde;
extern crate diesel_migrations; extern crate diesel_migrations;
use actix_identity::{CookieIdentityPolicy, IdentityService}; use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{web, App, FromRequest, HttpServer}; use actix_web::{web, App, HttpServer};
use diesel::{ use diesel::{
r2d2::{self, ConnectionManager}, r2d2::{self, ConnectionManager},
sqlite::SqliteConnection, sqlite::SqliteConnection,
@ -53,7 +53,7 @@ async fn main() {
#[cfg(not(feature = "dev"))] #[cfg(not(feature = "dev"))]
{ {
embedded_migrations::run(&pool.get().unwrap()).unwrap_or_else(|e| { embedded_migrations::run(&pool.get().unwrap()).unwrap_or_else(|e| {
eprintln!("Can't prepare database: {}.", e); eprintln!("Can't prepare database: {}", e);
process::exit(1); process::exit(1);
}); });
} }
@ -76,8 +76,6 @@ async fn main() {
}; };
let port = config.port; let port = config.port;
let max_filesize_json = (config.max_filesize as f64 * 1.37) as usize;
println!("Listening on port {}", port); println!("Listening on port {}", port);
HttpServer::new(move || { HttpServer::new(move || {
@ -111,9 +109,6 @@ async fn main() {
) )
.service( .service(
web::resource("/f/{id}") web::resource("/f/{id}")
.data(web::Json::<routes::files::PutFile>::configure(|cfg| {
cfg.limit(max_filesize_json)
}))
.route(web::get().to(routes::files::get)) .route(web::get().to(routes::files::get))
.route(web::put().to(routes::files::put)) .route(web::put().to(routes::files::put))
.route(web::delete().to(routes::files::delete)), .route(web::delete().to(routes::files::delete)),
@ -133,13 +128,13 @@ async fn main() {
}) })
.bind(&format!("localhost:{}", port)) .bind(&format!("localhost:{}", port))
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
eprintln!("Can't bind webserver to specified port: {}.", e); eprintln!("Can't bind webserver to specified port: {}", e);
process::exit(1); process::exit(1);
}) })
.run() .run()
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
eprintln!("Can't start webserver: {}.", e); eprintln!("Can't start webserver: {}", e);
process::exit(1); process::exit(1);
}); });
} }

View File

@ -83,20 +83,8 @@ async fn auth(
} }
} }
/// Match result from REPLACE queries for PUT routes /// Match result from REPLACE queries
fn match_replace_result_put<T: Serialize>( fn match_replace_result<T: Serialize>(
result: Result<T, BlockingError<diesel::result::Error>>,
) -> Result<HttpResponse, Error> {
match result {
Ok(x) => Ok(HttpResponse::Created().json(x)),
Err(_) => Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into()),
}
}
/// Match result from REPLACE queries for POST routes
fn match_replace_result_post<T: Serialize>(
result: Result<T, BlockingError<diesel::result::Error>>, result: Result<T, BlockingError<diesel::result::Error>>,
id: i32, id: i32,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
@ -286,6 +274,7 @@ pub async fn logout(identity: Identity) -> impl Responder {
} }
pub mod files { pub mod files {
use crate::routes::match_replace_result;
use crate::{ use crate::{
queries::{self, SelectQuery}, queries::{self, SelectQuery},
routes::{auth, match_find_error, parse_id}, routes::{auth, match_find_error, parse_id},
@ -328,17 +317,15 @@ pub mod files {
} }
} }
/// Request body when PUTting files /// Common code for PUT and POST routes
#[derive(Deserialize)] async fn put_post(
pub struct PutFile { id: i32,
pub base64: String, mut body: Multipart,
pub filename: String, pool: web::Data<Pool>,
} config: web::Data<Config>,
) -> Result<HttpResponse, Error> {
/// Common setup for both PUT and POST let mut path = config.files_dir.clone();
async fn setup(config: &Config) -> Result<(PathBuf, PathBuf), Error> { let mut relative_path = PathBuf::new();
let path = config.files_dir.clone();
let relative_path = PathBuf::new();
let dir_path = path.clone(); let dir_path = path.clone();
if web::block(move || fs::create_dir_all(dir_path)) if web::block(move || fs::create_dir_all(dir_path))
.await .await
@ -349,78 +336,6 @@ pub mod files {
.into()); .into());
} }
Ok((path, relative_path))
}
/// Common conversion for both PUT and POST
fn pts(path: &PathBuf) -> Result<String, Error> {
match path.to_str() {
Some(rp) => Ok(rp.to_owned()),
None => Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into()),
}
}
/// PUT a new file entry
pub async fn put(
request: HttpRequest,
path: web::Path<String>,
body: web::Json<PutFile>,
pool: web::Data<Pool>,
config: web::Data<Config>,
identity: Identity,
password_hash: web::Data<Vec<u8>>,
) -> Result<HttpResponse, Error> {
auth(identity, request, &password_hash).await?;
let id = parse_id(&path)?;
let (mut path, mut relative_path) = setup(&config).await?;
let mut filename = body.filename.clone();
filename = format!("{:x}.{}", Utc::now().timestamp(), filename);
path.push(&filename);
relative_path.push(&filename);
let relative_path = pts(&relative_path)?;
let contents = match web::block(move || base64::decode(&body.base64)).await {
Ok(contents) => contents,
Err(_) => {
return Err(HttpResponse::BadRequest()
.body("Invalid base64 encoded file")
.into())
}
};
if web::block(move || fs::write(&path, contents))
.await
.is_err()
{
return Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into());
}
match web::block(move || queries::files::replace(id, &relative_path, pool)).await {
Ok(file) => Ok(HttpResponse::Created().json(file)),
Err(_) => Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into()),
}
}
/// POST a new file entry using a multipart body
pub async fn post(
request: HttpRequest,
mut body: Multipart,
pool: web::Data<Pool>,
config: web::Data<Config>,
identity: Identity,
password_hash: web::Data<Vec<u8>>,
) -> Result<HttpResponse, Error> {
auth(identity, request, &password_hash).await?;
let id = random_id(&pool).await?;
let (mut path, mut relative_path) = setup(&config).await?;
let mut field = match body.next().await { let mut field = match body.next().await {
Some(f) => f?, Some(f) => f?,
None => { None => {
@ -444,7 +359,14 @@ pub mod files {
let filename = format!("{:x}.{}", Utc::now().timestamp(), filename); let filename = format!("{:x}.{}", Utc::now().timestamp(), filename);
path.push(&filename); path.push(&filename);
relative_path.push(&filename); relative_path.push(&filename);
let relative_path = pts(&relative_path)?; let relative_path = match path.to_str() {
Some(rp) => rp.to_owned(),
None => {
return Err(HttpResponse::InternalServerError()
.body("Internal server error")
.into())
}
};
let mut f = match web::block(move || File::create(&path)).await { let mut f = match web::block(move || File::create(&path)).await {
Ok(f) => f, Ok(f) => f,
@ -479,12 +401,39 @@ pub mod files {
}; };
} }
match web::block(move || queries::files::replace(id, &relative_path, pool)).await { match_replace_result(
Ok(_) => Ok(HttpResponse::Created().body(format!("{}", radix_fmt::radix_36(id)))), web::block(move || queries::files::replace(id, &relative_path, pool)).await,
Err(_) => Err(HttpResponse::InternalServerError() id,
.body("Internal server error") )
.into()), }
}
/// PUT a new file entry
pub async fn put(
request: HttpRequest,
path: web::Path<String>,
body: Multipart,
pool: web::Data<Pool>,
config: web::Data<Config>,
identity: Identity,
password_hash: web::Data<Vec<u8>>,
) -> Result<HttpResponse, Error> {
auth(identity, request, &password_hash).await?;
let id = parse_id(&path)?;
put_post(id, body, pool, config).await
}
/// POST a new file entry using a multipart body
pub async fn post(
request: HttpRequest,
body: Multipart,
pool: web::Data<Pool>,
config: web::Data<Config>,
identity: Identity,
password_hash: web::Data<Vec<u8>>,
) -> Result<HttpResponse, Error> {
auth(identity, request, &password_hash).await?;
let id = random_id(&pool).await?;
put_post(id, body, pool, config).await
} }
} }
@ -492,8 +441,7 @@ pub mod links {
use crate::{ use crate::{
queries::{self, SelectQuery}, queries::{self, SelectQuery},
routes::{ routes::{
auth, match_find_error, match_replace_result_post, match_replace_result_put, parse_id, auth, match_find_error, match_replace_result, parse_id, timestamp_to_last_modified,
timestamp_to_last_modified,
}, },
Pool, Pool,
}; };
@ -535,10 +483,10 @@ pub mod links {
password_hash: web::Data<Vec<u8>>, password_hash: web::Data<Vec<u8>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
auth(identity, request, &password_hash).await?; auth(identity, request, &password_hash).await?;
let id = parse_id(&path)?; let id = parse_id(&path)?;
match_replace_result_put( match_replace_result(
web::block(move || queries::links::replace(id, &body.forward, pool)).await, web::block(move || queries::links::replace(id, &body.forward, pool)).await,
id,
) )
} }
@ -551,9 +499,8 @@ pub mod links {
password_hash: web::Data<Vec<u8>>, password_hash: web::Data<Vec<u8>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
auth(identity, request, &password_hash).await?; auth(identity, request, &password_hash).await?;
let id = random_id(&pool).await?; let id = random_id(&pool).await?;
match_replace_result_post( match_replace_result(
web::block(move || queries::links::replace(id, &body.forward, pool)).await, web::block(move || queries::links::replace(id, &body.forward, pool)).await,
id, id,
) )
@ -565,8 +512,7 @@ pub mod texts {
use crate::{ use crate::{
queries::{self, SelectQuery}, queries::{self, SelectQuery},
routes::{ routes::{
auth, match_find_error, match_replace_result_post, match_replace_result_put, parse_id, auth, match_find_error, match_replace_result, parse_id, timestamp_to_last_modified,
timestamp_to_last_modified,
}, },
Pool, Pool,
}; };
@ -636,11 +582,11 @@ pub mod texts {
password_hash: web::Data<Vec<u8>>, password_hash: web::Data<Vec<u8>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
auth(identity, request, &password_hash).await?; auth(identity, request, &password_hash).await?;
let id = parse_id(&path)?; let id = parse_id(&path)?;
match_replace_result_put( match_replace_result(
web::block(move || queries::texts::replace(id, &body.contents, body.highlight, pool)) web::block(move || queries::texts::replace(id, &body.contents, body.highlight, pool))
.await, .await,
id,
) )
} }
@ -653,9 +599,8 @@ pub mod texts {
password_hash: web::Data<Vec<u8>>, password_hash: web::Data<Vec<u8>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
auth(identity, request, &password_hash).await?; auth(identity, request, &password_hash).await?;
let id = random_id(&pool).await?; let id = random_id(&pool).await?;
match_replace_result_post( match_replace_result(
web::block(move || queries::texts::replace(id, &body.contents, body.highlight, pool)) web::block(move || queries::texts::replace(id, &body.contents, body.highlight, pool))
.await, .await,
id, id,

View File

@ -86,8 +86,6 @@ pub struct Config {
pub pool_size: u32, pub pool_size: u32,
/// Directory where to store static files /// Directory where to store static files
pub files_dir: PathBuf, pub files_dir: PathBuf,
/// Maximum allowed file size
pub max_filesize: usize,
/// Highlight.js configuration /// Highlight.js configuration
pub highlight: HighlightConfig, pub highlight: HighlightConfig,
} }
@ -113,14 +111,12 @@ impl Default for Config {
}; };
let pool_size = std::cmp::max(1, num_cpus::get() as u32 / 2); let pool_size = std::cmp::max(1, num_cpus::get() as u32 / 2);
let files_dir = get_data_dir().join("files"); let files_dir = get_data_dir().join("files");
let max_filesize = 10_000_000;
Self { Self {
port, port,
database_url, database_url,
pool_size, pool_size,
files_dir, files_dir,
max_filesize,
highlight: HighlightConfig::default(), highlight: HighlightConfig::default(),
} }
} }
@ -211,14 +207,12 @@ impl Config {
.expect("Invalid FILES_DIR") .expect("Invalid FILES_DIR")
} }
}; };
let max_filesize = parse_env!("MAX_FILESIZE");
Self { Self {
port, port,
database_url, database_url,
pool_size, pool_size,
files_dir, files_dir,
max_filesize,
highlight: HighlightConfig::default(), highlight: HighlightConfig::default(),
} }
} }