Categories:

Chunk File Uploading on GoLang

This is the backend implementation of chunk file upload under golang platform. The backend system is only implemented. The philosophy behind is it attempted to upload everything in a single request. So, with the chunking strategy, they processed fixed-size chunk requests until all of the file parts are uploaded.

File Chunk data are transmitted from the client-side with the headers like content-type, content-range, content expiry etc, and file form data (filename, filetype). Therefore each chunk is processed by the server to get uploaded on the desired condition.

Go lang code

package main

import (
	"chunk-file-upload/controller"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

func main() {
	httpRouter := gin.Default()
	httpRouter.Use(cors.New(cors.Config{
		AllowOrigins:     []string{"*"},
		AllowMethods:     []string{"PUT", "PATCH", "GET", "POST", "OPTIONS", "DELETE"},
		AllowHeaders:     []string{"*"},
		AllowCredentials: true,
	}))
	httpRouter.POST("/chunk-upload", controller.ChunkUploadHandler)
	httpRouter.Run(":8080")
}
package controller

import (
	"io"
	"strconv"
	"strings"
	"net/http"
	"os"
	"github.com/gin-gonic/gin"
)

// Input -> add binding model for input
type Input struct {
	Model string `form:"model,omitempty" binding:"required"`
}

// Response -> response for the util scope
type Response struct {
	Success bool   `json:"success"`
	Message string `json:"message"`
	Name    string `json:"name"`
}

func ChunkUploadHandler(c *gin.Context) {
	var f *os.File
	file, uploadFile, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "content-type should be multipart/formdata"})
		return
	}

	contentRangeHeader := c.Request.Header.Get("Content-Range")
	rangeAndSize := strings.Split(contentRangeHeader, "/")
	rangeParts := strings.Split(rangeAndSize[0], "-")

	rangeMax, err := strconv.Atoi(rangeParts[1])
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Missing range in Content-Range header"})
		return
	}

	fileSize, err := strconv.Atoi(rangeAndSize[1])
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Missing file size in Content-Range header"})
		return
	}

	if fileSize > 100*1024*1024 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "File size should be less than 100MB"})
		return
	}

	var input Input
	err = c.ShouldBind(&input)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Missing model name in header"})
		return
	}

	tempDir, err := os.UserHomeDir()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Error resolving home directory"})
		return
	}

	if _, err := os.Stat(tempDir + "/" + input.Model); os.IsNotExist(err) {
		err := os.Mkdir(tempDir+"/"+input.Model, 0777)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Error creating temporary directory"})
			return
		}
	}

	if f == nil {
		f, err = os.OpenFile(tempDir+"/"+input.Model+"/"+uploadFile.Filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Error creating file"})
			return
		}
	}

	if _, err := io.Copy(f, file); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Error writing to a file"})
		return
	}

	f.Close()
	if rangeMax >= fileSize-1 {
		combinedFile := tempDir + "/" + input.Model + "/" + uploadFile.Filename

		uploadingFile, err := os.Open(combinedFile)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to upload file "})
			return
		}
		uploadingFile.Close()
		response := &Response{
			Success: true,
			Message: "Uploaded Successfully",
			Name:    uploadFile.Filename,
		}
		c.JSON(http.StatusOK, gin.H{"data": response})
		return
	}
	c.JSON(http.StatusOK, gin.H{"status": "uploading"})
}