nova_forms/components/
file_upload.rsuse std::{fmt::Display, str::FromStr};
use leptos::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{Group, GroupContext, Icon, QueryString};
use serde::de::Error;
use server_fn::codec::{MultipartData, MultipartFormData};
use web_sys::{wasm_bindgen::JsCast, FormData, HtmlInputElement};
#[component]
pub fn FileUpload(
#[prop(into)] bind: QueryString
) -> impl IntoView {
let (file_info, set_file_info) = create_signal(Vec::new());
let on_input = move |ev: web_sys::Event| {
let target = ev
.target()
.expect("target must exist")
.unchecked_into::<HtmlInputElement>();
if let Some(files) = target.files() {
let form_data: FormData = FormData::new().expect("can create form data");
for i in 0..files.length() {
let file = files.get(i).expect("file must exist");
let file_name = file.name();
form_data
.append_with_blob_and_filename(&file_name, &file, &file_name)
.expect("appending file to form data must be successful");
}
spawn_local(async move {
let mut new_file_infos = upload_file(form_data.into())
.await
.expect("couldn't upload file");
set_file_info.update(|file_info| {
file_info.append(&mut new_file_infos);
});
});
}
};
view! {
<Group bind=bind>
{
let group = expect_context::<GroupContext>();
let qs = group.qs();
view! {
<label class="button icon-button" for=qs.to_string()>
<input id=qs.to_string() type="file" class="sr-hidden" on:input=on_input disabled=cfg!(feature = "csr") />
<Icon label="Upload" icon="upload" />
</label>
<ul>
<For
each=move || file_info.get().into_iter().enumerate()
key=|(_, (file_id, _))| file_id.clone()
children=move |(i, (file_id, file_info))| {
let qs = qs.add_index(i);
view! {
<li>
{format!("{}", file_info.file_name)}
<input type="hidden" name=qs value=file_id.to_string() />
</li>
}
}
/>
</ul>
}
}
</Group>
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileId(Uuid);
#[cfg(feature = "ssr")]
impl FileId {
pub(crate) fn new() -> Self {
FileId(Uuid::new_v4())
}
}
impl Display for FileId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "file_id_{}", self.0)
}
}
impl Serialize for FileId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("file_id_{}", self.0))
}
}
impl<'de> Deserialize<'de> for FileId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut value = String::deserialize(deserializer)?;
if !value.starts_with("file_id_") {
return Err(D::Error::custom("prefix mismatch"));
}
match Uuid::from_str(&value.split_off(8)) {
Ok(uuid) => Ok(FileId(uuid)),
Err(_) => Err(D::Error::custom("invalid uuid")),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct FileInfo {
file_name: String,
content_type: Option<String>,
}
impl FileInfo {
pub fn new(file_name: String, content_type: Option<String>) -> Self {
FileInfo {
file_name,
content_type,
}
}
pub fn file_name(&self) -> &str {
&self.file_name
}
pub fn content_type(&self) -> Option<&str> {
self.content_type.as_ref().map(|s| s.as_str())
}
}
#[server(input = MultipartFormData)]
async fn upload_file(data: MultipartData) -> Result<Vec<(FileId, FileInfo)>, ServerFnError> {
use crate::FileStore;
let mut data = data.into_inner().unwrap();
let mut file_infos = Vec::new();
while let Ok(Some(mut field)) = data.next_field().await {
let content_type = field.content_type().map(|mime| mime.to_string());
let file_name = field.file_name().expect("no filename on field").to_string();
let file_info = FileInfo::new(file_name, content_type);
let mut data = Vec::new();
while let Ok(Some(chunk)) = field.chunk().await {
data.extend_from_slice(&chunk);
}
let file_store = expect_context::<FileStore>();
let file_id = file_store.insert(file_info.clone(), data).await?;
println!(
"inserted file {} into database with id {}",
file_info.file_name(),
file_id
);
file_infos.push((file_id, file_info));
}
Ok(file_infos)
}