More fullstack fixes (#4737)

* allow `--server` only builds!

* rollback example

* capture `/:route` routes

* fix: properly implement file uploads in forms

* implement desktop form uploads

* Performant serialization of form bytes

* finish up file stuff

* cleanups

* add request headers to streaming context

* remove some unwraps
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-07 23:07:43 -07:00
1 parent fc01a002fd
commit 92792aded9
47 files changed
+1241 -703

No files matched your search

+241 -198
View File
@@ -1,77 +1,16 @@
use crate::file_data::HasFileData;
use crate::FileData;
use std::{collections::HashMap, fmt::Debug, ops::Deref};
#[cfg(feature = "serialize")]
use crate::SerializedFileData;
use std::fmt::Debug;
use dioxus_core::Event;
pub type FormEvent = Event<FormData>;
/// A form value that may either be a list of values or a single value
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct FormValue(pub Vec<String>);
impl Deref for FormValue {
type Target = [String];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl FormValue {
/// Convenient way to represent Value as slice
pub fn as_slice(&self) -> &[String] {
&self.0
}
/// Return the first value, panicking if there are none
pub fn as_value(&self) -> String {
self.0.first().unwrap().clone()
}
/// Convert into [`Vec<String>`]
pub fn to_vec(self) -> Vec<String> {
self.0.clone()
}
}
impl PartialEq<str> for FormValue {
fn eq(&self, other: &str) -> bool {
self.0.len() == 1 && self.0.first().map(|s| s.as_str()) == Some(other)
}
}
/* DOMEvent: Send + SyncTarget relatedTarget */
pub struct FormData {
inner: Box<dyn HasFormData>,
}
impl<E: HasFormData> From<E> for FormData {
fn from(e: E) -> Self {
Self { inner: Box::new(e) }
}
}
impl PartialEq for FormData {
fn eq(&self, other: &Self) -> bool {
self.value() == other.value() && self.values() == other.values()
}
}
impl Debug for FormData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FormEvent")
.field("value", &self.value())
.field("values", &self.values())
.field("valid", &self.valid())
.finish()
}
}
impl FormData {
/// Create a new form event
pub fn new(event: impl HasFormData + 'static) -> Self {
@@ -104,10 +43,25 @@ impl FormData {
/// Collect all the named form values from the containing form.
///
/// Every input must be named!
pub fn values(&self) -> HashMap<String, FormValue> {
pub fn values(&self) -> Vec<(String, FormValue)> {
self.inner.values()
}
/// Get the first value with the given name
pub fn get_first(&self, name: &str) -> Option<FormValue> {
self.values()
.into_iter()
.find_map(|(k, v)| if k == name { Some(v) } else { None })
}
/// Get all values with the given name
pub fn get(&self, name: &str) -> Vec<FormValue> {
self.values()
.into_iter()
.filter_map(|(k, v)| if k == name { Some(v) } else { None })
.collect()
}
/// Get the files of the form event
pub fn files(&self) -> Vec<FileData> {
self.inner.files()
@@ -125,146 +79,69 @@ impl FormData {
}
}
/// An object that has all the data for a form event
pub trait HasFormData: HasFileData + std::any::Any {
fn value(&self) -> String {
Default::default()
}
fn valid(&self) -> bool {
true
}
fn values(&self) -> HashMap<String, FormValue> {
Default::default()
}
/// return self as Any
fn as_any(&self) -> &dyn std::any::Any;
}
impl FormData {
#[cfg(feature = "serialize")]
/// Parse the values into a struct with one field per value
#[cfg(feature = "serialize")]
pub fn parsed_values<T>(&self) -> Result<T, serde_json::Error>
where
T: serde::de::DeserializeOwned,
{
use crate::SerializedFileData;
let values = &self.values();
let mut map = serde_json::Map::new();
for (key, value) in values {
if value.0.len() == 1 {
map.insert(key.clone(), serde_json::Value::String(value.0[0].clone()));
} else {
let arr = value
.0
.iter()
.cloned()
.map(serde_json::Value::String)
.collect();
map.insert(key.clone(), serde_json::Value::Array(arr));
let entry = map
.entry(key.clone())
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
match value {
FormValue::Text(text) => {
entry
.as_array_mut()
.expect("entry should be an array")
.push(serde_json::Value::String(text.clone()));
}
// we create the serialized variant with no bytes
// SerializedFileData, if given a real path, will read the bytes from disk (synchronously)
FormValue::File(Some(file_data)) => {
let serialized = SerializedFileData {
path: file_data.path().to_owned(),
size: file_data.size(),
last_modified: file_data.last_modified(),
content_type: file_data.content_type(),
contents: None,
};
entry
.as_array_mut()
.expect("entry should be an array")
.push(serde_json::to_value(&serialized).unwrap_or(serde_json::Value::Null));
}
FormValue::File(None) => {
entry
.as_array_mut()
.expect("entry should be an array")
.push(
serde_json::to_value(SerializedFileData::empty())
.unwrap_or(serde_json::Value::Null),
);
}
}
}
serde_json::from_value(serde_json::Value::Object(map))
}
}
#[cfg(feature = "serialize")]
/// A serialized form data object
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFormData {
#[serde(default)]
value: String,
#[serde(default)]
values: HashMap<String, FormValue>,
#[serde(default)]
valid: bool,
#[serde(default)]
files: Vec<SerializedFileData>,
}
#[cfg(feature = "serialize")]
impl SerializedFormData {
/// Create a new serialized form data object
pub fn new(value: String, values: HashMap<String, FormValue>) -> Self {
Self {
value,
values,
valid: true,
files: vec![],
}
}
/// Add files to the serialized form data object
pub fn with_files(mut self, files: Vec<SerializedFileData>) -> Self {
self.files = files;
self
}
/// Create a new serialized form data object from a traditional form data object
pub async fn async_from(data: &FormData) -> Result<Self, dioxus_core::Error> {
Ok(Self {
value: data.value(),
values: data.values(),
valid: data.valid(),
files: {
let mut out = vec![];
for file in data.files() {
out.push(SerializedFileData {
name: file.name(),
size: file.size(),
content_type: file.content_type(),
last_modified: file.last_modified(),
path: file.pathbuf(),
contents: file.read_bytes().await?,
});
// Go through the map and convert single-element arrays to just the element
let map = map
.into_iter()
.map(|(k, v)| match v {
serde_json::Value::Array(arr) if arr.len() == 1 => {
(k, arr.into_iter().next().unwrap())
}
out
},
})
}
_ => (k, v),
})
.collect::<serde_json::Map<String, serde_json::Value>>();
fn from_lossy(data: &FormData) -> Self {
Self {
value: data.value(),
values: data.values(),
valid: data.valid(),
files: vec![],
}
}
}
#[cfg(feature = "serialize")]
impl HasFormData for SerializedFormData {
fn value(&self) -> String {
self.value.clone()
}
fn values(&self) -> HashMap<String, FormValue> {
self.values.clone()
}
fn valid(&self) -> bool {
self.valid
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(feature = "serialize")]
impl HasFileData for SerializedFormData {
fn files(&self) -> Vec<FileData> {
self.files
.iter()
.map(|f| FileData::new(f.clone()))
.collect()
serde_json::from_value(serde_json::Value::Object(map))
}
}
@@ -274,20 +151,186 @@ impl HasFileData for FormData {
}
}
#[cfg(feature = "serialize")]
impl serde::Serialize for FormData {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
SerializedFormData::from_lossy(self).serialize(serializer)
impl<E: HasFormData> From<E> for FormData {
fn from(e: E) -> Self {
Self { inner: Box::new(e) }
}
}
impl PartialEq for FormData {
fn eq(&self, other: &Self) -> bool {
self.value() == other.value() && self.values() == other.values()
}
}
impl Debug for FormData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FormEvent")
.field("value", &self.value())
.field("values", &self.values())
.field("valid", &self.valid())
.finish()
}
}
/// A value in a form, either text or a file
#[derive(Debug, Clone, PartialEq)]
pub enum FormValue {
Text(String),
File(Option<FileData>),
}
impl PartialEq<str> for FormValue {
fn eq(&self, other: &str) -> bool {
match self {
FormValue::Text(s) => s == other,
FormValue::File(_f) => false,
}
}
}
impl PartialEq<&str> for FormValue {
fn eq(&self, other: &&str) -> bool {
match self {
FormValue::Text(s) => s == other,
FormValue::File(_f) => false,
}
}
}
/// An object that has all the data for a form event
pub trait HasFormData: HasFileData + std::any::Any {
fn value(&self) -> String;
fn valid(&self) -> bool;
fn values(&self) -> Vec<(String, FormValue)>;
/// return self as Any
fn as_any(&self) -> &dyn std::any::Any;
}
#[cfg(feature = "serialize")]
impl<'de> serde::Deserialize<'de> for FormData {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = SerializedFormData::deserialize(deserializer)?;
Ok(Self {
inner: Box::new(data),
})
pub use serialize::*;
#[cfg(feature = "serialize")]
mod serialize {
use crate::SerializedFileData;
use super::*;
/// A serialized form data object
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFormData {
#[serde(default)]
pub value: String,
#[serde(default)]
pub values: Vec<SerializedFormObject>,
#[serde(default)]
pub valid: bool,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFormObject {
pub key: String,
pub text: Option<String>,
pub file: Option<SerializedFileData>,
}
#[cfg(feature = "serialize")]
impl SerializedFormData {
/// Create a new serialized form data object
pub fn new(value: String, values: Vec<SerializedFormObject>) -> Self {
Self {
value,
values,
valid: true,
}
}
/// Create a serialized form data object from a form data object
fn from_form_lossy(data: &FormData) -> Self {
if let Some(data) = data.downcast::<SerializedFormData>() {
return data.clone();
}
let values = data
.values()
.iter()
.map(|(key, value)| match value {
FormValue::Text(s) => SerializedFormObject {
key: key.clone(),
text: Some(s.to_string()),
file: None,
},
FormValue::File(f) => SerializedFormObject {
key: key.clone(),
text: None,
file: if let Some(f) = f {
Some(SerializedFileData {
path: f.path(),
size: f.size(),
last_modified: f.last_modified(),
content_type: f.content_type(),
contents: None,
})
} else {
Some(SerializedFileData::empty())
},
},
})
.collect();
Self {
values,
value: data.value(),
valid: data.valid(),
}
}
}
impl HasFormData for SerializedFormData {
fn value(&self) -> String {
self.value.clone()
}
fn values(&self) -> Vec<(String, FormValue)> {
todo!()
}
fn valid(&self) -> bool {
self.valid
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl HasFileData for SerializedFormData {
fn files(&self) -> Vec<FileData> {
self.values
.iter()
.filter_map(|v| v.file.as_ref().map(|f| FileData::new(f.clone())))
.collect()
}
}
impl serde::Serialize for FormData {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
SerializedFormData::from_form_lossy(self).serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for FormData {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = SerializedFormData::deserialize(deserializer)?;
Ok(Self {
inner: Box::new(data),
})
}
}
}
+134 -60
View File
@@ -1,7 +1,6 @@
use std::{path::PathBuf, pin::Pin, prelude::rust_2024::Future};
use bytes::Bytes;
use futures_util::Stream;
use std::{path::PathBuf, pin::Pin, prelude::rust_2024::Future};
#[derive(Clone)]
pub struct FileData {
@@ -49,11 +48,20 @@ impl FileData {
self.inner.inner()
}
pub fn pathbuf(&self) -> PathBuf {
pub fn path(&self) -> PathBuf {
self.inner.path()
}
}
impl PartialEq for FileData {
fn eq(&self, other: &Self) -> bool {
self.name() == other.name()
&& self.size() == other.size()
&& self.last_modified() == other.last_modified()
&& self.path() == other.path()
}
}
pub trait NativeFileData: Send + Sync {
fn name(&self) -> String;
fn size(&self) -> u64;
@@ -82,75 +90,141 @@ impl std::fmt::Debug for FileData {
}
}
impl std::cmp::PartialEq for FileData {
fn eq(&self, other: &Self) -> bool {
self.inner.name() == other.inner.name()
&& self.inner.size() == other.inner.size()
&& self.inner.last_modified() == other.inner.last_modified()
}
}
pub trait HasFileData: std::any::Any {
fn files(&self) -> Vec<FileData>;
}
/// A serializable representation of file data\
#[cfg(feature = "serialize")]
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFileData {
pub name: String,
pub path: PathBuf,
pub size: u64,
pub last_modified: u64,
pub content_type: Option<String>,
pub contents: bytes::Bytes,
}
pub use serialize::*;
#[cfg(feature = "serialize")]
impl NativeFileData for SerializedFileData {
fn name(&self) -> String {
self.name.clone()
mod serialize {
use super::*;
/// A serializable representation of file data
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub struct SerializedFileData {
pub path: PathBuf,
pub size: u64,
pub last_modified: u64,
pub content_type: Option<String>,
pub contents: Option<bytes::Bytes>,
}
fn size(&self) -> u64 {
self.size
impl SerializedFileData {
/// Create a new empty serialized file data object
pub fn empty() -> Self {
Self {
path: PathBuf::new(),
size: 0,
last_modified: 0,
content_type: None,
contents: None,
}
}
}
fn last_modified(&self) -> u64 {
self.last_modified
impl NativeFileData for SerializedFileData {
fn name(&self) -> String {
self.path()
.file_name()
.unwrap()
.to_string_lossy()
.into_owned()
}
fn size(&self) -> u64 {
self.size
}
fn last_modified(&self) -> u64 {
self.last_modified
}
fn read_bytes(
&self,
) -> Pin<Box<dyn Future<Output = Result<Bytes, dioxus_core::Error>> + 'static>> {
let contents = self.contents.clone();
let path = self.path.clone();
Box::pin(async move {
if let Some(contents) = contents {
return Ok(contents);
}
#[cfg(not(target_arch = "wasm32"))]
if path.exists() {
return Ok(std::fs::read(path).map(Bytes::from)?);
}
Err(dioxus_core::Error::msg("File contents not available"))
})
}
fn read_string(
&self,
) -> Pin<Box<dyn Future<Output = Result<String, dioxus_core::Error>> + 'static>> {
let contents = self.contents.clone();
let path = self.path.clone();
Box::pin(async move {
if let Some(contents) = contents {
return Ok(String::from_utf8(contents.to_vec())?);
}
#[cfg(not(target_arch = "wasm32"))]
if path.exists() {
return Ok(std::fs::read_to_string(path)?);
}
Err(dioxus_core::Error::msg("File contents not available"))
})
}
fn byte_stream(
&self,
) -> Pin<
Box<
dyn futures_util::Stream<Item = Result<Bytes, dioxus_core::Error>> + 'static + Send,
>,
> {
let contents = self.contents.clone();
let path = self.path.clone();
Box::pin(futures_util::stream::once(async move {
if let Some(contents) = contents {
return Ok(contents);
}
#[cfg(not(target_arch = "wasm32"))]
if path.exists() {
return Ok(std::fs::read(path).map(Bytes::from)?);
}
Err(dioxus_core::Error::msg("File contents not available"))
}))
}
fn inner(&self) -> &dyn std::any::Any {
self
}
fn path(&self) -> PathBuf {
self.path.clone()
}
fn content_type(&self) -> Option<String> {
self.content_type.clone()
}
}
fn read_bytes(
&self,
) -> Pin<Box<dyn Future<Output = Result<Bytes, dioxus_core::Error>> + 'static>> {
let contents = self.contents.clone();
Box::pin(async move { Ok(contents) })
}
fn read_string(
&self,
) -> Pin<Box<dyn Future<Output = Result<String, dioxus_core::Error>> + 'static>> {
let contents = self.contents.clone();
Box::pin(async move { Ok(String::from_utf8(contents.to_vec())?) })
}
fn byte_stream(
&self,
) -> Pin<Box<dyn futures_util::Stream<Item = Result<Bytes, dioxus_core::Error>> + 'static + Send>>
{
let contents = self.contents.clone();
Box::pin(futures_util::stream::once(async move { Ok(contents) }))
}
fn inner(&self) -> &dyn std::any::Any {
self
}
fn path(&self) -> PathBuf {
self.path.clone()
}
fn content_type(&self) -> Option<String> {
self.content_type.clone()
impl<'de> serde::Deserialize<'de> for FileData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let sfd = SerializedFileData::deserialize(deserializer)?;
Ok(FileData::new(sfd))
}
}
}