working(ish) upload

This commit is contained in:
2026-03-26 15:59:54 +01:00
parent 2d19e3b5d7
commit cd030a6c44
21 changed files with 651 additions and 126 deletions

View File

@ -1,13 +1,13 @@
use dioxus::prelude::*;
const ECHO_CSS: Asset = asset!("/assets/styling/echo.css");
//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 }
// document::Link { rel: "stylesheet", href: ECHO_CSS }
div {
id: "echo",

View File

@ -4,3 +4,4 @@ pub use hero::Hero;
mod echo;
pub use echo::Echo;
pub mod progress;

View File

@ -0,0 +1,23 @@
use dioxus::prelude::*;
use dioxus_primitives::progress::{self, ProgressIndicatorProps, ProgressProps};
#[component]
pub fn Progress(props: ProgressProps) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: asset!("./style.css") }
progress::Progress {
class: "progress",
value: props.value,
max: props.max,
attributes: props.attributes,
{props.children}
}
}
}
#[component]
pub fn ProgressIndicator(props: ProgressIndicatorProps) -> Element {
rsx! {
progress::ProgressIndicator { class: "progress-indicator", attributes: props.attributes, {props.children} }
}
}

View File

@ -0,0 +1,2 @@
mod component;
pub use component::*;

View File

@ -0,0 +1,31 @@
.progress {
position: relative;
overflow: hidden;
width: 200px;
height: .5rem;
box-sizing: border-box;
border-radius: 9999px;
background: var(--primary-color-5);
}
.progress[data-state='indeterminate'] .progress-indicator {
width: 50%;
animation: indeterminate 1s infinite linear;
}
.progress-indicator {
width: var(--progress-value, 0%);
height: 100%;
background-color: var(--secondary-color-1);
transition: width 250ms ease;
}
@keyframes indeterminate {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(200%);
}
}

View File

@ -1,7 +1,7 @@
use dioxus::prelude::*;
use views::{Blog, Home, Navbar};
use views::{Blog, Home, Navbar, Upload};
use tracing::Level;
mod components;
mod views;
@ -9,6 +9,8 @@ mod views;
#[derive(Debug, Clone, Routable, PartialEq)]
#[rustfmt::skip]
enum Route {
#[route("/upload")]
Upload {},
#[layout(Navbar)]
#[route("/")]
Home {},
@ -17,10 +19,11 @@ enum Route {
}
const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
//const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
fn main() {
dioxus::logger::init(Level::INFO).expect("failed to init logger");
dioxus::launch(App);
}
@ -28,7 +31,7 @@ fn main() {
fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
// document::Link { rel: "stylesheet", href: MAIN_CSS }
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
Router::<Route> {}

View File

@ -2,12 +2,12 @@ use crate::Route;
use dioxus::prelude::*;
use crate::components::{Hero};
const BLOG_CSS: Asset = asset!("/assets/styling/blog.css");
//const BLOG_CSS: Asset = asset!("/assets/styling/blog.css");
#[component]
pub fn Blog(id: i32) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: BLOG_CSS }
// document::Link { rel: "stylesheet", href: BLOG_CSS }
div {
id: "blog",

View File

@ -6,3 +6,6 @@ pub use blog::Blog;
mod navbar;
pub use navbar::Navbar;
mod upload;
pub use upload::Upload;

View File

@ -1,12 +1,12 @@
use crate::Route;
use dioxus::prelude::*;
const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css");
//const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css");
#[component]
pub fn Navbar() -> Element {
rsx! {
document::Link { rel: "stylesheet", href: NAVBAR_CSS }
// document::Link { rel: "stylesheet", href: NAVBAR_CSS }
div {
id: "navbar",

154
src/views/upload.rs Normal file
View File

@ -0,0 +1,154 @@
use dioxus::{
fullstack::{ByteStream, FileStream},
prelude::*,
};
use dioxus_html::{FileData, HasFileData};
use futures::StreamExt;
use std::{ env, fs, io::Write };
use random_string;
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()) {
res_size /= 1000.0;
current += 1;
}
res = ((res_size * 100.0).round() / 100.0).to_string() + " " + sizes[current];
res
}
#[post("/api/upload")]
async fn upload_file(mut upload: FileStream) -> Result<String, HttpError> {
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 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")) {
break cur;
}
};
if upload.size().unwrap() > UPLOAD_SIZE_LIMIT {
return HttpError::payload_too_large("this file is too large");
}
let mut file = match fs::File::create(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 > UPLOAD_SIZE_LIMIT {
error = Some("Uploaded file to 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(UPLOAD_FOLDER.clone() + &filename);
HttpError::internal_server_error(err)?
}
None => { 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_id) => { rsx! { input { type:"button" , "/upload/dl/{file_id}"} } },
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 {
//TODO: global config struct
let UPLOAD_SIZE_LIMIT: u64 = 1024 * 1024 * 1024 * 1;
let mut selected : Signal<Vec<(String, String, Option<Result<String, HttpError>>)>> = use_signal(|| vec![]);
rsx! {
div { class : "p-2",
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: true, class : "hidden", oninput: move |e| async move {
for file in e.files() {
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) {
//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")) });
continue;
}
let res = upload_file(file.clone().into()).await;
selected.with_mut(|files| { files[idx].2 = Some(res) });
}
}}
}
}
}
}