display file info
This commit is contained in:
39
Cargo.lock
generated
39
Cargo.lock
generated
@ -408,6 +408,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
@ -862,8 +863,13 @@ version = "2.3.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358"
|
checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"byteorder",
|
||||||
"diesel_derives",
|
"diesel_derives",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
|
"itoa",
|
||||||
|
"pq-sys",
|
||||||
|
"r2d2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2534,6 +2540,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|||||||
name = "httpserver"
|
name = "httpserver"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"diesel",
|
"diesel",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
@ -2541,6 +2548,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"random-string",
|
"random-string",
|
||||||
"reqwest 0.13.2",
|
"reqwest 0.13.2",
|
||||||
|
"serde",
|
||||||
"tracing",
|
"tracing",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@ -3867,6 +3875,17 @@ dependencies = [
|
|||||||
"zerocopy",
|
"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]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -4056,6 +4075,17 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
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]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@ -4487,6 +4517,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|||||||
@ -7,13 +7,15 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[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 = { version = "0.7.1", features = ["router", "fullstack"] }
|
||||||
dioxus-html = "0.7.3"
|
dioxus-html = "0.7.3"
|
||||||
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
|
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
random-string = "1.1.0"
|
random-string = "1.1.0"
|
||||||
reqwest = { version = "0.13.2", features = ["stream"] }
|
reqwest = { version = "0.13.2", features = ["stream"] }
|
||||||
|
serde = "1.0.228"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
wasm-bindgen = "0.2.114"
|
wasm-bindgen = "0.2.114"
|
||||||
web-sys = { version = "0.3.91", features = [ "Navigator", "Clipboard" ] }
|
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
|
# 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"]
|
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
|
# 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]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|||||||
59
README.md
59
README.md
@ -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
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
@ -7,8 +7,9 @@
|
|||||||
'Noto Color Emoji';
|
'Noto Color Emoji';
|
||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
monospace;
|
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-zinc-900: oklch(21% 0.006 285.885);
|
||||||
--color-black: #000;
|
|
||||||
--color-white: #fff;
|
--color-white: #fff;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
--text-4xl: 2.25rem;
|
--text-4xl: 2.25rem;
|
||||||
@ -167,6 +168,12 @@
|
|||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
.static {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -194,6 +201,12 @@
|
|||||||
.resize {
|
.resize {
|
||||||
resize: both;
|
resize: both;
|
||||||
}
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.flex-row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
.border {
|
.border {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@ -218,6 +231,12 @@
|
|||||||
--tw-font-weight: var(--font-weight-bold);
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
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 {
|
.text-white {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<String> {
|
|
||||||
Ok(input)
|
|
||||||
}
|
|
||||||
@ -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 ??" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,7 @@
|
|||||||
|
//mod hero;
|
||||||
|
//pub use hero::Hero;
|
||||||
|
|
||||||
mod hero;
|
pub mod upload;
|
||||||
pub use hero::Hero;
|
|
||||||
|
|
||||||
mod echo;
|
|
||||||
pub use echo::Echo;
|
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
pub mod toast;
|
pub mod toast;
|
||||||
|
|||||||
194
src/components/upload/create.rs
Normal file
194
src/components/upload/create.rs
Normal file
@ -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<String, HttpError> {
|
||||||
|
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<Result<String, HttpError>>)>) -> 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<Vec<(String, String, Option<Result<String, HttpError>>)>> = 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) });
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/components/upload/get.rs
Normal file
0
src/components/upload/get.rs
Normal file
84
src/components/upload/info.rs
Normal file
84
src/components/upload/info.rs
Normal file
@ -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<Utc>,
|
||||||
|
deletion_pos: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[post("/api/upload/info")]
|
||||||
|
async fn get_file_info(filename: String) -> Result<FetchedInfo, HttpError> {
|
||||||
|
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 ..."} } }
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/components/upload/mod.rs
Normal file
5
src/components/upload/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod create;
|
||||||
|
pub use create::UploadFile;
|
||||||
|
|
||||||
|
mod info;
|
||||||
|
pub use info::FileInfo;
|
||||||
@ -3,6 +3,8 @@ use diesel::pg::PgConnection;
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
|
|||||||
22
src/main.rs
22
src/main.rs
@ -1,6 +1,6 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use views::{Upload};
|
use crate::components::upload::{FileInfo, UploadFile};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
@ -10,12 +10,12 @@ use crate::components::toast::ToastProvider;
|
|||||||
mod components;
|
mod components;
|
||||||
mod views;
|
mod views;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Routable, PartialEq)]
|
//#[derive(Debug, Clone, Routable, PartialEq)]
|
||||||
#[rustfmt::skip]
|
//#[rustfmt::skip]
|
||||||
enum Route {
|
//enum Route {
|
||||||
#[route("/upload")]
|
// #[route("/upload")]
|
||||||
Upload {},
|
// Upload { },
|
||||||
}
|
//}
|
||||||
|
|
||||||
|
|
||||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
@ -34,10 +34,10 @@ fn App() -> Element {
|
|||||||
div {
|
div {
|
||||||
class: "h-screen w-screen bg-zinc-900 text-white",
|
class: "h-screen w-screen bg-zinc-900 text-white",
|
||||||
ToastProvider {
|
ToastProvider {
|
||||||
Upload {}
|
FileInfo {file: "ULI0GZWJQH9BGEZR1VZ1"}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Router::<Route> {}
|
// Router::<Route> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
14
src/utils.rs
Normal file
14
src/utils.rs
Normal file
@ -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]
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,2 +1,2 @@
|
|||||||
mod upload;
|
//mod upload;
|
||||||
pub use upload::Upload;
|
//pub use upload::Upload;
|
||||||
|
|||||||
@ -6,17 +6,6 @@ use dioxus::{
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use crate::config::ServerConfig;
|
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")]
|
#[cfg(feature = "server")]
|
||||||
use httpserver::models::{GetFile,NewFile};
|
use httpserver::models::{GetFile,NewFile};
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
@ -25,178 +14,7 @@ use diesel::prelude::*;
|
|||||||
use httpserver::DB;
|
use httpserver::DB;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use dioxus_primitives::toast::{
|
use dioxus_primitives::toast::{
|
||||||
ToastOptions,
|
ToastOptions,
|
||||||
consume_toast
|
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<String, HttpError> {
|
|
||||||
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<Result<String, HttpError>>)>) -> 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<Vec<(String, String, Option<Result<String, HttpError>>)>> = 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) });
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user