- read

Upload and Retrieve Images to MongoDB GridFS using Golang and Fiber

Roshan Paturkar 171

Initially when I started working with go fiber and Mongodb, I faced a lot of issues. I didn’t find any documentation that could help me to create APIs that upload and retrieve the images in MongoDB GridFS using go fiber.

What is Go Fiber?

Fiber is an Express inspired web framework built on top of FastHttp, the fastest HTTP engine for Go. Designed to ease things up for fast development with zero memory allocation and performance in mind.

What is MongoDB GridFS?

MongoDB GridFS is a specification for storing and retrieving files that exceed the BSON-document size limit of 16 MB. GridFS divides files into parts, or chunks, and stores each of those chunks as a separate document. GridFS uses two collections to store the file parts and file metadata. GridFS is a specification for storing and retrieving files that exceed the BSON document size limit of 16 MB. GridFS divides files into parts, or chunks, and stores each of those chunks as a separate document. GridFS uses two collections to store the file parts and file metadata.

Let's dive into the code

You have to install below packages:

go get -u github.com/gofiber/fiber/v2
go get go.mongodb.org/mongo-driver/mongo
go get github.com/joho/godotenv

Create the main package and function with the fiber app instance

package main

import "github.com/gofiber/fiber/v2"

func main() {
app := fiber.New()

app.Listen(":3000")
}

Now, it's time to create the first ever API, POST image
POST localhost:3000/api/image
Get the file header from the form file fileHeader, err := c.formFile("image")
It will return file header and the error if any.
Note: Whenever any step returns an error check before moving further like the below code

fileHeader, err := c.formFile("image")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

Read file name and extract the extension of the file. If file is not the type of image return an error. After that open the fileHeader and read the binary data file, err := fileHeader.Open() & content, err := io.ReadAll(file) IO is a built in package in GO that help to do IO related operations.
Create the MongoDB client. After that create a new one or set the existing GridFS bucket bucket, err := gridfs.NewBucket(db, options.GridFSBucket().SetName("images")) Now we are good to open the upload stream. Open upload stream with file extension as metadata using uploadStream, err := bucket.OpenUploadStream(fileHeader.Filename, options.GridFSUpload().setMetadata(fiber.Map{"ext": fileExtension}))
Write data to the stream, close it once done, and return the required data.

app.Post("/api/image", func(c *fiber.Ctx) error {
// Check if file is present in request body or not
fileHeader, err := c.FormFile("image")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

// Check if file is of type image or not
fileExtension := regexp.MustCompile(`\.[a-zA-Z0-9]+$`).FindString(fileHeader.Filename)
if fileExtension != ".jpg" && fileExtension != ".jpeg" && fileExtension != ".png" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": "Invalid file type",
})
}

// Read file content
file, err := fileHeader.Open()
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
content, err := io.ReadAll(file)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

// Create db connection
db := mongoClient().Database("go-fs")
// Create bucket
bucket, err := gridfs.NewBucket(db, options.GridFSBucket().SetName("images"))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

// Upload file to GridFS bucket
uploadStream, err := bucket.OpenUploadStream(fileHeader.Filename, options.GridFSUpload().SetMetadata(fiber.Map{"ext": fileExtension}))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

// Close upload stream
fieldId := uploadStream.FileID
defer uploadStream.Close()

// Write file content to upload stream
fileSize, err := uploadStream.Write(content)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

// Return response
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"error": false,
"msg": "Image uploaded successfully",
"image": fiber.Map{
"id": fieldId,
"name": fileHeader.Filename,
"size": fileSize,
},
})
})

Response

{
"error": false,
"image": {
"id": "644f87e1a34be9619755b2ac",
"name": "goFiber.png",
"size": 11829
},
"msg": "Image uploaded successfully"
}

GET localhost:3000/api/image/id/:id
This API retrieves the image from MongoDB GridFS using image ID. Extract the image ID from the request and convert it to MongoDB Object ID.
id, err = primitive.ObjectIDFromHex(c.Params("id"))

Create the DB connection and empty the MongoDB BSON object. Fetch the data for the respective object ID (image ID) and store it in a BSON variable.
Create & set the GridFS bucket and save the download stream to the buffer.
var buffer bytes.Buffer
bucket,_=gridfs.NewBucket(db,options.GridFSBucket().SetName("images"))
buckt.DownloadToStream(id, &buffer)

Set the required headers according to image type.

func setResponseHeaders(c *fiber.Ctx, buff bytes.Buffer, ext string) error {
switch ext {
case ".png":
c.Set("Content-Type", "image/png")
case ".jpg":
c.Set("Content-Type", "image/jpeg")
case ".jpeg":
c.Set("Content-Type", "image/jpeg")
}

c.Set("Cache-Control", "public, max-age=31536000")
c.Set("Content-Length", strconv.Itoa(len(buff.Bytes())))

return c.Next()
}

After that return the image buffer.
return c.Send(buffer.Bytes())

app.Get("/api/image/id/:id", func(c *fiber.Ctx) error {
// Get image id from request params and convert it to ObjectID
id, err := primitive.ObjectIDFromHex(c.Params("id"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

// Create db connection
db := mongoClient().Database("go-fs")

// Create variable to store image metadata
var avatarMetadata bson.M

// Get image metadata from GridFS bucket
if err := db.Collection("images.files").FindOne(c.Context(), fiber.Map{"_id": id}).Decode(&avatarMetadata); err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Avatar not found",
})
}

// Create buffer to store image content
var buffer bytes.Buffer
// Create bucket
bucket, _ := gridfs.NewBucket(db, options.GridFSBucket().SetName("images"))
// Download image from GridFS bucket to buffer
bucket.DownloadToStream(id, &buffer)

// Set required headers
setResponseHeaders(c, buffer, avatarMetadata["metadata"].(bson.M)["ext"].(string))

// Return image
return c.Send(buffer.Bytes())
})

Response:

GET localhost:3000/api/image/name/:name
Same thing we have to do with this API as well except fetching metadata with image name and download stream by name.

db.Collection("images.files").FindOne(c.Context(), fiber.Map{"filename": name}).Decode(&avatarMetadata)

bucket.DownloadToStreamByName(name, &buffer)

and return the response.

app.Get("/api/image/name/:name", func(c *fiber.Ctx) error {
// Get image name from request params
name := c.Params("name")

// Create db connection
db := mongoClient().Database("go-fs")

// Create variable to store image metadata
var avatarMetadata bson.M

// Get image metadata from GridFS bucket
if err := db.Collection("images.files").FindOne(c.Context(), fiber.Map{"filename": name}).Decode(&avatarMetadata); err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Avatar not found",
})
}

// Create buffer to store image content
var buffer bytes.Buffer
// Create bucket
bucket, _ := gridfs.NewBucket(db, options.GridFSBucket().SetName("images"))
// Download image from GridFS bucket to buffer
bucket.DownloadToStreamByName(name, &buffer)

// Set required headers
setResponseHeaders(c, buffer, avatarMetadata["metadata"].(bson.M)["ext"].(string))

// Return image
return c.Send(buffer.Bytes())
})

Response:

DELETE localhost:3000/api/image/id/:id
To delete the image from the bucket, GridFS provides the API that accepts the id as input and deletes the file from the bucket. It deletes all the chunks and associated metadata of the file.
bucket.Delete(id)

app.Delete("/api/image/id/:id", func(c *fiber.Ctx) error {
// Get image id from request params and convert it to ObjectID
id, err := primitive.ObjectIDFromHex(c.Params("id"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

// Create db connection
db := mongoClient().Database("go-fs")

// Create bucket
bucket, _ := gridfs.NewBucket(db, options.GridFSBucket().SetName("images"))

// Delete image from GridFS bucket
if err := bucket.Delete(id); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

// Return success message
return c.JSON(fiber.Map{
"error": false,
"msg": "Image deleted successfully",
})
})

Response:

You can find the code repo here https://github.com/roshanpaturkar/go-mongo-fs

If you want to deep dive into Go and MongoDB with Fiber https://github.com/roshanpaturkar/go-tasks