From 16b70c5701b3e9f6a4f1265cce741867f3e568ea Mon Sep 17 00:00:00 2001
From: Pierre Jarriges <pierre.jarriges@tutanota.com>
Date: Mon, 18 Jul 2022 16:38:35 +0200
Subject: [PATCH] wip upload ans create file

---
 Cargo.lock                            |  77 +++++++++++++-----
 Cargo.toml                            |   1 +
 admin-frontend/src/components/root.js |  14 ++++
 admin-frontend/src/xhr.js             |  18 +++++
 src/main.rs                           |   1 +
 src/service.rs                        |   2 +
 src/service/static_files.rs           | 110 ++++++++++++++++++++++++++
 7 files changed, 205 insertions(+), 18 deletions(-)
 create mode 100644 src/service/static_files.rs

diff --git a/Cargo.lock b/Cargo.lock
index cfa0e81..2892c70 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -101,7 +101,7 @@ dependencies = [
  "http",
  "httparse",
  "indexmap",
- "itoa",
+ "itoa 0.4.8",
  "language-tags",
  "lazy_static",
  "log",
@@ -128,6 +128,24 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "actix-multipart"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "774bfeb11b54bf9c857a005b8ab893293da4eaff79261a66a9200dab7f5ab6e3"
+dependencies = [
+ "actix-service",
+ "actix-utils",
+ "actix-web",
+ "bytes 0.5.6",
+ "derive_more",
+ "futures-util",
+ "httparse",
+ "log",
+ "mime",
+ "twoway",
+]
+
 [[package]]
 name = "actix-router"
 version = "0.2.7"
@@ -1111,13 +1129,13 @@ dependencies = [
 
 [[package]]
 name = "http"
-version = "0.2.4"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
 dependencies = [
  "bytes 1.1.0",
  "fnv",
- "itoa",
+ "itoa 1.0.2",
 ]
 
 [[package]]
@@ -1163,7 +1181,7 @@ dependencies = [
  "http-body",
  "httparse",
  "httpdate",
- "itoa",
+ "itoa 0.4.8",
  "pin-project 1.0.8",
  "socket2",
  "tokio",
@@ -1257,6 +1275,12 @@ version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
 
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
 [[package]]
 name = "js-sys"
 version = "0.3.55"
@@ -1281,6 +1305,7 @@ name = "kuadrado_server"
 version = "2.0.2"
 dependencies = [
  "actix-files",
+ "actix-multipart",
  "actix-web",
  "actix-web-middleware-redirect-https",
  "chrono",
@@ -1313,9 +1338,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.102"
+version = "0.2.126"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
 
 [[package]]
 name = "linked-hash-map"
@@ -1325,9 +1350,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
 
 [[package]]
 name = "lock_api"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
 dependencies = [
  "scopeguard",
 ]
@@ -1588,9 +1613,9 @@ checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"
 
 [[package]]
 name = "once_cell"
-version = "1.8.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
 
 [[package]]
 name = "opaque-debug"
@@ -2107,7 +2132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
 dependencies = [
  "indexmap",
- "itoa",
+ "itoa 0.4.8",
  "ryu",
  "serde",
 ]
@@ -2119,7 +2144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
 dependencies = [
  "form_urlencoded",
- "itoa",
+ "itoa 0.4.8",
  "ryu",
  "serde",
 ]
@@ -2537,9 +2562,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
 
 [[package]]
 name = "tracing"
-version = "0.1.28"
+version = "0.1.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
+checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
 dependencies = [
  "cfg-if 1.0.0",
  "log",
@@ -2549,11 +2574,11 @@ dependencies = [
 
 [[package]]
 name = "tracing-core"
-version = "0.1.20"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf"
+checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
 dependencies = [
- "lazy_static",
+ "once_cell",
 ]
 
 [[package]]
@@ -2612,6 +2637,16 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
+[[package]]
+name = "twoway"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47"
+dependencies = [
+ "memchr",
+ "unchecked-index",
+]
+
 [[package]]
 name = "typed-builder"
 version = "0.4.1"
@@ -2635,6 +2670,12 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
 
+[[package]]
+name = "unchecked-index"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
+
 [[package]]
 name = "unicase"
 version = "2.6.0"
diff --git a/Cargo.toml b/Cargo.toml
index 739a029..714776b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,3 +25,4 @@ time = "0.2.7"
 regex = "1.5"
 tokio = { version = "0.2", features = ["full"] }
 sitemap = "0.4.1"
+actix-multipart = "0.3"
diff --git a/admin-frontend/src/components/root.js b/admin-frontend/src/components/root.js
index e4cf916..449b476 100644
--- a/admin-frontend/src/components/root.js
+++ b/admin-frontend/src/components/root.js
@@ -1,4 +1,5 @@
 
+const { fetch_post_file } = require("../xhr");
 const CreateArticleForm = require("./create-article-form");
 const UpdateArticleForm = require("./update-article-form");
 
@@ -30,6 +31,19 @@ class RootComponent {
             tag: "main",
             contents: [
                 { tag: "h1", contents: "Kuadrado admin panel" },
+                {
+                    tag: "form",
+                    style_rules: { border: "1px solid black" },
+                    enctype: "multipart/form-data",
+                    onsubmit: function (e) {
+                        e.preventDefault();
+                        fetch_post_file(e.target).then(res => console.log(res)).catch(err => console.log(err));
+                    },
+                    contents: [
+                        { tag: "input", name: "file", type: "file" },
+                        { tag: "input", type: "submit" }
+                    ]
+                },
                 {
                     tag: "nav",
                     contents: [
diff --git a/admin-frontend/src/xhr.js b/admin-frontend/src/xhr.js
index a650a6d..870dd8e 100644
--- a/admin-frontend/src/xhr.js
+++ b/admin-frontend/src/xhr.js
@@ -104,6 +104,23 @@ function fetch_all_articles() {
     });
 }
 
+function fetch_post_file(form) {
+    return new Promise((resolve, reject) => {
+        fetch("/post-file", {
+            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)
+            } else {
+                resolve(res_text);
+            }
+        })
+    })
+
+}
+
 
 module.exports = {
     fetch_article,
@@ -112,4 +129,5 @@ module.exports = {
     fetch_update_article,
     fetch_delete_article,
     fetch_all_articles,
+    fetch_post_file,
 }
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 7506cd5..cdeaa0f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -87,6 +87,7 @@ async fn main() -> std::io::Result<()> {
             .service(get_articles_by_category)
             .service(get_article)
             .service(get_all_articles)
+            .service(post_file)
             /////////////////////////////////////////////////////////////////////////////////////////////////////////////
             // STANDARD FILES ///////////////////////////////////////////////////////////////////////////////////////////
             .service(resource("/favicon.ico").route(get().to(favicon)))
diff --git a/src/service.rs b/src/service.rs
index 91376c0..6f1d53f 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -1,4 +1,6 @@
 mod admin_auth;
 mod articles;
+mod static_files;
 pub use admin_auth::*;
 pub use articles::*;
+pub use static_files::*;
diff --git a/src/service/static_files.rs b/src/service/static_files.rs
new file mode 100644
index 0000000..1842be7
--- /dev/null
+++ b/src/service/static_files.rs
@@ -0,0 +1,110 @@
+use crate::{middleware::AuthenticatedAdminMiddleware, AppState};
+use actix_multipart::Multipart;
+use actix_web::{
+    post,
+    web::{block, Data},
+    HttpRequest, HttpResponse, Responder,
+};
+use futures::StreamExt;
+use std::{
+    fs::{remove_file, File},
+    io::Write,
+    path::Path,
+    sync::Arc,
+};
+
+#[post("/post-file")]
+pub async fn post_file(
+    app_state: Data<AppState>,
+    mut payload: Multipart,
+    middleware: Data<AuthenticatedAdminMiddleware<'_>>,
+    req: HttpRequest,
+) -> impl Responder {
+    if middleware.exec(&app_state, &req, None).await.is_err() {
+        return HttpResponse::Unauthorized().finish();
+    }
+
+    while let Some(item) = payload.next().await {
+        match item {
+            Ok(mut field) => {
+                // Field in turn is stream of *Bytes* object
+                // A multipart/form-data stream has to contain `content_disposition`
+                let content_disposition = field
+                    .content_disposition()
+                    .expect("Missing Content Disposition header");
+
+                let filename = content_disposition.get_filename().expect("Missin filename");
+
+                let filepath = Arc::new(Path::new(format!("./tmp/{filename}")));
+
+                // File::create is blocking operation, use threadpool
+                let mut f = block(|| File::create(*filepath.clone())).await;
+
+                if f.is_err() {
+                    return HttpResponse::InternalServerError()
+                        .body(format!("Error creating file {:?}", f));
+                }
+
+                let 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() {
+                                error = Some("Error writing chunk".to_string());
+                                break 'chunks;
+                            }
+                        }
+                        Err(e) => {
+                            error = format!("Error writing file {} : {:?}", filename, e);
+                            break 'chunks;
+                        }
+                    }
+                }
+
+                if let Some(err) = error {
+                    block(|| remove_file(*filepath.clone())).await.unwrap();
+                    return HttpResponse::InternalServerError().body(err);
+                }
+            }
+            Err(e) => {
+                return HttpResponse::InternalServerError().body(format!("FIELD ERR {:?}", e))
+            }
+        }
+    }
+
+    HttpResponse::Ok().body("File was successfully uploaded")
+}
+
+// use futures::TryStreamExt;
+// use std::io::Write;
+
+// pub async fn save_file(mut payload: Multipart) -> impl Responder {
+//     println!("SAVE FILE");
+//     // iterate over multipart stream
+//     while let Some(mut field) = payload.try_next().await.unwrap() {
+//         // A multipart/form-data stream has to contain `content_disposition`
+//         let content_disposition = field.content_disposition().unwrap();
+//         let filename = content_disposition.get_filename().unwrap();
+//         let filepath = format!("./tmp/{filename}");
+
+//         // File::create is blocking operation, use threadpool
+//         let mut f = actix_web::web::block(|| std::fs::File::create(filepath))
+//             .await
+//             .unwrap();
+
+//         // Field in turn is stream of *Bytes* object
+//         while let Some(chunk) = field.try_next().await.unwrap() {
+//             // filesystem operations are blocking, we have to use threadpool
+//             f = actix_web::web::block(move || f.write_all(&chunk).map(|_| f))
+//                 .await
+//                 .unwrap();
+//         }
+//     }
+
+//     HttpResponse::Ok().body("sucess")
+// }
-- 
GitLab