Back to blog

How to Upload to IPFS Using Go

How to Upload to IPFS Using Go

Steve

Finding the right programming language for the software you want to build can be quite the task. When it comes to the web, Javascript/Typescript dominate the playing field, but it has its own issues. We saw this in our own stack and took the steps to rewrite our API in what has become one of our favorite languages: Go. Not only does the Gopher match the spirit of our own mascot Pinnie, it has also proven to be easy to work with and incredibly powerful. With that said, it seems fitting we show how you can upload a file to Pinata using Go! If you’ve never touched Go, then this could be a fun exercise to try something new, so don’t be shy and let’s get to it 😎

Setup

Before we start writing some code, we’ll need to setup a few things first. To start, you’ll need a free Pinata account, so don’t just sit there: sign up here! After you sign in, we need to make an API key; just click on the API keys tab on the left and then click “New Key” in the top right. You can either make an Admin key or just select Write for Files. After creating the Key, you’ll get a larger JWT which you will want to save for later. Last thing you need inside the Pinata dashboard is your Gateway, which is what we can use to retrieve files. Go to the Gateway tab on the left sidebar and you should see your gateway waiting for you. Copy the domain as you see it which should be something like red-fashionable-caribou-745.mypinata.cloud.

Now that we have our Pinata credentials, we’ll need to setup our Go develop environment. If you already have Go installed and ready to use then you can skip this part. First you’ll want to visit the Go Install Page where you can download the latest version. From there, the steps might vary depending on your machine, so be sure to follow the steps on that page. Once it’s installed make sure it’s working by running the command below.

go version

If you get a successful version like go version go1.24.0 darwin/arm64 then you’re good to go!

With Go installed we can setup our project with the main files we need. In the terminal, run the following commands, paying close attention to replace YOUR_USERNAME with your own GitHub username.

mkdir pinata-go
cd pinata-go
go mod init github.com/YOUR_USERNAME/pinata-go
touch main.go

We’re ready to start writing some code!

Implementing Uploads

Go (pun intended) ahead and open up the main.go file in your text editor of choice and let’s put the following code inside.

package main

import "fmt"

func main() {
	fmt.Println("Hello, Go!")
}

This is a really simple starter program that we can use to get a little familiar with how Go works. At the top of the file, we declare our package name, keeping it simple by sticking with main. Then we import the fmt package from the Go standard library. If you’re familiar with Javascript, then you can think of this similar to how Node.js has fs as an included package. However, the more you work with Go the more you’ll see how it has a much larger set of included tools. Moving on, we declare our function main() and simply print out some text to the console. What’s unique about Go, compared to something like Javascript, is that it is a compiled language. This means we can create an executable that can run anywhere, which is pretty sweet! Let’s do that now with our small program. In the terminal, run the following command.

go build .

Once you run this, you should now see a file called pinata-go - this is our binary! You can run it like so:

./pinata-go

If successful, you should see the printed "Hello, Go!" in the console. You just wrote your first program in Go! While I enjoy a good hello world, let’s build something actually useful. We can use the standard libraries and our Pinata credentials to upload files with speed and efficiency. Back in our main.go file, let’s paste in the following code.

package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
)

type UploadResponse struct {
	Data struct {
		Id        string `json:"id"`
		Name      string `json:"name"`
		Cid       string `json:"cid"`
		Size      int    `json:"size"`
		CreatedAt string `json:"created_at"`
		MimeType  string `json:"mime_type"`
		Network   string `json:"network"`
	}
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Please provide a file path")
		fmt.Println("Usage: pinata-go /path/to/file.png")
		os.Exit(1)
	}

	filePath := os.Args[1]

	response, err := uploadFile(filePath)
	if err != nil {
		fmt.Printf("Error uploading file: %v\n", err)
		os.Exit(1)
	}

	formattedJSON, err := json.MarshalIndent(response.Data, "", "    ")
	if err != nil {
		fmt.Println("Error formatting JSON response")
		os.Exit(1)
	}
	fmt.Println("Upload successful!")
	fmt.Println(string(formattedJSON))

	gateway := os.Getenv("GATEWAY_URL")
	fmt.Printf("View File: https://%s/ipfs/%s", gateway, response.Data.Cid)
}

func uploadFile(filePath string) (UploadResponse, error) {
	stats, err := os.Stat(filePath)
	if os.IsNotExist(err) {
		return UploadResponse{}, errors.New("file does not exist")
	}
	if stats.IsDir() {
		return UploadResponse{}, errors.New("directories are not supported by this tool")
	}

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	file, err := os.Open(filePath)
	if err != nil {
		return UploadResponse{}, err
	}
	defer file.Close()

	part, err := writer.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		return UploadResponse{}, err
	}
	_, err = io.Copy(part, file)
	if err != nil {
		return UploadResponse{}, err
	}

	err = writer.WriteField("network", "public")
	if err != nil {
		return UploadResponse{}, err
	}

	err = writer.WriteField("name", stats.Name())
	if err != nil {
		return UploadResponse{}, err
	}

	err = writer.Close()
	if err != nil {
		return UploadResponse{}, err
	}

	url := "https://uploads.pinata.cloud/v3/files"
	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		return UploadResponse{}, err
	}

	req.Header.Set("Content-Type", writer.FormDataContentType())

	token := os.Getenv("PINATA_JWT")
	if token == "" {
		return UploadResponse{}, errors.New("PINATA_JWT environment variable not set")
	}
	req.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return UploadResponse{}, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return UploadResponse{}, fmt.Errorf("server returned error code %d: %s",
			resp.StatusCode, string(bodyBytes))
	}

	var response UploadResponse
	err = json.NewDecoder(resp.Body).Decode(&response)
	if err != nil {
		return UploadResponse{}, err
	}

	return response, nil
}

There’s a lot here, and that’s mostly due to how Go handles errors and the patterns it has in place. Once you get used to it you’ll see it’s actually pretty straight forward. At the top, we import some standard libraries as well as declaring a struct.

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
)

type UploadResponse struct {
	Data struct {
		Id        string `json:"id"`
		Name      string `json:"name"`
		Cid       string `json:"cid"`
		Size      int    `json:"size"`
		CreatedAt string `json:"created_at"`
		MimeType  string `json:"mime_type"`
		Network   string `json:"network"`
	}
}

The struct is important because, since we get a JSON response back from the Pinata API, we need the ability to decode the response, and we need the struct to do that. It also forces us to have type safety which is also a great feature of Go. Before we get into the main function, let’s look at the uploadFile function first.

func uploadFile(filePath string) (UploadResponse, error) {
	stats, err := os.Stat(filePath)
	if os.IsNotExist(err) {
		return UploadResponse{}, errors.New("file does not exist")
	}
	if stats.IsDir() {
		return UploadResponse{}, errors.New("directories are not supported by this tool")
	}

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	file, err := os.Open(filePath)
	if err != nil {
		return UploadResponse{}, err
	}
	defer file.Close()

	part, err := writer.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		return UploadResponse{}, err
	}
	_, err = io.Copy(part, file)
	if err != nil {
		return UploadResponse{}, err
	}

	err = writer.WriteField("network", "public")
	if err != nil {
		return UploadResponse{}, err
	}

	err = writer.WriteField("name", stats.Name())
	if err != nil {
		return UploadResponse{}, err
	}

	err = writer.Close()
	if err != nil {
		return UploadResponse{}, err
	}

	url := "https://uploads.pinata.cloud/v3/files"
	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		return UploadResponse{}, err
	}

	req.Header.Set("Content-Type", writer.FormDataContentType())

	token := os.Getenv("PINATA_JWT")
	if token == "" {
		return UploadResponse{}, errors.New("PINATA_JWT environment variable not set")
	}
	req.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return UploadResponse{}, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return UploadResponse{}, fmt.Errorf("server returned error code %d: %s",
			resp.StatusCode, string(bodyBytes))
	}

	var response UploadResponse
	err = json.NewDecoder(resp.Body).Decode(&response)
	if err != nil {
		return UploadResponse{}, err
	}

	return response, nil
}

To start, we take in a file path for the file we want to upload, and with it we do some simple checks to make sure the file exists and that it’s not a directory. Then we start putting together the body of the request using the writer, where we can write the file, the network which in our case is public, and the name of the file. Once all the fields on our form are filled, we can close the writer and prepare the request with the API url, our body, and the method of POST. We also add a Header for our PINATA_JWT that will authorize the request. Finally we send the request with our &http.Client{} and check to make sure it was a successful request and return an error if not. Then we use the json decoder to decode the response and match it to our UploadResponse{} struct, and finally return the response. Now let’s see how it’s used in main():

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Please provide a file path")
		fmt.Println("Usage: pinata-go /path/to/file.png")
		os.Exit(1)
	}

	filePath := os.Args[1]

	response, err := uploadFile(filePath)
	if err != nil {
		fmt.Printf("Error uploading file: %v\n", err)
		os.Exit(1)
	}

	formattedJSON, err := json.MarshalIndent(response.Data, "", "    ")
	if err != nil {
		fmt.Println("Error formatting JSON response")
		os.Exit(1)
	}
	fmt.Println("Upload successful!")
	fmt.Println(string(formattedJSON))

	gateway := os.Getenv("GATEWAY_URL")
	fmt.Printf("View File: https://%s/ipfs/%s", gateway, response.Data.Cid)
}

At the beginning of the program, we check the arguments provided in the terminal and make sure it’s just one and that it’s a file path. Then we take that argument and pass it into uploadFile that we just looked at. Once we get the response, we use the json package again to format the data so it prints out nicely in the terminal. Lastly, we add in a URL to access the file using our GATEWAY_URL!

Everything is ready to go, so now it’s time to test this out. One thing you might have noticed is that we haven’t pasted our API key or Gateway Domain anywhere. That’s because we’re accessing them from our system environment, so there’s a few ways you can do that. One way is to export them in your shell config like .bashrc or .zshrc like so:

export PINATA_JWT="YOUR_JWT"
export GATEWAY_URL="YOUR_DOMAIN"

This is not necessarily the most secure way to handle it, so you should research on the best approach for you. In this tutorial, we’ll take the middle ground by creating a .env file in our repo and pasting in the contents above. Then, in your terminal, you can run the following:

source .env

Now let’s build the project and try uploading a file. You can drop any file into your project, like this fun picture of Pinnie. Then run this in the terminal:

go build .
./pinata-go ./pinnie.png

If successful, you should see something like this!

Upload successful!
{
    "id": "01929106-f653-76bf-924c-00aaf9fe02fe",
    "name": "pinnie.png",
    "cid": "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4",
    "size": 32928,
    "created_at": "2024-10-15T16:33:26.368307Z",
    "mime_type": "image/png",
    "network": "public"
}
View File: https://dweb.mypinata.cloud/ipfs/bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4

You can copy and paste that URL in your browser to see the image 🔥 But wait, I’ve got one more thing. If you run go install . in the terminal, and you have the environment variables set, you can run pinata-go wherever you are on your machine! Turns into a handy program you can use to upload to Pinata whenever you need it. If you like the idea of that, then you may want to check out our IPFS CLI written in Go where you can do a lot more than upload a single file. We would highly recommend reading our blog post on it.

Wrapping Up

After writing a program in Go you can probably see why it’s a favorite within the Pinata team. With the right structure and best practices, it becomes an efficient and productive way to ship code that is performant and safe. Pinata pursues those values and we hope it shows when you use our products, whether it’s an API or our CLI or our docs. We simply want to make IPFS easy for developers so you can spend more time shipping your app.

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.