From 7d114c7414c6fbb06d0772c3dbf936f757282086 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Mon, 15 Jan 2024 17:07:14 -0500 Subject: [PATCH] file upload example --- examples/server_fns_axum/Cargo.toml | 1 + examples/server_fns_axum/src/app.rs | 86 ++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index c23ec1ae3..fb1d7b762 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -27,6 +27,7 @@ thiserror = "1.0" wasm-bindgen = "0.2" serde_toml = "0.0.1" toml = "0.8.8" +web-sys = { version = "0.3.67", features = ["FileList", "File"] } [features] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index af9d4b5d0..dd5edb0fb 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -1,23 +1,16 @@ -use crate::error_template::ErrorTemplate; -use http::{Request, Response}; use leptos::{html::Input, *}; -use leptos_meta::{Link, Stylesheet}; -use leptos_router::*; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use server_fn::{ - codec::{ - Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, Rkyv, SerdeLite, - }, - error::NoCustomError, - request::{browser::BrowserRequest, BrowserMockReq, ClientReq, Req}, - response::{browser::BrowserResponse, ClientRes, Res}, - rkyv::AlignedVec, +use leptos_meta::{provide_meta_context, Link, Stylesheet}; +use leptos_router::{ActionForm, Route, Router, Routes}; +use server_fn::codec::{ + GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, }; #[cfg(feature = "ssr")] use std::sync::{ atomic::{AtomicU8, Ordering}, Mutex, }; +use wasm_bindgen::JsCast; +use web_sys::{FormData, HtmlFormElement, SubmitEvent}; #[component] pub fn TodoApp() -> impl IntoView { @@ -49,7 +42,7 @@ pub fn HomePage() -> impl IntoView {

"Alternative Encodings"

- + } } @@ -310,3 +303,68 @@ pub fn RkyvExample() -> impl IntoView { } } + +#[component] +pub fn FileUpload() -> impl IntoView { + /// A simple file upload function, which does just returns the length of the file. + /// + /// On the server, this uses the `multer` crate, which provides a streaming API. + #[server( + input = MultipartFormData, +)] + pub async fn file_length( + data: MultipartData, + ) -> Result { + // `.into_inner()` returns the inner `multer` stream + // it is `None` if we call this on the client, but always `Some(_)` on the server, so is safe to + // unwrap + let mut data = data.into_inner().unwrap(); + + // this will just measure the total number of bytes uploaded + let mut count = 0; + while let Ok(Some(mut field)) = data.next_field().await { + println!("\n[NEXT FIELD]\n"); + let name = field.name().unwrap_or_default().to_string(); + println!(" [NAME] {name}"); + while let Ok(Some(chunk)) = field.chunk().await { + let len = chunk.len(); + count += len; + println!(" [CHUNK] {len}"); + // in a real server function, you'd do something like saving the file here + } + } + + Ok(count) + } + + let upload_action = create_action(|data: &FormData| { + let data = data.clone(); + // `MultipartData` implements `From` + file_length(data.into()) + }); + + view! { +

File Upload

+

Uploading files is fairly easy using multipart form data.

+
(); + let form_data = FormData::new_with_form(&target).unwrap(); + upload_action.dispatch(form_data); + }> + + +
+

+ {move || if upload_action.input().get().is_none() && upload_action.value().get().is_none() { + "Upload a file.".to_string() + } else if upload_action.pending().get() { + "Uploading...".to_string() + } else if let Some(Ok(value)) = upload_action.value().get() { + value.to_string() + } else { + format!("{:?}", upload_action.value().get()) + }} +

+ } +}