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.

src/main.rs
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");
}
Terminal
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_limit on the upload route only. The rest of the app keeps its tight default.
  • Always sanitise client-supplied filenames, as above, or generate your own.