diff --git a/Cargo.lock b/Cargo.lock index ee00906..f5586f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,6 +408,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link 0.2.1", ] @@ -862,8 +863,13 @@ version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358" dependencies = [ + "bitflags 2.11.0", + "byteorder", "diesel_derives", "downcast-rs", + "itoa", + "pq-sys", + "r2d2", ] [[package]] @@ -2534,6 +2540,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" name = "httpserver" version = "0.1.0" dependencies = [ + "chrono", "diesel", "dioxus", "dioxus-html", @@ -2541,6 +2548,7 @@ dependencies = [ "futures", "random-string", "reqwest 0.13.2", + "serde", "tracing", "wasm-bindgen", "web-sys", @@ -3867,6 +3875,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pq-sys" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "574ddd6a267294433f140b02a726b0640c43cf7c6f717084684aaa3b285aba61" +dependencies = [ + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -4056,6 +4075,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.7.3" @@ -4487,6 +4517,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 096afba..4cab82d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -diesel = "2.3.7" +chrono = { version = "0.4.44", features = ["serde"] } +diesel = { version = "2.3.7", features = ["postgres", "r2d2"], optional = true } dioxus = { version = "0.7.1", features = ["router", "fullstack"] } dioxus-html = "0.7.3" dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } futures = "0.3.32" random-string = "1.1.0" reqwest = { version = "0.13.2", features = ["stream"] } +serde = "1.0.228" tracing = "0.1.44" wasm-bindgen = "0.2.114" web-sys = { version = "0.3.91", features = [ "Navigator", "Clipboard" ] } @@ -27,7 +29,7 @@ desktop = ["dioxus/desktop"] # The feature that are only required for the mobile = ["dioxus/mobile"] build target should be optional and only enabled in the mobile = ["dioxus/mobile"] feature mobile = ["dioxus/mobile"] # The feature that are only required for the server = ["dioxus/server"] build target should be optional and only enabled in the server = ["dioxus/server"] feature -server = ["dioxus/server"] +server = ["dioxus/server", "dep:diesel"] [profile.release] opt-level = "z" diff --git a/README.md b/README.md deleted file mode 100644 index f7a582b..0000000 --- a/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Development - -Your new jumpstart project includes basic organization with an organized `assets` folder and a `components` folder. -If you chose to develop with the router feature, you will also have a `views` folder. - -``` -project/ -├─ assets/ # Any assets that are used by the app should be placed here -├─ src/ -│ ├─ main.rs # The entrypoint for the app. It also defines the routes for the app. -│ ├─ components/ -│ │ ├─ mod.rs # Defines the components module -│ │ ├─ hero.rs # The Hero component for use in the home page -│ │ ├─ echo.rs # The echo component uses server functions to communicate with the server -│ ├─ views/ # The views each route will render in the app. -│ │ ├─ mod.rs # Defines the module for the views route and re-exports the components for each route -│ │ ├─ blog.rs # The component that will render at the /blog/:id route -│ │ ├─ home.rs # The component that will render at the / route -├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project -``` - -### Automatic Tailwind (Dioxus 0.7+) - -As of Dioxus 0.7, there no longer is a need to manually install tailwind. Simply `dx serve` and you're good to go! - -Automatic tailwind is supported by checking for a file called `tailwind.css` in your app's manifest directory (next to Cargo.toml). To customize the file, use the dioxus.toml: - -```toml -[application] -tailwind_input = "my.css" -tailwind_output = "assets/out.css" -``` - -### Tailwind Manual Install - -To use tailwind plugins or manually customize tailwind, you can can install the Tailwind CLI and use it directly. - -1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm -2. Install the Tailwind CSS CLI: https://tailwindcss.com/docs/installation/tailwind-cli -3. Run the following command in the root of the project to start the Tailwind CSS compiler: - -```bash -npx @tailwindcss/cli -i ./input.css -o ./assets/tailwind.css --watch -``` - -### Serving Your App - -Run the following command in the root of your project to start developing with the default platform: - -```bash -dx serve --platform web -``` - -To run for a different platform, use the `--platform platform` flag. E.g. -```bash -dx serve --platform desktop -``` - - diff --git a/assets/tailwind.css b/assets/tailwind.css index d7780de..9396f7c 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -7,8 +7,9 @@ 'Noto Color Emoji'; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --color-blue-100: oklch(93.2% 0.032 255.585); + --color-blue-800: oklch(42.4% 0.199 265.638); --color-zinc-900: oklch(21% 0.006 285.885); - --color-black: #000; --color-white: #fff; --spacing: 0.25rem; --text-4xl: 2.25rem; @@ -167,6 +168,12 @@ .visible { visibility: visible; } + .static { + position: static; + } + .flex { + display: flex; + } .hidden { display: none; } @@ -194,6 +201,12 @@ .resize { resize: both; } + .flex-col { + flex-direction: column; + } + .flex-row { + flex-direction: row; + } .border { border-style: var(--tw-border-style); border-width: 1px; @@ -218,6 +231,12 @@ --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold); } + .text-blue-100 { + color: var(--color-blue-100); + } + .text-blue-800 { + color: var(--color-blue-800); + } .text-white { color: var(--color-white); } diff --git a/src/components/echo.rs b/src/components/echo.rs deleted file mode 100644 index 1d3567e..0000000 --- a/src/components/echo.rs +++ /dev/null @@ -1,37 +0,0 @@ -use dioxus::prelude::*; - -//const ECHO_CSS: Asset = asset!("/assets/styling/echo.css"); - -#[component] -pub fn Echo() -> Element { - let mut response = use_signal(|| String::new()); - - rsx! { -// document::Link { rel: "stylesheet", href: ECHO_CSS } - - div { - id: "echo", - h4 { "ServerFn Echo" } - input { - placeholder: "Type here to echo...", - oninput: move |event| async move { - let data = echo_server(event.value()).await.unwrap(); - - response.set(data); - }, - } - - if !response().is_empty() { - p { - "Server echoed: " - i { "{response}" } - } - } - } - } -} - -#[post("/api/echo")] -async fn echo_server(input: String) -> Result { - Ok(input) -} diff --git a/src/components/hero.rs b/src/components/hero.rs deleted file mode 100644 index e1f47db..0000000 --- a/src/components/hero.rs +++ /dev/null @@ -1,21 +0,0 @@ -use dioxus::prelude::*; - -const HEADER_SVG: Asset = asset!("/assets/header.svg"); - -#[component] -pub fn Hero() -> Element { - rsx! { - div { - id: "hero", - img { src: HEADER_SVG, id: "header" } - div { id: "links", - a { href: "https://dioxuslabs.com/learn/0.7/", "📚 Learn Dioxus" } - a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" } - a { href: "https://github.com/dioxus-community/", "📡📡 📡 📡 📡" } - a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" } - a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus", "some useless shit" } - a { href: "https://discord.gg/XgGxMSkvUM", "who tf uses discord ??" } - } - } - } -} diff --git a/src/components/mod.rs b/src/components/mod.rs index 02f8c70..b4cadc3 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,8 +1,7 @@ +//mod hero; +//pub use hero::Hero; -mod hero; -pub use hero::Hero; +pub mod upload; -mod echo; -pub use echo::Echo; pub mod progress; pub mod toast; diff --git a/src/components/upload/create.rs b/src/components/upload/create.rs new file mode 100644 index 0000000..105c1a6 --- /dev/null +++ b/src/components/upload/create.rs @@ -0,0 +1,194 @@ +use dioxus::{ + fullstack::{FileStream}, + prelude::*, +}; + +use httpserver::utils::byte_to_human_size; + +#[cfg(feature = "server")] +use crate::config::ServerConfig; + +use crate::config::ClientConfig; + +use std::time::Duration; + +use std::fs; +use std::fs::File; +use std::io::Write; +use futures::StreamExt; + + + +#[cfg(feature = "server")] +use httpserver::models::{GetFile,NewFile}; +#[cfg(feature = "server")] +use diesel::prelude::*; +#[cfg(feature = "server")] +use httpserver::DB; + + + +use dioxus_primitives::toast::{ + ToastOptions, + consume_toast +}; + +#[post("/api/upload")] +async fn upload_file(mut upload: FileStream) -> Result { + use httpserver::schema::file; + let s_config = ServerConfig::load(); + let c_config = ClientConfig::load(); + + let mut total_len: u64 = 0; + let mut error: Option<&str> = None; + + let filename: String = loop { + let cur = random_string::generate(20, "ABCDEFGHIJKLMNOPQRTSTUVWXYZ0123456789"); + if (!fs::exists(s_config.upload_folder.clone() + &cur).expect("can't check if file exists")) { + break cur; + } + }; + + if let Some(size) = upload.size() { + if size > c_config.upload_max_size.try_into().unwrap() { + return HttpError::payload_too_large("this file is too large"); + } + } + + if upload.file_name().len() > 255 { + return HttpError::bad_request("Filename too long"); + } + + + let mut file = match fs::File::create(s_config.upload_folder.clone() + &filename) { + Ok(val) => { val }, + Err(_) => { return HttpError::internal_server_error("failed to open the output file") } + }; + + while let Some(chunk) = upload.next().await { + match chunk { + Ok(bytes) => { + total_len += bytes.len() as u64; + if total_len > c_config.upload_max_size.try_into().unwrap() { + error = Some("Uploaded file too large"); + break; + } + + if file.write(&bytes).is_err() { error == Some("failed write"); break; }; + }, + Err(_) => { error = Some("unknown"); break; } + } + } + + match error { + Some(err)=> { + file.sync_data(); + fs::remove_file(s_config.upload_folder.clone() + &filename); + HttpError::internal_server_error(err)? + } + None => { + let new_file = NewFile{filename: upload.file_name(), file_size: &(total_len as i64), stored_filename: &filename}; + + DB.with(|pool| diesel::insert_into(file::table) + .values(&new_file) + .returning(GetFile::as_returning()) + .get_result(&mut pool.get().unwrap()) + .expect("Failed to save the file in db")); + Ok(filename) + } + } + +} + +pub fn build_table(files: Vec<(String, String, Option>)>) -> Element { + rsx! { + table { + tr { + th { "filename" } + th { "size" } + th { "url" } + } + + { files.iter().map(|file| rsx! { + tr { + td { class: "px-2", "{file.0}" } + td { class: "px-2", "{file.1}" } + td { class: "px-2", + match &file.2 { + Some(res) => { match res { + Ok(file_url) => {let url = file_url.clone(); rsx! { input { + type:"button", + onclick: move |_| { + let toast_api = consume_toast(); + toast_api + .success( + "Success".to_string(), + ToastOptions::new() + .description("The url has been copied successfully") + .duration(Duration::from_secs(5)) + .permanent(false), + ); + let _ = web_sys::window().unwrap().navigator().clipboard().write_text(&url); + }, + value: "{file_url}" + } } }, + Err(e) => { + let msg = e.message.clone().unwrap(); + rsx! { p { "Upload failed, reason : {msg}" } } + } + } } + None => { rsx! { p { "Waiting for file to be uploaded" } } } + } + } + } + }) } + } + } +} + +#[component] +pub fn UploadFile() -> Element { + let config = ClientConfig::load(); + + let mut selected : Signal>)>> = use_signal(|| vec![]); + + rsx! { + div { class : "p-2 h-full w-full", + p { class: "text-4xl font-bold p-1", "Upload a file" } + + form { + + label { for :"file", { + if selected.len() == 0 { + rsx! { "No file selected" } + } else { + build_table(selected()) + } + } } + + input { id: "file", r#type : "file" ,multiple: false, class : "hidden", oninput: move |e| async move { + let file = &e.files()[0]; + selected.with_mut(|files| {files.push((file.name(), byte_to_human_size(file.size()), None)); } ); + let idx = selected().len() - 1; + + if file.size() > config.upload_max_size as u64 { + //messy but firefox can't handle when server returns early + selected.with_mut(|files| { files[idx].2 = Some(HttpError::payload_too_large("This file is too large")) }); + return ; + } + + let res = match upload_file(file.clone().into()).await { + Ok(file_id) => { + let location = web_sys::window().unwrap().location(); + let host = location.host().expect("unknown host"); + let protocol = location.protocol().expect("unknown protocol"); + Ok(protocol + "//" + &host + "/upload/" + &file_id) + }, + Err(err) => { Err(err) } + }; + selected.with_mut(|files| { files[idx].2 = Some(res) }); + }} + } + } + } +} diff --git a/src/components/upload/get.rs b/src/components/upload/get.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/components/upload/info.rs b/src/components/upload/info.rs new file mode 100644 index 0000000..8654635 --- /dev/null +++ b/src/components/upload/info.rs @@ -0,0 +1,84 @@ +use std::time::SystemTime; + +use chrono::DateTime; +use chrono::Utc; + +use dioxus::fullstack::httperror; +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +use httpserver::utils::byte_to_human_size; + +#[cfg(feature = "server")] +use httpserver::models::GetFile; +#[cfg(feature = "server")] +use diesel::prelude::*; +#[cfg(feature = "server")] +use httpserver::DB; + +#[cfg(feature = "server")] +use httpserver::schema; + + +#[derive(Serialize, Deserialize)] +struct FetchedInfo { + filename: String, + size: i64, + created_at: DateTime, + deletion_pos: Option, +} + + +#[post("/api/upload/info")] +async fn get_file_info(filename: String) -> Result { + use schema::file; + let result = DB.with(|pool| schema::file::table.select((file::filename, file::file_size, file::created_at)) + .filter(file::stored_filename.eq(filename)) + .filter(file::deleted.eq(false)) + .load::<(String, i64, SystemTime)>(&mut pool.get().unwrap())); + + if let Err(_) = result { + return HttpError::internal_server_error("Database request failed"); + } + let db_file = result.unwrap(); + if db_file.len() == 0 { + return HttpError::internal_server_error("The requested file does not exist"); + } + + let db_file = &db_file[0]; + + Ok(FetchedInfo { filename: db_file.0.clone(), size: db_file.1, created_at: db_file.2.into(), deletion_pos: None }) +} + +fn show_file_info(file_id: String, file_info: &FetchedInfo) -> Element { + let creation_date = file_info.created_at.format("%Y-%m-%d at %T"); + rsx! { + div { class: "p-2", + h1 { class: "text-4xl font-bold", "download a file" } + div { class : "flex flex-col", + p { "File name : {file_info.filename}" } + p { "This file was uploaded on the {creation_date}" } + p { "File size : {byte_to_human_size(file_info.size.try_into().unwrap())}" } + p { "Position in the deletion queue : ", + match file_info.deletion_pos { + Some(pos) => pos.to_string(), + None => "unknown".to_string() + } + } + a { class: "height-20px text-blue-800 underline", href: "/upload/{file_id}/dl", "click here to download the file"} + } + } + } +} + +#[component] +pub fn FileInfo(file: String) -> Element { + let file_id = file.clone(); + let file_info = use_resource(move || get_file_info(file.clone())); + + match &*file_info.read_unchecked() { + Some(Ok(info)) => { show_file_info(file_id, info) }, + Some(Err(err)) => { rsx! { p {"server retrned an error : {err}"} } }, + _ => { rsx! { p {"Loading ..."} } } + } +} diff --git a/src/components/upload/mod.rs b/src/components/upload/mod.rs new file mode 100644 index 0000000..21028a8 --- /dev/null +++ b/src/components/upload/mod.rs @@ -0,0 +1,5 @@ +mod create; +pub use create::UploadFile; + +mod info; +pub use info::FileInfo; diff --git a/src/lib.rs b/src/lib.rs index f1d74a1..490a9d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ use diesel::pg::PgConnection; #[cfg(feature = "server")] use diesel::prelude::*; +pub mod utils; + pub mod config; #[cfg(feature = "server")] diff --git a/src/main.rs b/src/main.rs index 9eef16e..18c329a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use dioxus::prelude::*; -use views::{Upload}; +use crate::components::upload::{FileInfo, UploadFile}; use tracing::Level; pub mod config; @@ -10,12 +10,12 @@ use crate::components::toast::ToastProvider; mod components; mod views; -#[derive(Debug, Clone, Routable, PartialEq)] -#[rustfmt::skip] -enum Route { - #[route("/upload")] - Upload {}, -} +//#[derive(Debug, Clone, Routable, PartialEq)] +//#[rustfmt::skip] +//enum Route { +// #[route("/upload")] +// Upload { }, +//} const FAVICON: Asset = asset!("/assets/favicon.ico"); @@ -34,10 +34,10 @@ fn App() -> Element { div { class: "h-screen w-screen bg-zinc-900 text-white", ToastProvider { - Upload {} + FileInfo {file: "ULI0GZWJQH9BGEZR1VZ1"} +// Router:: {} } } -// Router:: {} } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..e8184b5 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,14 @@ +pub fn byte_to_human_size(size: u64) -> String { + let sizes = vec!["B", "KB", "MB", "GB", "TB", "WTFAREYOUDOINGB"]; + let mut current = 0; + + let mut res_size: f64 = size as f64; + + while res_size >= 1000.0 && current < sizes.len() { + res_size /= 1000.0; + current += 1; + } + + ((res_size * 100.0).round() / 100.0).to_string() + " " + sizes[current] +} + diff --git a/src/views/mod.rs b/src/views/mod.rs index a71900a..aea56c1 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -1,2 +1,2 @@ -mod upload; -pub use upload::Upload; +//mod upload; +//pub use upload::Upload; diff --git a/src/views/upload.rs b/src/views/upload.rs index 8ace0f2..977db67 100644 --- a/src/views/upload.rs +++ b/src/views/upload.rs @@ -6,17 +6,6 @@ use dioxus::{ #[cfg(feature = "server")] use crate::config::ServerConfig; -use crate::config::ClientConfig; - -use std::time::Duration; - -use std::fs; -use std::fs::File; -use std::io::Write; -use futures::StreamExt; - - - #[cfg(feature = "server")] use httpserver::models::{GetFile,NewFile}; #[cfg(feature = "server")] @@ -25,178 +14,7 @@ use diesel::prelude::*; use httpserver::DB; - use dioxus_primitives::toast::{ ToastOptions, consume_toast }; - -pub fn byte_to_human_size(size: u64) -> String { - let sizes = vec!["B", "KB", "MB", "GB", "TB", "WTFAREYOUDOINGB"]; - let mut current = 0; - - let mut res_size: f64 = size as f64; - - while res_size >= 1000.0 && current < sizes.len() { - res_size /= 1000.0; - current += 1; - } - - ((res_size * 100.0).round() / 100.0).to_string() + " " + sizes[current] -} - -#[post("/api/upload")] -async fn upload_file(mut upload: FileStream) -> Result { - use httpserver::schema::file; - let s_config = ServerConfig::load(); - let c_config = ClientConfig::load(); - - let mut total_len: u64 = 0; - let mut error: Option<&str> = None; - - let filename: String = loop { - let cur = random_string::generate(20, "ABCDEFGHIJKLMNOPQRTSTUVWXYZ0123456789"); - if (!fs::exists(s_config.upload_folder.clone() + &cur).expect("can't check if file exists")) { - break cur; - } - }; - - if let Some(size) = upload.size() { - if size > c_config.upload_max_size.try_into().unwrap() { - return HttpError::payload_too_large("this file is too large"); - } - } - - - let mut file = match fs::File::create(s_config.upload_folder.clone() + &filename) { - Ok(val) => { val }, - Err(_) => { return HttpError::internal_server_error("failed to open the output file") } - }; - - while let Some(chunk) = upload.next().await { - match chunk { - Ok(bytes) => { - total_len += bytes.len() as u64; - if total_len > c_config.upload_max_size.try_into().unwrap() { - error = Some("Uploaded file too large"); - break; - } - - if file.write(&bytes).is_err() { error == Some("failed write"); break; }; - }, - Err(_) => { error = Some("unknown"); break; } - } - } - - match error { - Some(err)=> { - file.sync_data(); - fs::remove_file(s_config.upload_folder.clone() + &filename); - HttpError::internal_server_error(err)? - } - None => { - let new_file = NewFile{filename: upload.file_name(), file_size: &(total_len as i64), stored_filename: &filename}; - - DB.with(|pool| diesel::insert_into(file::table) - .values(&new_file) - .returning(GetFile::as_returning()) - .get_result(&mut pool.get().unwrap()) - .expect("Failed to save the file in db")); - Ok(filename) - } - } - -} - -pub fn build_table(files: Vec<(String, String, Option>)>) -> Element { - rsx! { - table { - tr { - th { "filename" } - th { "size" } - th { "url" } - } - - { files.iter().map(|file| rsx! { - tr { - td { class: "px-2", "{file.0}" } - td { class: "px-2", "{file.1}" } - td { class: "px-2", - match &file.2 { - Some(res) => { match res { - Ok(file_url) => {let url = file_url.clone(); rsx! { input { - type:"button", - onclick: move |_| { - let toast_api = consume_toast(); - toast_api - .success( - "Success".to_string(), - ToastOptions::new() - .description("The url has been copied successfully") - .duration(Duration::from_secs(5)) - .permanent(false), - ); - let _ = web_sys::window().unwrap().navigator().clipboard().write_text(&url); - }, - value: "{file_url}" - } } }, - Err(e) => { - let msg = e.message.clone().unwrap(); - rsx! { p { "Upload failed, reason : {msg}" } } - } - } } - None => { rsx! { p { "Waiting for file to be uploaded" } } } - } - } - } - }) } - } - } -} - -#[component] -pub fn Upload() -> Element { - let config = ClientConfig::load(); - - let mut selected : Signal>)>> = use_signal(|| vec![]); - - rsx! { - div { class : "p-2 h-full w-full", - p { class: "text-4xl font-bold p-1", "Upload a file" } - - form { - - label { for :"file", { - if selected.len() == 0 { - rsx! { "No file selected" } - } else { - build_table(selected()) - } - } } - - input { id: "file", r#type : "file" ,multiple: false, class : "hidden", oninput: move |e| async move { - let file = &e.files()[0]; - selected.with_mut(|files| {files.push((file.name(), byte_to_human_size(file.size()), None)); } ); - let idx = selected().len() - 1; - - if file.size() > config.upload_max_size as u64 { - //messy but firefox can't handle when server returns early - selected.with_mut(|files| { files[idx].2 = Some(HttpError::payload_too_large("This file is too large")) }); - return ; - } - - let res = match upload_file(file.clone().into()).await { - Ok(file_id) => { - let location = web_sys::window().unwrap().location(); - let host = location.host().expect("unknown host"); - let protocol = location.protocol().expect("unknown protocol"); - Ok(protocol + "//" + &host + "/upload/" + &file_id) - }, - Err(err) => { Err(err) } - }; - selected.with_mut(|files| { files[idx].2 = Some(res) }); - }} - } - } - } -}