nova_forms/components/
file_upload.rs

1use 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// See this for reference: https://github.com/leptos-rs/leptos/blob/96e2b5cba10d2296f262820be19cac9b615b0d23/examples/server_fns_axum/src/app.rs
14
15/// A component that allows users to upload files.
16/// The files are automatically uploaded to the server and stored in the `FileStore`.
17#[component]
18pub fn FileUpload(
19    /// The query string to bind to a list of `FileId`s.
20    #[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                            // renders each item to a view
70                            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/// A unique identifier for a file.
90#[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/// Contains information about a file, but not the file contents itself.
132#[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            //let len = chunk.len();
172            //println!("[{file_name}]\t{len}");
173            //progress::add_chunk(&name, len).await;
174            // in a real server function, you'd do something like saving the file here
175        }
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}