From f0714b2945302fc194345aa727cd8eaf321af002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Th=C3=A9riault?= Date: Wed, 15 Jan 2020 03:26:29 -0500 Subject: [PATCH] Start working on POST routes --- Cargo.lock | 37 ++++++++++ Cargo.toml | 2 + src/main.rs | 6 +- src/routes.rs | 201 ++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 202 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d86d3f4..cae5d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,24 @@ dependencies = [ "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "actix-multipart" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "actix-service 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-utils 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "derive_more 0.99.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "twoway 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "actix-router" version = "0.2.4" @@ -675,6 +693,7 @@ version = "0.2.0" dependencies = [ "actix-files 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "actix-identity 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-multipart 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -685,6 +704,7 @@ dependencies = [ "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libsqlite3-sys 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1701,11 +1721,25 @@ dependencies = [ "trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "twoway" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unchecked-index 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "typenum" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicase" version = "2.5.1" @@ -1965,6 +1999,7 @@ dependencies = [ "checksum actix-http 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019" "checksum actix-identity 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a379b0639c293292d71defb8cc1f94c87b7705c904adf044338ad392df77c7a" "checksum actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "21705adc76bbe4bc98434890e73a89cd00c6015e5704a60bb6eea6c3b72316b6" +"checksum actix-multipart 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4397935fca2a37a5353f94faa758fb176712806f605466b5a60373b204f0d836" "checksum actix-router 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9d7a10ca4d94e8c8e7a87c5173aba1b97ba9a6563ca02b0e1cd23531093d3ec8" "checksum actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6a0a55507046441a496b2f0d26a84a65e67c8cafffe279072412f624b5fb6d" "checksum actix-server 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "51d3455eaac03ca3e49d7b822eb35c884b861f715627254ccbe4309d08f1841a" @@ -2143,7 +2178,9 @@ dependencies = [ "checksum toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "01d1404644c8b12b16bfcffa4322403a91a451584daaaa7c28d3152e6cbc98cf" "checksum trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f" "checksum trust-dns-resolver 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f" +"checksum twoway 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" "checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" +"checksum unchecked-index 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" "checksum unicase 2.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2e2e6bd1e59e56598518beb94fd6db628ded570326f0a98c679a304bd9f00150" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426" diff --git a/Cargo.toml b/Cargo.toml index c259ab8..e6aab86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ license = "MIT" actix-files = "0.2.1" actix-identity = "0.2.1" actix-rt = "1.0.0" +actix-multipart = "0.2.0" actix-web = "2.0.0" base64 = "0.11.0" blake2 = "0.8.1" @@ -26,6 +27,7 @@ diesel_migrations = "1.4.0" dirs = "2.0.2" dotenv = { version = "0.15.0", optional = true } env_logger = "0.7.1" +futures = "0.3.1" lazy_static = "1.4.0" num_cpus = "1.11.1" toml = "0.5.5" diff --git a/src/main.rs b/src/main.rs index d4a220a..3033a1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,9 +94,9 @@ async fn main() { .route("/", web::get().to(routes::index)) .route("/logout", web::get().to(routes::logout)) .route("/config", web::get().to(routes::get_config)) - .route("/f", web::get().to(routes::files::gets)) - .route("/l", web::get().to(routes::links::gets)) - .route("/t", web::get().to(routes::texts::gets)) + .route("/f", web::get().to(routes::files::select)) + .route("/l", web::get().to(routes::links::select)) + .route("/t", web::get().to(routes::texts::select)) .route("/f/{id}", web::get().to(routes::files::get)) .route("/l/{id}", web::get().to(routes::links::get)) .route("/t/{id}", web::get().to(routes::texts::get)) diff --git a/src/routes.rs b/src/routes.rs index 2578990..8382b50 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -22,6 +22,7 @@ fn parse_id(id: &str) -> Result { } } +/// Authenticates a user async fn auth( identity: Identity, request: HttpRequest, @@ -126,7 +127,7 @@ fn escape_html(text: &str) -> String { /// GET multiple entries macro_rules! select { ($m:ident) => { - pub async fn gets( + pub async fn select( request: HttpRequest, query: actix_web::web::Query, pool: actix_web::web::Data, @@ -167,6 +168,18 @@ macro_rules! delete { }; } +/// Verify if an entry exists +macro_rules! exists { + ($m:ident) => { + pub async fn exists(id: i32, pool: actix_web::web::Data) -> bool { + match actix_web::web::block(move || crate::queries::$m::find(id, pool)).await { + Ok(_) => true, + Err(_) => false, + } + } + }; +} + #[cfg(feature = "dev")] lazy_static! { static ref RESOURCES_DIR: PathBuf = { @@ -270,9 +283,15 @@ pub mod files { }; use actix_files::NamedFile; use actix_identity::Identity; - use actix_web::{error::BlockingError, http, web, Error, HttpRequest, HttpResponse}; + use actix_multipart::Multipart; + use actix_web::{web, Error, HttpRequest, HttpResponse}; use chrono::Utc; - use std::{fs, path::PathBuf}; + use futures::StreamExt; + use std::{ + fs::{self, File}, + io::Write, + path::PathBuf, + }; select!(files); @@ -303,6 +322,45 @@ pub mod files { pub filename: String, } + /// Common setup for both routes + async fn setup(config: &Config) -> Result<(PathBuf, PathBuf), Error> { + let path = config.files_dir.clone(); + let relative_path = PathBuf::new(); + let dir_path = path.clone(); + if web::block(move || fs::create_dir_all(dir_path)) + .await + .is_err() + { + return Err(HttpResponse::InternalServerError() + .body("Internal server error") + .into()); + } + + Ok((path, relative_path)) + } + /// Common conversion for both routes + fn pts(path: &PathBuf) -> Result { + match path.to_str() { + Some(rp) => Ok(rp.to_owned()), + None => Err(HttpResponse::InternalServerError() + .body("Internal server error") + .into()), + } + } + /// Common database query for both routes + async fn query( + id: i32, + relative_path: String, + pool: web::Data, + ) -> Result { + 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()), + } + } + /// PUT a new file entry pub async fn put( request: HttpRequest, @@ -316,46 +374,107 @@ pub mod files { auth(identity, request, &password_hash).await?; let id = parse_id(&path)?; - let result = web::block(move || { - let mut path = config.files_dir.clone(); - let mut relative_path = PathBuf::new(); - if fs::create_dir_all(&path).is_err() { - return Err(http::StatusCode::from_u16(500).unwrap()); + 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()) } - - let mut filename = body.filename.clone(); - filename = format!("{:x}.{}", Utc::now().timestamp(), filename); - path.push(&filename); - relative_path.push(&filename); - - let relative_path = match relative_path.to_str() { - Some(rp) => rp, - None => return Err(http::StatusCode::from_u16(500).unwrap()), - }; - - let contents = match base64::decode(&body.base64) { - Ok(contents) => contents, - Err(_) => return Err(http::StatusCode::from_u16(400).unwrap()), - }; - if fs::write(&path, contents).is_err() { - return Err(http::StatusCode::from_u16(500).unwrap()); - } - - match queries::files::replace(id, relative_path, pool) { - Ok(file) => Ok(file), - Err(_) => Err(http::StatusCode::from_u16(500).unwrap()), - } - }) - .await; - match result { - Ok(file) => Ok(HttpResponse::Created().json(file)), - Err(e) => match e { - BlockingError::Error(sc) => Err(HttpResponse::new(sc).into()), - BlockingError::Canceled => Err(HttpResponse::InternalServerError() - .body("Internal server error") - .into()), - }, + }; + if web::block(move || fs::write(&path, contents)) + .await + .is_err() + { + return Err(HttpResponse::InternalServerError() + .body("Internal server error") + .into()); } + + query(id, relative_path, pool).await + } + + /// POST a new file entry using a multipart body + pub async fn post( + request: HttpRequest, + mut body: Multipart, + pool: web::Data, + config: web::Data, + identity: Identity, + password_hash: web::Data>, + ) -> Result { + auth(identity, request, &password_hash).await?; + + let id = parse_id(&path)?; + let (mut path, mut relative_path) = setup(&config).await?; + + let mut field = match body.next().await { + Some(f) => f?, + None => { + return Err(HttpResponse::BadRequest() + .body("Empty multipart body") + .into()) + } + }; + let content_disposition = match field.content_disposition() { + Some(cd) => cd, + None => { + return Err(HttpResponse::BadRequest() + .body("Missing content disposition") + .into()) + } + }; + let filename = match content_disposition.get_filename() { + Some(n) => n, + None => return Err(HttpResponse::BadRequest().body("Missing filename").into()), + }; + let filename = format!("{:x}.{}", Utc::now().timestamp(), filename); + path.push(&filename); + relative_path.push(&filename); + let relative_path = pts(&relative_path)?; + + let mut f = match web::block(move || File::create(&path)).await { + Ok(f) => f, + Err(_) => { + return Err(HttpResponse::InternalServerError() + .body("Internal server error") + .into()) + } + }; + while let Some(chunk) = field.next().await { + let data = match chunk { + Ok(c) => c, + Err(_) => { + return Err(HttpResponse::BadRequest() + .body("Invalid multipart data") + .into()) + } + }; + + f = match web::block(move || match f.write_all(&data) { + Ok(_) => Ok(f), + Err(_) => Err(()), + }) + .await + { + Ok(f) => f, + Err(_) => { + return Err(HttpResponse::InternalServerError() + .body("Internal server error") + .into()) + } + }; + } + + query(id, relative_path, pool).await } delete!(files);