🔗 files.link
tutorials

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.