How to Upload Files from Rust with reqwest
Step-by-step guide to uploading files from Rust via the files.link REST API — using reqwest, three HTTP calls, no vendor crate.
Rust's HTTP story
reqwest is the de facto Rust HTTP client. It supports both blocking and async modes, handles TLS, JSON, multipart, and presigned-URL uploads cleanly. AWS has an official Rust SDK now (aws-sdk-rust), but for a managed file-hosting service like files.link, you can skip the vendor crate and use plain reqwest.
The 3-step presigned flow
1. POST file metadata to /v1/files/{folderId} → presigned PUT URL.
2. PUT file bytes to the presigned URL.
3. POST /v1/files/confirm-upload with the file id.
Code: minimal Rust upload (async)
Cargo.toml:
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }src/main.rs:
use reqwest::Client;
use serde::Deserialize;
use std::env;
use tokio::fs;
const BASE: &str = "https://api.files.link";
const FOLDER_ID: &str = "your-folder-uuid-here";
#[derive(Deserialize)]
struct UrlEntry { url: String, id: String, s3Key: String }
#[derive(Deserialize)]
struct MetaResponse { urls: Vec<UrlEntry> }
async fn upload(path: &str) -> Result<String, Box<dyn std::error::Error>> {
let api_key = env::var("FILESLINK_KEY")?;
let bytes = fs::read(path).await?;
let filename = std::path::Path::new(path)
.file_name().unwrap().to_string_lossy().to_string();
let size = bytes.len();
let client = Client::new();
// Step 1: presigned URL
let meta: MetaResponse = client
.post(format!("{}/v1/files/{}", BASE, FOLDER_ID))
.header("Authorization", &api_key)
.json(&serde_json::json!({
"filesMetadata": [{ "name": filename, "size": size }]
}))
.send().await?
.json().await?;
let entry = &meta.urls[0];
// Step 2: PUT the bytes
client.put(&entry.url).body(bytes).send().await?
.error_for_status()?;
// Step 3: confirm
client
.post(format!("{}/v1/files/confirm-upload", BASE))
.header("Authorization", &api_key)
.json(&serde_json::json!({ "ids": [&entry.id] }))
.send().await?
.error_for_status()?;
Ok(entry.s3Key.clone()) // combine with your CDN base URL for a public link
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = upload("./example.pdf").await?;
println!("uploaded: {}", key);
Ok(())
}Set FILESLINK_KEY in env, swap FOLDER_ID, cargo run. The full upload flow in ~50 lines of safe Rust.
Streaming and large files
fs::read loads the entire file into memory. For multi-GB uploads, stream the body with tokio::fs::File + reqwest::Body::wrap_stream:
let file = tokio::fs::File::open(path).await?;
let stream = tokio_util::io::ReaderStream::new(file);
client.put(&entry.url)
.body(reqwest::Body::wrap_stream(stream))
.send().await?;For files >100MB, use the multipart endpoint (/v1/files/multipart/{folderId}/initiate). Each part has its own presigned URL — parallelize with futures::stream::FuturesUnordered for max throughput.
See Rust file upload for more patterns.