Cookbook / Files and pages
File uploads
Accept multipart uploads and stream them straight to disk, with a per-route size limit.
By default Axeon buffers request bodies up to the body-size limit. For big uploads, turn on stream_body() for that route: the body is never held in memory, and req.multipart() hands you each field as it arrives.
use axeon::{Server, ServerError};
use std::path::PathBuf;
fn main() {
let mut app = Server::new();
let dir = PathBuf::from("uploads");
std::fs::create_dir_all(&dir).expect("cannot create uploads dir");
app.route("/upload")
.body_limit(1 << 30) // 1 GiB, only for this route
.stream_body()
.post(move |req| {
let dir = dir.clone();
async move {
let mut form = req.multipart()?;
let mut saved = Vec::new();
while let Some(field) = form.next_field().await? {
if let Some(name) = field.file_name().map(|n| n.to_string()) {
// Never trust the client's filename as a path.
let safe = PathBuf::from(&name)
.file_name()
.map(|n| n.to_owned())
.ok_or_else(|| ServerError::BadRequest("bad filename".into()))?;
field.save_to(dir.join(&safe)).await?;
saved.push(safe.to_string_lossy().into_owned());
}
}
Ok::<_, ServerError>(format!("saved {}", saved.join(", ")))
}
});
app.listen("127.0.0.1:3000").expect("server failed to start");
}
curl -F 'file=@./photo.jpg' http://127.0.0.1:3000/upload
Notes#
- Streaming keeps memory flat: a 1 GiB upload does not use 1 GiB of RAM.
- Keep the generous
body_limiton the upload route only. The rest of the app keeps its tight default. - Always sanitise client-supplied filenames, as above, or generate your own.