dioxus_fullstack/payloads/
files.rs1use super::*;
2use axum_core::extract::Request;
3use dioxus_fullstack_core::RequestError;
4use dioxus_html::FileData;
5
6#[cfg(feature = "server")]
7use std::path::Path;
8use std::{
9 pin::Pin,
10 task::{Context, Poll},
11};
12
13pub struct FileStream {
21 data: Option<FileData>,
22 name: String,
23 size: Option<u64>,
24 content_type: Option<String>,
25 #[cfg(feature = "server")]
26 server_body: Option<axum_core::body::BodyDataStream>,
27
28 #[allow(clippy::type_complexity)]
30 client_body: Option<Pin<Box<dyn Stream<Item = Result<Bytes, StreamingError>> + Send>>>,
31}
32
33impl FileStream {
34 pub fn file_name(&self) -> &str {
36 &self.name
37 }
38
39 pub fn size(&self) -> Option<u64> {
41 self.size
42 }
43
44 pub fn content_type(&self) -> Option<&str> {
46 self.content_type.as_deref()
47 }
48
49 #[cfg(feature = "server")]
51 pub fn body_mut(&mut self) -> Option<&mut axum_core::body::BodyDataStream> {
52 self.server_body.as_mut()
53 }
54
55 #[cfg(feature = "server")]
57 pub async fn from_path(file: impl AsRef<Path>) -> Result<Self, std::io::Error> {
58 Self::from_path_buf(file.as_ref()).await
59 }
60
61 #[cfg(feature = "server")]
62 async fn from_path_buf(file: &Path) -> Result<Self, std::io::Error> {
63 let metadata = file.metadata()?;
64 let contents = tokio::fs::File::open(&file).await?;
65 let mime = dioxus_asset_resolver::native::get_mime_from_ext(
66 file.extension().and_then(|s| s.to_str()),
67 );
68 let size = metadata.len();
69 let name = file
70 .file_name()
71 .and_then(|s| s.to_str())
72 .unwrap_or("file")
73 .to_string();
74
75 let reader_stream = tokio_util::io::ReaderStream::new(contents);
77
78 let body = axum_core::body::Body::from_stream(reader_stream).into_data_stream();
81
82 Ok(Self {
83 data: None,
84 name,
85 size: Some(size),
86 content_type: Some(mime.to_string()),
87 #[cfg(feature = "server")]
88 server_body: Some(body),
89 client_body: None,
90 })
91 }
92
93 #[cfg(feature = "server")]
98 pub fn from_raw(
99 name: String,
100 size: Option<u64>,
101 content_type: String,
102 body: axum_core::body::BodyDataStream,
103 ) -> Self {
104 Self {
105 data: None,
106 name,
107 size,
108 content_type: Some(content_type),
109 #[cfg(feature = "server")]
110 server_body: Some(body),
111 client_body: None,
112 }
113 }
114}
115
116impl IntoRequest for FileStream {
117 #[allow(unreachable_code)]
118 fn into_request(
119 self,
120 #[allow(unused_mut)] mut builder: ClientRequest,
121 ) -> impl Future<Output = ClientResult> + 'static {
122 async move {
123 let Some(file_data) = self.data else {
124 return Err(RequestError::Request(
125 "FileStream has no data to send".into(),
126 ));
127 };
128
129 #[cfg(feature = "web")]
130 if cfg!(target_arch = "wasm32") {
131 use js_sys::escape;
132 use wasm_bindgen::JsCast;
133
134 let as_file = file_data.inner().downcast_ref::<web_sys::File>().unwrap();
135 let as_blob = as_file.dyn_ref::<web_sys::Blob>().unwrap();
136 let content_type = as_blob.type_();
137 let content_length = as_blob.size().to_string();
138 let name = as_file.name();
139
140 return builder
144 .header("Content-Type", content_type)?
145 .header("Content-Length", content_length.clone())?
146 .header("X-Content-Size", content_length)?
147 .header(
148 "Content-Disposition",
149 format!("attachment; filename=\"{}\"", escape(&name)),
150 )?
151 .send_js_value(as_blob.clone().into())
152 .await;
153 }
154
155 #[cfg(not(target_arch = "wasm32"))]
156 {
157 use std::ascii::escape_default;
158
159 use futures::TryStreamExt;
160
161 let content_type = self
162 .content_type
163 .unwrap_or_else(|| "application/octet-stream".to_string());
164 let content_length = self.size.map(|s| s.to_string());
165 let name = self.name;
166 let stream = file_data.byte_stream().map_err(|_| StreamingError::Failed);
167
168 let mut chars = vec![];
170 for byte in name.chars() {
171 chars.extend(escape_default(byte as u8));
172 }
173 let filename = String::from_utf8(chars).map_err(|_| {
174 RequestError::Request(
175 "Failed to escape filename for Content-Disposition".into(),
176 )
177 });
178
179 if let Some(length) = content_length {
180 builder = builder.header("Content-Length", length)?;
181 }
182
183 if let Ok(filename) = filename {
184 builder = builder.header(
185 "Content-Disposition",
186 format!("attachment; filename=\"{}\"", filename),
187 )?;
188 }
189
190 return builder
191 .header("Content-Type", content_type)?
192 .send_body_stream(stream)
193 .await;
194 }
195
196 unimplemented!("FileStream::into_request is only implemented for web targets");
197 }
198 }
199}
200
201impl<S> FromRequest<S> for FileStream {
202 type Rejection = ServerFnError;
203
204 fn from_request(
205 req: Request,
206 _: &S,
207 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
208 async move {
209 tracing::info!("Extracting FileUpload from request: {:?}", req);
210
211 let disposition = req.headers().get("Content-Disposition");
212 let filename = match disposition.map(|s| s.to_str()) {
213 Some(Ok(dis)) => {
214 let content = content_disposition::parse_content_disposition(dis);
215 content
216 .filename_full()
217 .unwrap_or_else(|| "file".to_string())
218 }
219 _ => "file".to_string(),
220 };
221
222 let size = req
225 .headers()
226 .get("X-Content-Size")
227 .and_then(|s| s.to_str().ok())
228 .and_then(|s| s.parse::<u64>().ok());
229
230 let content_type = req
231 .headers()
232 .get("Content-Type")
233 .and_then(|s| s.to_str().ok())
234 .map(|s| s.to_string());
235
236 Ok(FileStream {
237 data: None,
238 name: filename,
239 content_type,
240 size,
241 client_body: None,
242 #[cfg(feature = "server")]
243 server_body: Some(req.into_body().into_data_stream()),
244 })
245 }
246 }
247}
248
249impl FromResponse for FileStream {
250 fn from_response(res: ClientResponse) -> impl Future<Output = Result<Self, ServerFnError>> {
251 async move {
252 if !res.status().is_success() {
254 let status_code = res.status().as_u16();
255 let canonical_reason = res
256 .status()
257 .canonical_reason()
258 .unwrap_or("Unknown error")
259 .to_string();
260 let bytes = res.bytes().await.unwrap_or_default();
261 let message = String::from_utf8(bytes.to_vec()).unwrap_or(canonical_reason);
262
263 return Err(ServerFnError::ServerError {
264 message,
265 code: status_code,
266 details: None,
267 });
268 }
269
270 let name = res
272 .headers()
273 .get("Content-Disposition")
274 .and_then(|h| h.to_str().ok())
275 .and_then(|dis| {
276 let cd = content_disposition::parse_content_disposition(dis);
277 cd.filename().map(|(name, _)| name.to_string())
278 })
279 .unwrap_or_else(|| "file".to_string());
280
281 let content_type = res
283 .headers()
284 .get("Content-Type")
285 .and_then(|h| h.to_str().ok())
286 .map(|s| s.to_string());
287
288 let size = res.content_length().or_else(|| {
290 res.headers()
291 .get("X-Content-Size")
292 .and_then(|h| h.to_str().ok())
293 .and_then(|s| s.parse::<u64>().ok())
294 });
295
296 Ok(Self {
297 data: None,
298 name,
299 size,
300 content_type,
301 client_body: Some(Box::pin(res.bytes_stream())),
302 #[cfg(feature = "server")]
303 server_body: None,
304 })
305 }
306 }
307}
308
309#[cfg(feature = "server")]
310impl IntoResponse for FileStream {
311 fn into_response(self) -> axum::response::Response {
312 use axum::body::Body;
313
314 let Some(body) = self.server_body else {
315 use dioxus_fullstack_core::HttpError;
316 return HttpError::new(http::StatusCode::BAD_REQUEST, "FileStream has no body")
317 .into_response();
318 };
319
320 let mut res = axum::response::Response::new(Body::from_stream(body));
321
322 if let Some(content_type) = &self.content_type {
324 res.headers_mut()
325 .insert("Content-Type", content_type.parse().unwrap());
326 }
327 if let Some(size) = self.size {
328 res.headers_mut()
329 .insert("Content-Length", size.to_string().parse().unwrap());
330 }
331 res.headers_mut().insert(
332 "Content-Disposition",
333 format!("attachment; filename=\"{}\"", self.name)
334 .parse()
335 .unwrap(),
336 );
337
338 res
339 }
340}
341
342impl From<FileData> for FileStream {
343 fn from(value: FileData) -> Self {
344 Self {
345 name: value.name().to_string(),
346 content_type: value.content_type().map(|s| s.to_string()),
347 size: Some(value.size()),
348 data: Some(value),
349 client_body: None,
350 #[cfg(feature = "server")]
351 server_body: None,
352 }
353 }
354}
355
356impl Stream for FileStream {
357 type Item = Result<Bytes, StreamingError>;
358
359 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
360 #[cfg(feature = "server")]
362 if let Some(body) = self.server_body.as_mut() {
363 return Pin::new(body)
364 .poll_next(cx)
365 .map_err(|_| StreamingError::Failed);
366 }
367
368 if let Some(body) = self.client_body.as_mut() {
370 return body.as_mut().poll_next(cx);
371 }
372
373 Poll::Ready(None)
375 }
376}
377
378impl std::fmt::Debug for FileStream {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 f.debug_struct("FileStream")
381 .field("name", &self.name)
382 .field("size", &self.size)
383 .field("content_type", &self.content_type)
384 .finish()
385 }
386}