This is a submission for the The Pinata Challenge
What I Built
I built a multiple file upload using Golang and React, It able to upload multiple files to Pinata.
Here's a breakdown of what it does:
- It checks if the HTTP method of the request is POST. If not, it sends an error response with a status code of 405 (Method Not Allowed) and returns.
- It parses the multipart form data from the request using the ParseMultipartForm function, with a maximum file size of maxFileSize. If there is an error parsing the form data, it sends an error response with a status code of 400 (Bad Request) and returns.
- It retrieves the list of uploaded files from the request using MultipartForm.File["files"]. If no files were uploaded, it sends an error response with a status code of 400 (Bad Request) and returns.
- It initializes two slices: responses to store the successful uploads, and errors to store any errors that occur during the upload process.
- It creates a sync.WaitGroup and a sync.Mutex to synchronize the concurrent file uploads.
- It iterates over each uploaded file and launches a goroutine to upload the file using the uploadFileToPinata function. The wg.Add(1) increments the wait group counter, and the defer wg.Done() decrements it when the goroutine completes.
- The uploadFileToPinata function uploads each file to a Pinata API endpoint and returns the response and any error that occurred during the upload.
- The mu.Lock() and mu.Unlock() calls ensure that the responses and errors slices are updated atomically.
- After all the goroutines are complete, the function waits for them to finish using wg.Wait().
- It constructs a JSON response struct result with the successful uploads and errors.
- It sets the response header's content type to JSON.
- It checks for errors. If so, it sets the response status code to 206 (Partial Content). Otherwise, it sets the status code to 200 (OK).
- It encodes the result struct as JSON and writes it to the response body.
Overall, this code snippet handles the file upload process, performs concurrent uploads, and sends a JSON response with the successful uploads and any errors that occurred during the process.
Demo
My Code
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"sync"
"github.com/joho/godotenv"
)
const (
maxFileSize = 10 << 20 // 10 MB
)
type PinataResponse struct {
IpfsHash string `json:"IpfsHash"`
PinSize int `json:"PinSize"`
Timestamp string `json:"Timestamp"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
func main() {
// Load .env file
err := godotenv.Load(".env")
if err != nil {
log.Fatal("Error loading .env file")
}
// http.HandleFunc("/upload", handleUpload)
http.Handle("/upload", corsMiddleware(http.HandlerFunc(handleUpload)))
fmt.Println("Server is running on http://localhost:9000")
log.Fatal(http.ListenAndServe(":9000", nil))
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, pinata_api_key, pinata_secret_api_key")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func handleUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
sendErrorResponse(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
err := r.ParseMultipartForm(maxFileSize)
if err != nil {
sendErrorResponse(w, "Failed to parse multipart form: "+err.Error(), http.StatusBadRequest)
return
}
files := r.MultipartForm.File["files"]
if len(files) == 0 {
sendErrorResponse(w, "No files were uploaded", http.StatusBadRequest)
return
}
responses := make([]PinataResponse, 0, len(files))
errors := make([]string, 0)
var wg sync.WaitGroup
var mu sync.Mutex
for _, fileHeader := range files {
wg.Add(1)
go func(fh *multipart.FileHeader) {
defer wg.Done()
response, err := uploadFileToPinata(fh)
mu.Lock()
defer mu.Unlock()
if err != nil {
errors = append(errors, fmt.Sprintf("Error uploading %s: %v", fh.Filename, err))
} else {
responses = append(responses, response)
}
}(fileHeader)
}
wg.Wait()
result := struct {
SuccessfulUploads []PinataResponse `json:"successful_uploads"`
Errors []string `json:"errors,omitempty"`
}{
SuccessfulUploads: responses,
Errors: errors,
}
w.Header().Set("Content-Type", "application/json")
if len(errors) > 0 {
w.WriteHeader(http.StatusPartialContent)
} else {
w.WriteHeader(http.StatusOK)
}
json.NewEncoder(w).Encode(result)
}
func uploadFileToPinata(fileHeader *multipart.FileHeader) (PinataResponse, error) {
file, err := fileHeader.Open()
if err != nil {
return PinataResponse{}, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
part, err := writer.CreateFormFile("file", filepath.Base(fileHeader.Filename))
if err != nil {
return PinataResponse{}, fmt.Errorf("failed to create form file: %w", err)
}
_, err = io.Copy(part, file)
if err != nil {
return PinataResponse{}, fmt.Errorf("failed to copy file content: %w", err)
}
err = writer.Close()
if err != nil {
return PinataResponse{}, fmt.Errorf("failed to close multipart writer: %w", err)
}
// Load environment variables
pinataAPIKey := os.Getenv("PINATA_API_KEY")
pinataAPISecret := os.Getenv("PINATA_API_SECRET")
pinataAPIURL := os.Getenv("PINATA_API_URL")
req, err := http.NewRequest("POST", pinataAPIURL, &requestBody)
if err != nil {
return PinataResponse{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("pinata_api_key", pinataAPIKey)
req.Header.Set("pinata_secret_api_key", pinataAPISecret)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return PinataResponse{}, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return PinataResponse{}, fmt.Errorf("pinata API returned non-OK status: %s", resp.Status)
}
var pinataResp PinataResponse
err = json.NewDecoder(resp.Body).Decode(&pinataResp)
if err != nil {
return PinataResponse{}, fmt.Errorf("failed to decode Pinata response: %w", err)
}
return pinataResp, nil
}
func sendErrorResponse(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}
Find the frontend code on the shared github profile
Top comments (0)