Many web applications, especially user-facing ones, provide an interface for uploading files. This can range from simple files, such as an avatar picture, to complex items such as ultra-secure, cryptographically signed contracts.
In any case, for many web services, this is a must-have — just as critical as the ability to download or access these files again once they are uploaded.
In this guide, we’ll demonstrate how to implement file upload and download in a Rust web application. We’ll use the warp web framework, but the basics will mostly apply to other async web frameworks.
There’s a nifty crate for dealing with async multipart streams called mpart-async, which streams incoming files instead of loading them into memory completely, as warp does at this time (there is an issue for this).
However, for the purpose of this tutorial, we’ll use warp’s built-in method for handling multipart requests. If your use case involves dealing with huge files you need to stream through or validate before loading into memory, the aforementioned mpart-async
crate is a sensible way to go for now.
We’ll build an example web application that enables users to upload .pdf
and .png
files up to 5 MB. Each file will be placed in the local ./files
folder with a randomly generated name and made available for download using the GET /files/$filename
endpoint.
To follow along, all you need is a reasonably recent Rust installation (1.39+) and a tool to send HTTP requests, such as cURL.
First, create a new Rust project.
cargo new rust-upload-download-example cd rust-upload-download-example
Next, edit the Cargo.toml
file and add the dependencies you’ll need.
[dependencies] tokio = { version = "0.2.21", features = ["macros", "rt-threaded", "fs"] } warp = "0.2.3" uuid = { version = "0.8", features = ["v4"] } futures = { version = "=0.3.5", default-features = false } bytes = "0.5.6"
We’re using warp to build the web service, which uses Tokio underneath. The other dependencies are used to handle the file uploads with warp. With uuid
, we’ll create unique names for the uploaded files and the futures and bytes crates will help us deal with the incoming file stream.
Let’s start by creating a basic Warp web application that lets users download files from a local folder.
#[tokio::main] async fn main() { let download_route = warp::path("files").and(warp::fs::dir("./files/")); let router = download_route.recover(handle_rejection); println!("Server started at localhost:8080"); warp::serve(router).run(([0, 0, 0, 0], 8080)).await; } async fn handle_rejection(err: Rejection) -> std::result::Result<impl Reply, Infallible> { let (code, message) = if err.is_not_found() { (StatusCode::NOT_FOUND, "Not Found".to_string()) } else if err.find::<warp::reject::PayloadTooLarge>().is_some() { (StatusCode::BAD_REQUEST, "Payload too large".to_string()) } else { eprintln!("unhandled error: {:?}", err); ( StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error".to_string(), ) }; Ok(warp::reply::with_status(message, code)) }
In this code snippet, we define a download_route
at GET /files
, which, using Warp’s fs::dir
filter, serves files from the given path. That’s all we have to do to implement file downloads.
Of course, depending on your specific use case and the size of the files you’re dealing with — or if you have security concerns — the logic for downloading files will be more complex than this. But it’s adequate for our example.
Next, let’s look at the upload
route definition.
// in main let upload_route = warp::path("upload") .and(warp::post()) .and(warp::multipart::form().max_length(5_000_000)) .and_then(upload); let router = upload_route.or(download_route).recover(handle_rejection);
The upload route is a POST
endpoint. We use Warp’s multipart::form()
filter to pass through multipart requests.
We can also define a max length here. You might have noticed in the handle_rejection
error handling method that we explicitly handle the PayloadTooLarge
error. This error is triggered when a payload exceeds this limit.
Finally, let’s check out the upload
handler. This is the core piece of this small application, and it’s a bit more complex, so we’re going to go over it step by step.
async fn upload(form: FormData) -> Result<impl Reply, Rejection> { let parts: Vec<Part> = form.try_collect().await.map_err(|e| { eprintln!("form error: {}", e); warp::reject::reject() })?;
We immediately see the FormData
in the function signature. This is actually a warp::multipart::FormData
, which is a stream of multipart Part
elements. Since we’re dealing with a futures::Stream
, we can use the TryStreamExt
trait for some helpers. In this case, we use the try_collect
function to gather the whole stream into a collection asynchronously, logging the error if this fails.
To understand the next part, let’s look at an example request we might send to this server using cURL.
curl --location --request POST 'http://localhost:8080/upload' \ --header 'Content-Type: multipart/form-data' \ --form 'file=@/home/somewhere/picture.png'
As you can see in the --form
option, we define our file with the name file
. We could also add additional parameters, such as a file name or some additional metadata.
The next step is to iterate over our Part
s collected above to see if there is a file
field.
for p in parts { if p.name() == "file" { let content_type = p.content_type(); ...
We iterate over the parts we got and, if one is called file
, we’ll assume it’s a file. Now we need to make sure it has the correct content type — in this case, PDF or PNG — and process it further.
let file_ending; match content_type { Some(file_type) => match file_type { "application/pdf" => { file_ending = "pdf"; } "image/png" => { file_ending = "png"; } v => { eprintln!("invalid file type found: {}", v); return Err(warp::reject::reject()); } }, None => { eprintln!("file type could not be determined"); return Err(warp::reject::reject()); } }
We can use the .content_type()
method of Part
to check the actual file type. rust-mime-sniffer is another useful crate you could use to detect the file type.
We also set the file_ending
based on the file type, so we can append it to the file we want to create later on.
If the file is of an unknown type or doesn’t have a known type, we log the error and return.
The next step is to convert the Part
into a byte vector we can actually write to disk.
let value = p .stream() .try_fold(Vec::new(), |mut vec, data| { vec.put(data); async move { Ok(vec) } }) .await .map_err(|e| { eprintln!("reading file error: {}", e); warp::reject::reject() })?;
At this point, we can use Part
‘s .stream()
or .data()
methods to get to the underlying bytes::Buf
containing the data.
In this example, we turn the whole part into a stream again and, using another helper from TryStreamExt
called .try_fold()
, concatenate all the buffers to one Vec<u8>
with all our data.
This try_fold
is essentially similar to any other .reduce()
or .fold()
function, except that it’s asynchronous. We define an initial value (an empty vector) and then add each piece of data to it.
If any of this fails, we again log the error and return.
At this point, we parsed and validated the incoming file and have the data ready to be written to disk, which is the final step.
let file_name = format!("./files/{}.{}", Uuid::new_v4().to_string(), file_ending); tokio::fs::write(&file_name, value).await.map_err(|e| { eprint!("error writing file: {}", e); warp::reject::reject() })?; println!("created file: {}", file_name); } } Ok("success") }
First, we create a randomly generated, unique file name using the Uuid
crate and add the above-calculated file_ending
. Then, using Tokio’s fs::write
, which is an asynchronous equivalent to std::fs::write
, we write the data to a file with the generated file name. If it works out, we log the file name and return a success message to the caller.
Let’s try it using the following commands.
# first, upload some png curl --location --request POST 'http://localhost:8080/upload' \ --header 'Content-Type: multipart/form-data' \ --form 'file=@/home/somewhere/picture.png' # check the logs for the file name, or go into the ./files folder created file: ./files/7d678724-9480-489e-8a33-57e1ae5adb4d.png # request the file and pipe it to a new png curl http://localhost:8080/files/7d678724-9480-489e-8a33-57e1ae5adb4d.png > new_picture.png
It works! Fantastic. And all that in less than a hundred lines of Rust code with some very basic error handling and input validation using just a couple of crates.
You can find the full example code at GitHub.
Efficiently and robustly handling file uploads in a web service is not an easy task, but the Rust ecosystem provides all the tools to do so, even with options to asynchronously stream files for additional speed and flexibility.
The above example is a starting point for a real-world implementation. While it’s used in production systems, there are more things to consider. That said, the fundamentals are already there in the Rust web ecosystem to create great upload and download experiences for your users.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
12 Replies to "File upload and download in Rust"
Thanks for the interesting tutorial. I expanded it slightly by adding cors. This works fine for the download path, but it seems that its not working (even though compiling) for the upload part. The server throws an Internal Server Error about unknown header (“Content-Type”) event thought this is in the cors. Any idea why this could be?
let download_cors = … (similar to upload cors)
let upload_cors = warp::cors()
.allow_any_origin().allow_headers(vec![“User-Agent”, “Sec-Fetch-Mode”, “Referer”, “Origin”, “Access-Control-Request-Method”, “Access-Control-Request-Headers”, “Content-Type”, “Accept”, “Authorization”, “type”])
.allow_methods(vec![“GET”, “POST”, “DELETE”, “OPTIONS”]);
let upload_route = warp::path(“upload”).and(warp::post()).and(warp::multipart::form().max_length(5_000_000)).and_then(upload).with(upload_cors);
let download_route = warp::path(“files”).and(warp::fs::dir(“./files/”)).with(download_cors);
Hey!
Author here – thanks for the feedback, first and foremost.
I added your CORS config and for me it works. How are you calling it? I tested it with cURL and Postman and in both cases it worked just fine.
One thing I sometimes do to debug the errors, is add log statements (printlns for example) on top of the
async fn handle_rejection(err: Rejection) -> std::result::Result {
function, to see what rejection is thrown exactly and then I check the warp docs to see why that might happen.
Dear Mario
Thanks for the reply and sorry for the slow follow up. I figured out what was wrong. I was calling from a react app and the call actually did a pre-flight check using an options request, which of course was not implemented. Thus the call failed.
Florian
It seems that this example does not go into the details about how to properly structure a file and the functions within.
I myself care only about the part that claims to show how to send multipart form data to ANY REST API route that is willing to ingest it.
Code that is implementing upload function seems to raise errors and fail to compile.
the `?` operator can only be used in an async function that returns `Result` or `Option` (or another type that implements `Try`)
the trait `Try` is not implemented for `()`
required by `from_error’
It is not clear on how to properly use the code that was shown.
Hey Andzej!
I’m the author of the post. I’m not 100% sure what exact issue you’re encountering.
In terms of failure to compile – the code in the repository compiles just fine for me. Which version of Rust are you using?
In terms of structuring the application, you’re right, the goal of this post was not to provide a holistic overview of application structuring, but rather to provide a quick setup for uploading and downloading files using Warp and Rust.
What’s the exact errors you encounter, or which parts aren’t clear to you? I think it would be best if you opened a ticket at https://github.com/zupzup/warp-upload-download-example with your questions and I’ll be happy to help you!
kind regards,
Mario
Hi! Thanks for writing this article!
I would like to dynamically map the request path to a file path (by querying a database), but it seems to me that warp’s serve dir/fs function cannot be reused for that. How would you implement that?
Thanks!
Hey!
So, if I understand you correctly, you’d want to map something like https://example.com/files/some/interesting/file to a file, where you get the actual path of the file via a database query based on the path.
So somewhere in the DB, there is an entry like “path: /some/interesting/file” => “actualpath: /opt/files/24324.jpg/”.
In that case, I think you can only do this “manually”, by having a handler on “/files”, which then parses the path, does the query, if it finds anything, read the file (e.g. using tokio::fs) and returns the file to the user with the appropariate HTTP headers.
I’m not 100% sure I understand you correctly, but it sounds like something you’d have to implement step by step.
Hope this helps!
Mario
Thanks for the nice article, I believe there is a small typo in the block:
created file: ./files/7d678724-9480-489e-8a33-57e1ae5adb4d.png
# request the file and pipe it to a new png
curl http://localhost:8080/files/7d678724-9480-489e-8a33-57e1ae5adb4d.pdf (here I expected to see PNG ext)
Thanks for reading and for bringing this to our attention. The typo has been fixed.
Hi guys, upload using curl command is working just fine, however, upload from a React app in the Chrome browser gives error 500.
Application log output was:
unhandled error: Rejection(InvalidHeader { name: “content-type” })
Any suggestions ?
Not an issues anymore, the problem was on js code side.
Hey
As the error suggests, I believe the issue is with the “content-type” header. In the CURL command you can see
–header ‘Content-Type: multipart/form-data’
So in your React app, please check in the dev tools, if the request sent from your frontend actually contains this header and if not, you can try setting it manually on your upload request.
Hope this helps!