Back to blog

How to Upload Files in Rust

How to Upload Files in Rust

Steve

There are few languages that get a rouse out of developers more than Rust. When you strip away the memes and the lore behind this language, then you’ll find a robust ecosystem of developer apps, frameworks, even blockchains being built on Rust. In this tutorial, we’ll dip our toes into the deep waters of Rust and walk you through each step, so even if you have never used it before you’ll be safe and sound. Let’s dive in!

Setup

First thing first, you’ll want to make sure your develop environment is ready to go. With that, you’ll just need two things:

  • A code editor like VSCode, Cursor, Zed, etc.
  • Rust installed on your machine

Installing Rust is pretty easy, just use the following cURL script to start the install:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

If you’re on Windows, be sure to read the installation instructions here. Once you have Rust installed, try running these commands to make sure everything is working as expected.

cargo --version
rustc --version

Now let’s start up a new project using cargo, a package manager for Rust.

cargo new pinata_upload

This should create a new folder with our project files; let’s move into that directory and test to make sure everything is working as expected.

cd pinata_upload
cargo run

This should print a Hello, world! to your terminal, so we’re all set! We’ll edit our files soon, but the next thing we need is our Pinata API key and Gateway URL. If you haven’t already, go ahead and sign up for a free account here. Once you’re signed up, visit the API Keys page and create a new key in the top right. Give it a name that you can remember like rust_upload then give it a scope of files:write.

Once you create the API key, it will give you a JWT that you will want to save somewhere safe. Last thing we need to do is grab our Dedicated Gateway domain. Just visit the Gateways tab and copy the domain as you see it, and let’s store that with your API key for later.

CleanShot 2025-04-28 at 15.36.01@2x.png

Implementing Uploads

Now it’s time to actually start uploading some files! The cool thing about Rust is that it compiles to efficient binaries which can be run anywhere on your computer. With our little project we’ll make the smallest CLI that we can use wherever we want to upload a file to our Pinata account! To start, let’s open our pinata_upload project in our text editor and update the src/main.rs file.

use std::env;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use reqwest::multipart::{Form, Part};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use serde::Deserialize;
use tokio;

#[derive(Debug, Deserialize)]
struct PinataResponse {
    data: PinataData,
}

#[derive(Debug, Deserialize)]
struct PinataData {
    cid: String
}

async fn upload_to_pinata(file_path: &str, jwt: &str) -> Result<PinataData, Box<dyn std::error::Error>> {
    // Prepare the file for upload
    let path = Path::new(file_path);
    let file_name = path.file_name()
        .ok_or("Invalid file name")?
        .to_str()
        .ok_or("File name is not valid UTF-8")?;

    let mut file = File::open(path)?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;

    // Prepare headers with JWT auth
    let mut headers = HeaderMap::new();
    let auth_value = format!("Bearer {}", jwt);
    headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);

    // Create a client with proper headers
    let client = reqwest::Client::builder()
        .default_headers(headers)
        .build()?;

    // Prepare multipart form
    let file_part = Part::bytes(buffer)
        .file_name(file_name.to_string())
        .mime_str("application/octet-stream")?;

    // Set to "public" for public upload, "private" for private upload
    let network_part = Part::text("public");

    let form = Form::new()
        .part("file", file_part)
        .part("network", network_part);

    // Send the request
    let response = client.post("https://uploads.pinata.cloud/v3/files")
        .multipart(form)
        .send()
        .await?;

    if !response.status().is_success() {
        let error_text = response.text().await?;
        return Err(format!("Upload failed: {}", error_text).into());
    }

    // Parse the response
    let pinata_response: PinataResponse = response.json().await?;
    Ok(pinata_response.data)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Get command line arguments
    let args: Vec<String> = env::args().collect();

    if args.len() != 2 {
        println!("Usage: pinata_upload /path/to/file");
        return Ok(());
    }

    let file_path = &args[1];

    // Get JWT from environment variable
    let jwt = env::var("PINATA_JWT").expect("PINATA_JWT environment variable must be set");

    // Get optional gateway URL
    let gateway_url = env::var("GATEWAY_URL").ok();

    // Upload file to Pinata
    match upload_to_pinata(file_path, &jwt).await {
        Ok(data) => {
            println!("File uploaded successfully!");
            println!("CID: {}", data.cid);

            // If gateway URL is provided, print the full gateway URL
            if let Some(gateway) = gateway_url {
                println!("Gateway URL: https://{}/ipfs/{}", gateway.trim_end_matches('/'), data.cid);
            }
        },
        Err(err) => eprintln!("Error: {}", err),
    }

    Ok(())
}

There’s a lot going on here, so let’s break it down piece by piece to see what’s going on.

use std::env;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use reqwest::multipart::{Form, Part};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use serde::Deserialize;
use tokio;

#[derive(Debug, Deserialize)]
struct PinataResponse {
    data: PinataData,
}

#[derive(Debug, Deserialize)]
struct PinataData {
    cid: String
}

At the top of the file, we have some library imports. Some of them are standard libraries to read arguments from the terminal, read files, and there’s also some third party libraries like reqwest, serde, and tokio to handle API requests, serializing JSON, and handling async applications. Below our imports, we have some structs that will help us parse the upload response we get back and grab the cid. Now let’s look at our upload function.

async fn upload_to_pinata(file_path: &str, jwt: &str) -> Result<PinataData, Box<dyn std::error::Error>> {
    // Prepare the file for upload
    let path = Path::new(file_path);
    let file_name = path.file_name()
        .ok_or("Invalid file name")?
        .to_str()
        .ok_or("File name is not valid UTF-8")?;

    let mut file = File::open(path)?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;

    // Prepare headers with JWT auth
    let mut headers = HeaderMap::new();
    let auth_value = format!("Bearer {}", jwt);
    headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);

    // Create a client with proper headers
    let client = reqwest::Client::builder()
        .default_headers(headers)
        .build()?;

    // Prepare multipart form
    let file_part = Part::bytes(buffer)
        .file_name(file_name.to_string())
        .mime_str("application/octet-stream")?;

    // Set to "public" for public upload, "private" for private upload
    let network_part = Part::text("public");

    let form = Form::new()
        .part("file", file_part)
        .part("network", network_part);

    // Send the request
    let response = client.post("https://uploads.pinata.cloud/v3/files")
        .multipart(form)
        .send()
        .await?;

    if !response.status().is_success() {
        let error_text = response.text().await?;
        return Err(format!("Upload failed: {}", error_text).into());
    }

    // Parse the response
    let pinata_response: PinataResponse = response.json().await?;
    Ok(pinata_response.data)
}

This function is going to take in both a file_path and a jwt which will authorize our request. Once we have a path, we’ll use our standard libraries to check the file, open it, and read the file into a buffer. When we prep our API call headers with our jwt to authorize the request, then we start to build the request with client. That includes creating form data with file_part and attaching the file buffer as well as the network we want to upload to public. Then we simple make the API request with client.post with our form, then parse the response. This function will be used in our main application below.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Get command line arguments
    let args: Vec<String> = env::args().collect();

    if args.len() != 2 {
        println!("Usage: pinata_upload /path/to/file");
        return Ok(());
    }

    let file_path = &args[1];

    // Get JWT from environment variable
    let jwt = env::var("PINATA_JWT").expect("PINATA_JWT environment variable must be set");

    // Get optional gateway URL
    let gateway_url = env::var("GATEWAY_URL").ok();

    // Upload file to Pinata
    match upload_to_pinata(file_path, &jwt).await {
        Ok(data) => {
            println!("File uploaded successfully!");
            println!("CID: {}", data.cid);

            // If gateway URL is provided, print the full gateway URL
            if let Some(gateway) = gateway_url {
                println!("Gateway URL: https://{}/ipfs/{}", gateway.trim_end_matches('/'), data.cid);
            }
        },
        Err(err) => eprintln!("Error: {}", err),
    }

    Ok(())
}

In this main function, we’ll parse command line arguments for file path, grab the PINATA_JWT from the system environment as well as the GATEWAY_URL, then run the upload function. If successful, our app will print the resulting CID as well as a gateway link we can use to access it! Now, to test this out, you will need to export the environment variables to your shell like so.

export PINATA_JWT="YOUR_JWT"
export GATEWAY_URL="yourdomain.mypinata.cloud"

Then try running your command followed by the path to the file you want to upload. If you need something to upload try this image of Pinnie.

cargo run pinnie.png

If it works, then you should get this printed in the terminal!

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/pinata_upload pinnie.png`
File uploaded successfully!
CID: bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4
Gateway URL: https://dweb.mypinata.cloud/ipfs/bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4

How about that - you built a CLI in Rust! Now if we wanted to make this a binary we can run anywhere on our machine you can do the following:

  • Run cargo run build --release to build an optimized binary
  • Move the produced binary to a bin folder that’s part of your path, eg. cp target/release/pinata_upload ~/.local/share
  • Securely export your PINATA_JWT and GATEWAY_URL to your system environment using a combination of a password manager like 1Password and your .bashrc or .zshrc file, or go deeper in Rust to store your JWT in a secure dot file and have the program pull it from there instead

Now you can run pinata_upload anywhere on your computer to upload a file at blazing Rust speeds 🦀

While it’s unlikely you might ever use Rust again, it’s good practice to try other languages to see how they work. Each one has it’s own specific purpose, and if you’re using Pinata you can rest assured that our APIs will be a breeze to implement no matter what language you choose. Give it a shot today with a free account and experience the best uploading experience for blockchain storage.

Happy Pinning!

Subscribe to paid plan image

Share this post:

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.