From 232acae2c223e958b710768c58eb1f96191b1bdc Mon Sep 17 00:00:00 2001
From: Pierre Jarriges <pierre.jarriges@tutanota.com>
Date: Tue, 19 Jul 2022 15:10:12 +0200
Subject: [PATCH] basic upload files working

---
 .gitignore                            |  3 +-
 admin-frontend/src/components/root.js |  2 +-
 admin-frontend/src/xhr.js             |  7 +-
 src/main.rs                           |  2 +-
 src/service/static_files.rs           | 96 +++++++++++++++++++++------
 5 files changed, 82 insertions(+), 28 deletions(-)

diff --git a/.gitignore b/.gitignore
index ff96692..7bbcb76 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 449b476..d07a98c 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 870dd8e..4255cff 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 cdeaa0f..99b3a57 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 1842be7..a179b2b 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);
+    }
+}
-- 
GitLab