Skip to content

Commit 94730c6

Browse files
committed
feat: stream upload
1 parent a1f0bb8 commit 94730c6

File tree

4 files changed

+49
-64
lines changed

4 files changed

+49
-64
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/file-explorer-ui/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ path = "src/bin/main.rs"
1818
anyhow = { workspace = true }
1919
chrono = { workspace = true, features = ["serde"] }
2020
gloo = { workspace = true }
21-
gloo-file = { workspace = true }
21+
gloo-file = { workspace = true, features = ["futures"] }
2222
leptos = { workspace = true, features = ["csr"] }
2323
leptos_meta = { workspace = true, features = ["csr"] }
2424
leptos_router = { workspace = true, features = ["csr"] }

src/file-explorer-ui/src/api/mod.rs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
pub mod proto;
22

3-
use anyhow::{Error, Result};
3+
use anyhow::Result;
44
use gloo::utils::window;
5-
use reqwest::{header::CONTENT_TYPE, Url};
6-
use web_sys::{File, FormData};
5+
use reqwest::{header::CONTENT_TYPE, Client, Url};
6+
use web_sys::File;
77

88
use self::proto::DirectoryIndex;
99

@@ -32,17 +32,27 @@ impl Api {
3232
}
3333

3434
pub async fn upload(&self, file: File) -> Result<()> {
35+
let file_name = file.name();
36+
let reader = gloo_file::futures::read_as_bytes(&file.into()).await?;
37+
3538
let url = self.base_url.join("api/v1")?;
36-
let form_data = FormData::new()
37-
.map_err(|err| Error::msg(format!("Failed to create FormData: {:?}", err)))?;
38-
form_data
39-
.append_with_blob("file", &file)
40-
.map_err(|err| Error::msg(format!("Failed to append file to FormData: {:?}", err)))?;
41-
42-
gloo::net::http::Request::post(url.as_ref())
43-
.body(form_data)?
39+
let _response = Client::new()
40+
.post(url.as_ref())
41+
.header("Content-Type", "application/octet-stream")
42+
.header("X-File-Name", file_name)
43+
.body(reader)
4444
.send()
4545
.await?;
46+
// let form_data = FormData::new()
47+
// .map_err(|err| Error::msg(format!("Failed to create FormData: {:?}", err)))?;
48+
// form_data
49+
// .append_with_blob("file", &file)
50+
// .map_err(|err| Error::msg(format!("Failed to append file to FormData: {:?}", err)))?;
51+
52+
// gloo::net::http::Request::post(url.as_ref())
53+
// .body(form_data)?
54+
// .send()
55+
// .await?;
4656

4757
Ok(())
4858
}

src/http-server/src/handler/file_explorer/mod.rs

Lines changed: 26 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ use std::path::{Component, Path, PathBuf};
99
use anyhow::{Context, Result};
1010
use async_trait::async_trait;
1111
use bytes::Bytes;
12+
use futures::StreamExt;
13+
use http::HeaderName;
1214
use http::{HeaderValue, Method, Response, StatusCode, Uri, header::CONTENT_TYPE, request::Parts};
1315
use http_body_util::{BodyExt, Full};
14-
use multer::Multipart;
16+
use hyper::body::Incoming;
1517
use percent_encoding::{percent_decode_str, utf8_percent_encode};
1618
use proto::{DirectoryEntry, DirectoryIndex, EntryType, Sort};
1719
use rust_embed::Embed;
20+
use tokio::fs::File;
1821
use tokio::io::AsyncWriteExt;
1922

2023
use crate::handler::Handler;
@@ -23,6 +26,9 @@ use crate::server::{HttpRequest, HttpResponse};
2326
use self::proto::BreadcrumbItem;
2427
use self::utils::{PERCENT_ENCODE_SET, decode_uri, encode_uri};
2528

29+
const X_FILE_NAME: &str = "x-file-name";
30+
const X_FILE_NAME_HTTP_HEADER: HeaderName = HeaderName::from_static(X_FILE_NAME);
31+
2632
#[derive(Embed)]
2733
#[folder = "./ui"]
2834
struct FileExplorerAssets;
@@ -40,8 +46,8 @@ impl FileExplorer {
4046
}
4147
}
4248

43-
async fn handle_api(&self, parts: Parts, body: Bytes) -> Result<HttpResponse> {
44-
let path = Self::parse_req_uri(parts.uri.clone()).unwrap();
49+
async fn handle_api(&self, parts: Parts, body: Incoming) -> Result<HttpResponse> {
50+
let path = Self::parse_req_uri(parts.uri.clone())?;
4551

4652
match parts.method {
4753
Method::GET => match self.file_explorer.peek(path).await {
@@ -75,34 +81,13 @@ impl FileExplorer {
7581
Ok(Response::new(Full::new(Bytes::from(message))))
7682
}
7783
},
78-
Method::POST => {
79-
self.handle_file_upload(parts, body).await?;
80-
Ok(Response::new(Full::new(Bytes::from(
81-
"POST method is not supported",
82-
))))
83-
}
84+
Method::POST => self.handle_file_upload(parts, body).await,
8485
_ => Ok(Response::new(Full::new(Bytes::from("Unsupported method")))),
8586
}
8687
}
8788

88-
async fn handle_file_upload(&self, parts: Parts, body: Bytes) -> Result<HttpResponse> {
89-
// Extract the `multipart/form-data` boundary from the headers.
90-
let mb_boundary = parts
91-
.headers
92-
.get(CONTENT_TYPE)
93-
.and_then(|ct| ct.to_str().ok())
94-
.and_then(|ct| multer::parse_boundary(ct).ok());
95-
96-
// Send `BAD_REQUEST` status if the content-type is not multipart/form-data.
97-
let Some(boundary) = mb_boundary else {
98-
return Ok(Response::builder()
99-
.status(StatusCode::BAD_REQUEST)
100-
.body(Full::from("BAD REQUEST"))
101-
.unwrap());
102-
};
103-
104-
// Process the multipart e.g. you can store them in files.
105-
if let Err(err) = self.process_multipart(body, boundary).await {
89+
async fn handle_file_upload(&self, parts: Parts, body: Incoming) -> Result<HttpResponse> {
90+
if let Err(err) = self.process_multipart(body, parts).await {
10691
return Ok(Response::builder()
10792
.status(StatusCode::INTERNAL_SERVER_ERROR)
10893
.body(Full::from(format!("INTERNAL SERVER ERROR: {err}")))
@@ -112,31 +97,21 @@ impl FileExplorer {
11297
Ok(Response::new(Full::from("Success")))
11398
}
11499

115-
async fn process_multipart(&self, bytes: Bytes, boundary: String) -> Result<()> {
116-
let cursor = std::io::Cursor::new(bytes);
117-
let bytes_stream = tokio_util::io::ReaderStream::new(cursor);
118-
let mut multipart = Multipart::new(bytes_stream, boundary);
119-
120-
while let Some(mut field) = multipart.next_field().await? {
121-
let name = field.name();
122-
let file_name = field
123-
.file_name()
124-
.to_owned()
125-
.context("No file name available in form file.")?;
126-
let content_type = field.content_type();
127-
let mut file = tokio::fs::File::create(file_name)
100+
async fn process_multipart(&self, bytes: Incoming, parts: Parts) -> Result<()> {
101+
let file_name = parts
102+
.headers
103+
.get(X_FILE_NAME_HTTP_HEADER)
104+
.and_then(|hv| hv.to_str().ok())
105+
.context(format!("Missing '{X_FILE_NAME}' header"))?;
106+
let mut stream = bytes.into_data_stream();
107+
let mut file = File::create(file_name)
108+
.await
109+
.context("Failed to create target file for upload.")?;
110+
111+
while let Some(Ok(bytes)) = stream.next().await {
112+
file.write_all(&bytes)
128113
.await
129-
.context("Failed to create target file for upload.")?;
130-
131-
println!(
132-
"\n\nName: {name:?}, FileName: {file_name:?}, Content-Type: {content_type:?}\n\n"
133-
);
134-
135-
while let Some(field_chunk) = field.chunk().await? {
136-
file.write_all(&field_chunk)
137-
.await
138-
.context("Failed to write bytes to file")?;
139-
}
114+
.context("Failed to write bytes to file")?;
140115
}
141116

142117
Ok(())
@@ -293,7 +268,6 @@ impl FileExplorer {
293268
impl Handler for FileExplorer {
294269
async fn handle(&self, req: HttpRequest) -> Result<HttpResponse> {
295270
let (parts, body) = req.into_parts();
296-
let body = body.collect().await.unwrap().to_bytes();
297271

298272
if parts.uri.path().starts_with("/api/v1") {
299273
return self.handle_api(parts, body).await;

0 commit comments

Comments
 (0)