nova_forms/components/
file_upload.rs1use std::{fmt::Display, str::FromStr};
2
3use leptos::*;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::{Group, GroupContext, Icon, QueryStringPart};
8use serde::de::Error;
9use server_fn::codec::{MultipartData, MultipartFormData};
10use web_sys::{wasm_bindgen::JsCast, FormData, HtmlInputElement};
11
12
13#[component]
18pub fn FileUpload(
19 #[prop(into)] bind: QueryStringPart
21) -> impl IntoView {
22 let (file_info, set_file_info) = create_signal(Vec::new());
23
24 let on_input = move |ev: web_sys::Event| {
25 let target = ev
26 .target()
27 .expect("target must exist")
28 .unchecked_into::<HtmlInputElement>();
29
30 if let Some(files) = target.files() {
31 let form_data: FormData = FormData::new().expect("can create form data");
32
33 for i in 0..files.length() {
34 let file = files.get(i).expect("file must exist");
35 let file_name = file.name();
36
37 form_data
38 .append_with_blob_and_filename(&file_name, &file, &file_name)
39 .expect("appending file to form data must be successful");
40 }
41
42 spawn_local(async move {
43 let mut new_file_infos = upload_file(form_data.into())
44 .await
45 .expect("couldn't upload file");
46
47 set_file_info.update(|file_info| {
48 file_info.append(&mut new_file_infos);
49 });
50 });
51 }
52 };
53
54 view! {
55 <Group bind=bind>
56 {
57 let group = expect_context::<GroupContext>();
58 let qs = group.qs();
59
60 view! {
61 <label class="button icon-button" for=qs.to_string()>
62 <input id=qs.to_string() type="file" class="sr-hidden" on:input=on_input disabled=cfg!(feature = "csr") />
63 <Icon label="Upload" icon="upload" />
64 </label>
65 <ul>
66 <For
67 each=move || file_info.get().into_iter().enumerate()
68 key=|(_, (file_id, _))| file_id.clone()
69 children=move |(i, (file_id, file_info))| {
71 let qs = qs.add_index(i);
72
73 view! {
74 <li>
75 {format!("{}", file_info.file_name)}
76 <input type="hidden" name=qs value=file_id.to_string() />
77 </li>
78 }
79 }
80 />
81 </ul>
82 }
83 }
84 </Group>
85
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub struct FileId(Uuid);
92
93#[cfg(feature = "ssr")]
94impl FileId {
95 pub(crate) fn new() -> Self {
96 FileId(Uuid::new_v4())
97 }
98}
99
100impl Display for FileId {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 write!(f, "file_id_{}", self.0)
103 }
104}
105
106impl Serialize for FileId {
107 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108 where
109 S: serde::Serializer,
110 {
111 serializer.serialize_str(&format!("file_id_{}", self.0))
112 }
113}
114
115impl<'de> Deserialize<'de> for FileId {
116 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117 where
118 D: serde::Deserializer<'de>,
119 {
120 let mut value = String::deserialize(deserializer)?;
121 if !value.starts_with("file_id_") {
122 return Err(D::Error::custom("prefix mismatch"));
123 }
124 match Uuid::from_str(&value.split_off(8)) {
125 Ok(uuid) => Ok(FileId(uuid)),
126 Err(_) => Err(D::Error::custom("invalid uuid")),
127 }
128 }
129}
130
131#[derive(Clone, Serialize, Deserialize, Debug)]
133pub struct FileInfo {
134 file_name: String,
135 content_type: Option<String>,
136}
137
138impl FileInfo {
139 pub fn new(file_name: String, content_type: Option<String>) -> Self {
140 FileInfo {
141 file_name,
142 content_type,
143 }
144 }
145
146 pub fn file_name(&self) -> &str {
147 &self.file_name
148 }
149
150 pub fn content_type(&self) -> Option<&str> {
151 self.content_type.as_ref().map(|s| s.as_str())
152 }
153}
154
155#[server(input = MultipartFormData)]
156async fn upload_file(data: MultipartData) -> Result<Vec<(FileId, FileInfo)>, ServerFnError> {
157 use crate::FileStore;
158
159 let mut data = data.into_inner().unwrap();
160 let mut file_infos = Vec::new();
161
162 while let Ok(Some(mut field)) = data.next_field().await {
163 let content_type = field.content_type().map(|mime| mime.to_string());
164 let file_name = field.file_name().expect("no filename on field").to_string();
165 let file_info = FileInfo::new(file_name, content_type);
166
167 let mut data = Vec::new();
168
169 while let Ok(Some(chunk)) = field.chunk().await {
170 data.extend_from_slice(&chunk);
171 }
176
177 let file_store = expect_context::<FileStore>();
178 let file_id = file_store.insert(file_info.clone(), data).await?;
179
180 println!(
181 "inserted file {} into database with id {}",
182 file_info.file_name(),
183 file_id
184 );
185
186 file_infos.push((file_id, file_info));
187 }
188
189 Ok(file_infos)
190}