diff --git a/.gitignore b/.gitignore index ff9669290916918e439e269fccf1ec62b5621761..7bbcb76c9fd479989fa4eb73adad0cacbd9156d4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ target public/**/*.js public/**/view/* public/standard/test_sitemap.xml -public/standard/dyn_sitemap.xml \ No newline at end of file +public/standard/dyn_sitemap.xml +public/uploads \ No newline at end of file diff --git a/admin-frontend/src/components/root.js b/admin-frontend/src/components/root.js index 449b476ba543595827774d8b0bdc27a92e09ae73..d07a98cc3abcadebe21d05c0df51fc38d3a000d9 100644 --- a/admin-frontend/src/components/root.js +++ b/admin-frontend/src/components/root.js @@ -40,7 +40,7 @@ class RootComponent { fetch_post_file(e.target).then(res => console.log(res)).catch(err => console.log(err)); }, contents: [ - { tag: "input", name: "file", type: "file" }, + { tag: "input", name: "file", type: "file", multiple: true }, { tag: "input", type: "submit" } ] }, diff --git a/admin-frontend/src/xhr.js b/admin-frontend/src/xhr.js index 870dd8e61b89ca93cd7f6ec5c999c8dc730707cd..4255cff70527fd3c46e43c5178131b5cec67b38e 100644 --- a/admin-frontend/src/xhr.js +++ b/admin-frontend/src/xhr.js @@ -106,15 +106,14 @@ function fetch_all_articles() { function fetch_post_file(form) { return new Promise((resolve, reject) => { - fetch("/post-file", { + fetch("/post-files", { method: "POST", body: new FormData(form), }).then(async res => { - const res_text = await res.text(); if (res.status >= 400 && res.status < 600) { - reject(res_text) + reject((await res.text())) } else { - resolve(res_text); + resolve((await res.json())); } }) }) diff --git a/src/main.rs b/src/main.rs index cdeaa0f2958b810168728e124c8dd480c4f73f68..99b3a570b5c2685473c11391989f448d02c1392d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,7 +87,7 @@ async fn main() -> std::io::Result<()> { .service(get_articles_by_category) .service(get_article) .service(get_all_articles) - .service(post_file) + .service(post_files) ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // STANDARD FILES /////////////////////////////////////////////////////////////////////////////////////////// .service(resource("/favicon.ico").route(get().to(favicon))) diff --git a/src/service/static_files.rs b/src/service/static_files.rs index 1842be788cba58122e5e2843e5691640cb7de877..a179b2b603e4183d53dc59e2601c1e652ea769bf 100644 --- a/src/service/static_files.rs +++ b/src/service/static_files.rs @@ -1,20 +1,15 @@ use crate::{middleware::AuthenticatedAdminMiddleware, AppState}; use actix_multipart::Multipart; -use actix_web::{ - post, - web::{block, Data}, - HttpRequest, HttpResponse, Responder, -}; +use actix_web::{post, web::Data, HttpRequest, HttpResponse, Responder}; use futures::StreamExt; use std::{ - fs::{remove_file, File}, + fs::{create_dir_all, remove_file, File}, io::Write, - path::Path, - sync::Arc, }; -#[post("/post-file")] -pub async fn post_file( +// TODO separate file writing logic from Multipart implementation and implement the unit tests +#[post("/post-files")] +pub async fn post_files( app_state: Data<AppState>, mut payload: Multipart, middleware: Data<AuthenticatedAdminMiddleware<'_>>, @@ -24,6 +19,14 @@ pub async fn post_file( return HttpResponse::Unauthorized().finish(); } + let uploads_dir = app_state.env.public_dir.join("uploads"); + + if !uploads_dir.exists() { + create_dir_all(&uploads_dir).unwrap(); + } + + let mut uploaded_filepathes = Vec::new(); + while let Some(item) = payload.next().await { match item { Ok(mut field) => { @@ -33,43 +36,44 @@ pub async fn post_file( .content_disposition() .expect("Missing Content Disposition header"); - let filename = content_disposition.get_filename().expect("Missin filename"); + let filename = content_disposition + .get_filename() + .expect("Missing filename"); - let filepath = Arc::new(Path::new(format!("./tmp/{filename}"))); + let filepath = uploads_dir.join(&filename); - // File::create is blocking operation, use threadpool - let mut f = block(|| File::create(*filepath.clone())).await; + let f = File::create(&filepath); if f.is_err() { return HttpResponse::InternalServerError() .body(format!("Error creating file {:?}", f)); } - let f = f.unwrap(); - + let mut f = f.unwrap(); let mut error = None; // Field in turn is stream of *Bytes* object 'chunks: while let Some(chunk) = field.next().await { match chunk { Ok(chunk) => { - // filesystem operations are blocking, we have to use threadpool - if block(move || f.write_all(&chunk).map(|_| f)).await.is_err() { + if f.write_all(&chunk).is_err() { error = Some("Error writing chunk".to_string()); break 'chunks; } } Err(e) => { - error = format!("Error writing file {} : {:?}", filename, e); + error = Some(format!("Error writing file {} : {:?}", filename, e)); break 'chunks; } } } if let Some(err) = error { - block(|| remove_file(*filepath.clone())).await.unwrap(); + remove_file(&filepath).unwrap(); return HttpResponse::InternalServerError().body(err); } + + uploaded_filepathes.push(String::from(filepath.to_string_lossy())); } Err(e) => { return HttpResponse::InternalServerError().body(format!("FIELD ERR {:?}", e)) @@ -77,9 +81,11 @@ pub async fn post_file( } } - HttpResponse::Ok().body("File was successfully uploaded") + HttpResponse::Ok().json(uploaded_filepathes) } +// EXAMPLE FROM ACTIX REPO + // use futures::TryStreamExt; // use std::io::Write; @@ -108,3 +114,51 @@ pub async fn post_file( // HttpResponse::Ok().body("sucess") // } + +/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + * _______ ______ ______ _______ *@@ + * |__ __@ | ____@ / ____@ |__ __@ *@@ + * | @ | @__ \_ @_ | @ *@@ + * | @ | __@ \ @_ | @ *@@ + * | @ | @___ ____\ @ | @ *@@ + * |__@ |______@ \______@ |__@ *@@ + * *@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/ + +#[cfg(test)] +mod test_static_files { + use super::*; + use crate::middleware::get_auth_cookie; + use actix_web::{ + http::{Method, StatusCode}, + test, App, + }; + + #[tokio::test] + async fn post_files_unauthenticated_should_be_unauthorized() { + let app_state = AppState::for_test().await; + + let mut app = test::init_service( + App::new() + .app_data(Data::new(app_state.clone())) + .app_data(Data::new(AuthenticatedAdminMiddleware::new( + "kuadrado-admin-auth", + ))) + .service(post_files), + ) + .await; + + let req = test::TestRequest::with_uri("/post-files") + .method(Method::POST) + .cookie(get_auth_cookie( + "wrong-cookie", + app_state.encryption.random_ascii_lc_string(32), + )) + .to_request(); + + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + } +}