From 23ccd3f79332dd4ce372529ac47b379dc355ec18 Mon Sep 17 00:00:00 2001 From: tomoron Date: Fri, 10 Apr 2026 20:45:53 +0200 Subject: [PATCH] add toast and start adding postgres --- .envrc | 1 + .gitignore | 2 + Cargo.lock | 66 +++++++++++ Cargo.toml | 1 + assets/tailwind.css | 50 ++++++++ diesel.toml | 9 ++ shell.nix | 12 +- src/components/mod.rs | 1 + src/components/toast/component.rs | 15 +++ src/components/toast/mod.rs | 2 + src/components/toast/style.css | 185 ++++++++++++++++++++++++++++++ src/config.rs | 25 ++++ src/main.rs | 13 ++- src/models/file.rs | 0 src/views/upload.rs | 70 ++++++----- todo | 9 +- 16 files changed, 425 insertions(+), 36 deletions(-) create mode 100644 diesel.toml create mode 100644 src/components/toast/component.rs create mode 100644 src/components/toast/mod.rs create mode 100644 src/components/toast/style.css create mode 100644 src/config.rs create mode 100644 src/models/file.rs diff --git a/.envrc b/.envrc index 1d953f4..1d389a6 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ +watch_file .env use nix diff --git a/.gitignore b/.gitignore index 33cc4d7..1a4b6b8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ # These are backup files generated by rustfmt **/*.rs.bk +migrations/ +postgres/ serveurhttp test .env diff --git a/Cargo.lock b/Cargo.lock index 7e51e66..ee00906 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -776,6 +776,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", + "strsim", "syn 2.0.117", ] @@ -855,6 +856,38 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diesel" +version = "2.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358" +dependencies = [ + "diesel_derives", + "downcast-rs", +] + +[[package]] +name = "diesel_derives" +version = "2.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47618bf0fac06bb670c036e48404c26a865e6a71af4114dfd97dfe89936e404e" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" +dependencies = [ + "syn 2.0.117", +] + [[package]] name = "digest" version = "0.10.7" @@ -1630,12 +1663,32 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "dpi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "dsl_auto_type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" +dependencies = [ + "darling", + "either", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "dtoa" version = "1.0.11" @@ -1657,6 +1710,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2475,6 +2534,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" name = "httpserver" version = "0.1.0" dependencies = [ + "diesel", "dioxus", "dioxus-html", "dioxus-primitives", @@ -4822,6 +4882,12 @@ dependencies = [ "quote", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subsecond" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index 9bdd02f..096afba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +diesel = "2.3.7" 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 } diff --git a/assets/tailwind.css b/assets/tailwind.css index d5f91da..d7780de 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -7,6 +7,9 @@ 'Noto Color Emoji'; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --color-zinc-900: oklch(21% 0.006 285.885); + --color-black: #000; + --color-white: #fff; --spacing: 0.25rem; --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5 / 2.25); @@ -170,9 +173,34 @@ .table { display: table; } + .h-full { + height: 100%; + } + .h-screen { + height: 100vh; + } + .w-full { + width: 100%; + } + .w-screen { + width: 100vw; + } + .border-collapse { + border-collapse: collapse; + } .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } + .resize { + resize: both; + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .bg-zinc-900 { + background-color: var(--color-zinc-900); + } .p-1 { padding: calc(var(--spacing) * 1); } @@ -190,6 +218,16 @@ --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold); } + .text-white { + color: var(--color-white); + } + .underline { + text-decoration-line: underline; + } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } } @property --tw-rotate-x { syntax: "*"; @@ -211,10 +249,20 @@ syntax: "*"; inherits: false; } +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @property --tw-font-weight { syntax: "*"; inherits: false; } +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { @@ -223,7 +271,9 @@ --tw-rotate-z: initial; --tw-skew-x: initial; --tw-skew-y: initial; + --tw-border-style: solid; --tw-font-weight: initial; + --tw-outline-style: solid; } } } diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..a0d61bf --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "migrations" diff --git a/shell.nix b/shell.nix index db44cec..1bca4e3 100644 --- a/shell.nix +++ b/shell.nix @@ -3,14 +3,16 @@ pkgs.mkShell { buildInputs = with pkgs;[ rustup ]; - nativeBuildInputs = with pkgs;[ + nativeBuildInputs = with pkgs;[ + libpq + libmysqlclient + sqlite + ]; PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; + shellHook = '' - source .env - + [ ! -f .env ] || export $(grep -v '^#' .env | xargs) ''; - - UPLOAD_FOLDER="./test/"; } diff --git a/src/components/mod.rs b/src/components/mod.rs index 1d45b58..02f8c70 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -5,3 +5,4 @@ pub use hero::Hero; mod echo; pub use echo::Echo; pub mod progress; +pub mod toast; diff --git a/src/components/toast/component.rs b/src/components/toast/component.rs new file mode 100644 index 0000000..4b6878f --- /dev/null +++ b/src/components/toast/component.rs @@ -0,0 +1,15 @@ +use dioxus::prelude::*; +use dioxus_primitives::toast::{self, ToastProviderProps}; + +#[component] +pub fn ToastProvider(props: ToastProviderProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + toast::ToastProvider { + default_duration: props.default_duration, + max_toasts: props.max_toasts, + render_toast: props.render_toast, + {props.children} + } + } +} diff --git a/src/components/toast/mod.rs b/src/components/toast/mod.rs new file mode 100644 index 0000000..9a8ae55 --- /dev/null +++ b/src/components/toast/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/src/components/toast/style.css b/src/components/toast/style.css new file mode 100644 index 0000000..7bf994a --- /dev/null +++ b/src/components/toast/style.css @@ -0,0 +1,185 @@ +.toast-container { + position: fixed; + z-index: 9999; + right: 20px; + bottom: 20px; + max-width: 350px; +} + +.toast-list { + display: flex; + flex-direction: column-reverse; + padding: 0; + margin: 0; + gap: 0.75rem; +} + +.toast-item { + display: flex; +} + +.toast { + z-index: calc(var(--toast-count) - var(--toast-index)); + display: flex; + overflow: hidden; + width: 18rem; + height: 4rem; + box-sizing: border-box; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border: 1px solid var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); + border-radius: 0.5rem; + margin-top: -4rem; + box-shadow: 0 4px 12px rgb(0 0 0 / 15%); + filter: var(--light, none) + var( + --dark, + brightness(calc(0.5 + 0.5 * (1 - ((var(--toast-index) + 1) / 4)))) + ); + opacity: calc(1 - var(--toast-hidden)); + transform: scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + transition: transform 0.2s ease, margin-top 0.2s ease, opacity 0.2s ease; + + --toast-hidden: calc(min(max(0, var(--toast-index) - 2), 1)); +} + +.toast-container:not(:hover, :focus-within) + .toast[data-toast-even]:not([data-top]) { + animation: slide-up-even 0.2s ease-out; +} + +.toast-container:not(:hover, :focus-within) + .toast[data-toast-odd]:not([data-top]) { + animation: slide-up-odd 0.2s ease-out; +} + +@keyframes slide-up-even { + from { + transform: translateY(0.5rem) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } + + to { + transform: translateY(0) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } +} + +@keyframes slide-up-odd { + from { + transform: translateY(0.5rem) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } + + to { + transform: translateY(0) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } +} + +.toast[data-top] { + animation: slide-in 0.2s ease-out; +} + +.toast-container:hover .toast[data-top], +.toast-container:focus-within .toast[data-top] { + animation: slide-in 0 ease-out; +} + +@keyframes slide-in { + from { + opacity: 0; + transform: translateY(100%) + scale( + calc(110% - var(--toast-index) * 5%), + calc(110% - var(--toast-index) * 2%) + ); + } + + to { + opacity: 1; + transform: translateY(0) + scale( + calc(100% - var(--toast-index) * 5%), + calc(100% - var(--toast-index) * 2%) + ); + } +} + +.toast-container:hover .toast, +.toast-container:focus-within .toast { + margin-top: var(--toast-padding); + filter: brightness(1); + opacity: 1; + transform: scale(calc(100%)); +} + +.toast[data-type="success"] { + background-color: var(--primary-success-color); + color: var(--secondary-success-color); +} + +.toast[data-type="error"] { + background-color: var(--primary-error-color); + color: var(--contrast-error-color); +} + +.toast[data-type="warning"] { + background-color: var(--primary-warning-color); + color: var(--secondary-warning-color); +} + +.toast[data-type="info"] { + background-color: var(--primary-info-color); + color: var(--secondary-info-color); +} + +.toast-content { + flex: 1; + margin-right: 8px; + transition: filter 0.2s ease; +} + +.toast-title { + margin-bottom: 4px; + color: var(--secondary-color-4); + font-weight: 600; +} + +.toast-description { + color: var(--secondary-color-3); + font-size: 0.875rem; +} + +.toast-close { + align-self: flex-start; + padding: 0; + border: none; + margin: 0; + background: none; + color: var(--secondary-color-3); + cursor: pointer; + font-size: 18px; + line-height: 1; +} + +.toast-close:hover { + color: var(--secondary-color-1); +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..82e7dbb --- /dev/null +++ b/src/config.rs @@ -0,0 +1,25 @@ +#[cfg(feature = "server")] +pub struct ServerConfig { + pub upload_folder: String, +} + +#[cfg(feature = "server")] +impl ServerConfig { + pub fn load() -> Self { + Self { + upload_folder: "./test/".to_string() + } + } +} + +pub struct ClientConfig { + pub upload_max_size: usize +} + +impl ClientConfig { + pub fn load () -> Self { + Self { + upload_max_size: 1024 * 1024 * 1024 * 1 //1GB for testing + } + } +} diff --git a/src/main.rs b/src/main.rs index 75e82a5..0ff363a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,10 @@ use dioxus::prelude::*; use views::{Upload}; use tracing::Level; +pub mod config; + +use crate::components::toast::ToastProvider; + mod components; mod views; @@ -25,9 +29,14 @@ fn main() { fn App() -> Element { rsx! { document::Link { rel: "icon", href: FAVICON } -// document::Link { rel: "stylesheet", href: MAIN_CSS } document::Link { rel: "stylesheet", href: TAILWIND_CSS } + div { + class: "h-screen w-screen bg-zinc-900 text-white", + ToastProvider { + Upload {} + } + } - Router:: {} +// Router:: {} } } diff --git a/src/models/file.rs b/src/models/file.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/views/upload.rs b/src/views/upload.rs index ead8747..d3bec58 100644 --- a/src/views/upload.rs +++ b/src/views/upload.rs @@ -1,58 +1,62 @@ use dioxus::{ - fullstack::{ByteStream, FileStream}, + fullstack::{FileStream}, prelude::*, }; -use dioxus_html::{FileData, HasFileData}; +#[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; -use std::{ env, fs, io::Write }; - - -use random_string; +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; - let mut res: String; - while (res_size >= 1000.0 && current < sizes.len()) { + while res_size >= 1000.0 && current < sizes.len() { res_size /= 1000.0; current += 1; } - res = ((res_size * 100.0).round() / 100.0).to_string() + " " + sizes[current]; - res + ((res_size * 100.0).round() / 100.0).to_string() + " " + sizes[current] } #[post("/api/upload")] async fn upload_file(mut upload: FileStream) -> Result { - let UPLOAD_SIZE_LIMIT: u64 = 1024 * 1024 * 1024 * 1; - let UPLOAD_FOLDER: String = match env::var("UPLOAD_FOLDER") { - Ok(value) => { value }, - Err(_) => { return HttpError::internal_server_error("UPLOAD_FOLDER not set"); } - }; + 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(UPLOAD_FOLDER.clone() + &cur).expect("can't check if file exists")) { + 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 > UPLOAD_SIZE_LIMIT { + 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(UPLOAD_FOLDER.clone() + &filename) { + 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") } }; @@ -61,7 +65,7 @@ async fn upload_file(mut upload: FileStream) -> Result { match chunk { Ok(bytes) => { total_len += bytes.len() as u64; - if total_len > UPLOAD_SIZE_LIMIT { + if total_len > c_config.upload_max_size.try_into().unwrap() { error = Some("Uploaded file too large"); break; } @@ -75,7 +79,7 @@ async fn upload_file(mut upload: FileStream) -> Result { match error { Some(err)=> { file.sync_data(); - fs::remove_file(UPLOAD_FOLDER.clone() + &filename); + fs::remove_file(s_config.upload_folder.clone() + &filename); HttpError::internal_server_error(err)? } None => { Ok(filename) } @@ -101,7 +105,18 @@ pub fn build_table(files: Vec<(String, String, Option> Some(res) => { match res { Ok(file_url) => {let url = file_url.clone(); rsx! { input { type:"button", - onclick: move |_| { web_sys::window().unwrap().navigator().clipboard().write_text(&url); }, + 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) => { @@ -120,13 +135,12 @@ pub fn build_table(files: Vec<(String, String, Option> #[component] pub fn Upload() -> Element { - //TODO: global config struct - let UPLOAD_SIZE_LIMIT: u64 = 1024 * 1024 * 1024 * 1; + let config = ClientConfig::load(); let mut selected : Signal>)>> = use_signal(|| vec![]); rsx! { - div { class : "p-2", + div { class : "p-2 h-full w-full", p { class: "text-4xl font-bold p-1", "Upload a file" } form { @@ -144,7 +158,7 @@ pub fn Upload() -> Element { selected.with_mut(|files| {files.push((file.name(), byte_to_human_size(file.size()), None)); } ); let idx = selected().len() - 1; - if (file.size() > UPLOAD_SIZE_LIMIT) { + 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 ; @@ -152,8 +166,10 @@ pub fn Upload() -> Element { let res = match upload_file(file.clone().into()).await { Ok(file_id) => { - let host = web_sys::window().unwrap().location().host().expect("unknown_host"); - Ok(host + "/upload/" + &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) } }; diff --git a/todo b/todo index bf451f7..61dc33d 100644 --- a/todo +++ b/todo @@ -1,11 +1,16 @@ /upload + - can upload file DONE + - show current server space usage + - direct download on /upload/dl/ - can upload up to 100GB - if video, set mime - deletion rules: - older than 1 month - - more than 300GB used and more than 7 days old - - show file deletion info + - more than 300GB used, delete oldest until under 200GB used, unless it's less than 7 days old. hard limit on 500GB + - file info on /upload/ + - file deletion queue position + - show file deletion info /status (check status of dependent devices, maybe useless)