<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sang</title>
    <description>The latest articles on DEV Community by Sang (@its_sang).</description>
    <link>https://dev.to/its_sang</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F203411%2F4a9da8dd-476e-4060-9908-415d644f59c3.jpg</url>
      <title>DEV Community: Sang</title>
      <link>https://dev.to/its_sang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/its_sang"/>
    <language>en</language>
    <item>
      <title>Laveraging Pinata API to upload files</title>
      <dc:creator>Sang</dc:creator>
      <pubDate>Sat, 12 Oct 2024 19:40:13 +0000</pubDate>
      <link>https://dev.to/its_sang/laveraging-pinata-api-to-upload-files-c3i</link>
      <guid>https://dev.to/its_sang/laveraging-pinata-api-to-upload-files-c3i</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/pinata"&gt;The Pinata Challenge &lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built a multiple file upload using Golang and React, It able to upload multiple files to Pinata.&lt;/p&gt;

&lt;p&gt;Here's a breakdown of what it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;It initializes two slices: responses to store the successful uploads, and errors to store any errors that occur during the upload process.&lt;/li&gt;
&lt;li&gt;It creates a sync.WaitGroup and a sync.Mutex to synchronize the concurrent file uploads.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;The uploadFileToPinata function uploads each file to a Pinata API endpoint and returns the response and any error that occurred during the upload.&lt;/li&gt;
&lt;li&gt;The mu.Lock() and mu.Unlock() calls ensure that the responses and errors slices are updated atomically.&lt;/li&gt;
&lt;li&gt;After all the goroutines are complete, the function waits for them to finish using wg.Wait().&lt;/li&gt;
&lt;li&gt;It constructs a JSON response struct result with the successful uploads and errors.&lt;/li&gt;
&lt;li&gt;It sets the response header's content type to JSON.&lt;/li&gt;
&lt;li&gt;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).&lt;/li&gt;
&lt;li&gt;It encodes the result struct as JSON and writes it to the response body.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1nwkbmz1pyr68fcxgntd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1nwkbmz1pyr68fcxgntd.png" alt="Frontend demo" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7qefjvn857e3ohhbqfh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7qefjvn857e3ohhbqfh.png" alt="Uploaded Pinata files" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sangkips/fileupload" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package main

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

    "github.com/joho/godotenv"
)

const (
    maxFileSize = 10 &amp;lt;&amp;lt; 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) &amp;gt; 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(&amp;amp;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, &amp;amp;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 := &amp;amp;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(&amp;amp;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})
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the frontend code on the shared github profile&lt;/p&gt;

&lt;h2&gt;
  
  
  More Details
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>pinatachallenge</category>
      <category>webdev</category>
      <category>api</category>
    </item>
  </channel>
</rss>
