Welcome to this exciting lesson on enhancing our ToDo app by allowing it to support images for each item. In previous lessons, we've focused on adding and managing ToDo items using the Gin
framework. Now, we're taking things a step further by adding image functionality to our app. This means that you'll be able to attach an image to any ToDo item, making your app more interactive and visually appealing. By the end of this lesson, you'll have learned how to upload and retrieve images effectively using Gin
.
Before we can add image support, we need to modify the existing ToDo model. We can simply update it to include an ImagePath
attribute, which will store the path of the uploaded image file. This modification will allow each ToDo item to have its own associated image (only one!).
Here's the modified Todo
model:
Go1package models 2 3type Todo struct { 4 ID int `json:"id"` 5 Title string `json:"title"` 6 Completed bool `json:"completed"` 7 ImagePath string `json:"imagePath"` 8}
Next, let's dive into implementing the functionality that allows image uploads. This involves setting up a POST
route that accepts file uploads and updates the corresponding ToDo item with the image path.
controllers.go
:
Go1package controllers 2 3import ( 4 "net/http" 5 "strconv" 6 7 "github.com/gin-gonic/gin" 8 9 "codesignal.com/todoapp/services" 10) 11 12func UploadImage(c *gin.Context) { 13 id, err := strconv.Atoi(c.Param("id")) 14 if err != nil { 15 c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) 16 return 17 } 18 if success, imagePath := services.HandleImageUpload(c, id); success { 19 c.JSON(http.StatusOK, gin.H{"message": "Image added", "imagePath": imagePath}) 20 } else { 21 c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) 22 } 23}
services.go
:
Go1package services 2 3import ( 4 "path/filepath" 5 "strconv" 6 7 "github.com/gin-gonic/gin" 8 9 "codesignal.com/todoapp/models" 10) 11 12var todos []models.Todo // Assume this is initialized elsewhere 13 14func HandleImageUpload(c *gin.Context, id int) (bool, string) { 15 var todo *models.Todo 16 for i, t := range todos { 17 if t.ID == id { 18 todo = &todos[i] 19 } 20 } 21 if todo == nil { 22 c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"}) 23 return false, "" 24 } 25 26 file, err := c.FormFile("file") 27 if err != nil { 28 c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"}) 29 return false, "" 30 } 31 32 fileName := filepath.Base(file.Filename) 33 filePath := filepath.Join("uploads", fileName) 34 35 if err := c.SaveUploadedFile(file, filePath); err != nil { 36 return false, "" 37 } 38 todo.ImagePath = filePath 39 return true, filePath 40}
In this code snippet, we first convert the supplied ID from the URL parameter to an integer using strconv.Atoi
, then locate the ToDo item by its ID. We use c.FormFile("file")
to handle file uploads, which allows us to access files sent with the request. A check is performed to verify if a file is uploaded. If so, the file is saved to a designated directory using c.SaveUploadedFile(file, filePath)
, and the ImagePath
attribute of the ToDo item is updated. Finally, a JSON response is returned using c.JSON
, which sets the response status and sends JSON data back to the client. This response either confirms success or details an error.
Now that we have a way to upload images, the next step is to set up a GET
route to retrieve and display these images as needed. Let's see how you can accomplish this.
controllers.go
:
Go1func GetImage(c *gin.Context) { 2 id, err := strconv.Atoi(c.Param("id")) 3 if err != nil { 4 c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) 5 return 6 } 7 if success, imagePath := services.RetrieveImage(id); success { 8 c.File(imagePath) 9 } else { 10 c.JSON(http.StatusNotFound, gin.H{"error": "Image not found"}) 11 } 12}
services.go
:
Go1func RetrieveImage(id int) (bool, string) { 2 for _, todo := range todos { 3 if todo.ID == id { 4 if todo.ImagePath != "" { 5 return true, todo.ImagePath 6 } 7 break 8 } 9 } 10 return false, "" 11}
The GET
route allows retrieval of the image associated with a specific ToDo item by its ID. After converting the ID, the code searches for the matching ToDo item. We leverage c.Param("id")
to retrieve the ID from the URL. The c.File(todo.ImagePath)
method from *gin.Context
is used to send a file in the HTTP response, enabling the image retrieval. If no image is found for the provided ID, an error message is returned. This structured approach ensures both successful image retrieval and appropriate error handling when issues arise.
When enhancing a web application with file upload capabilities, it's vital to secure this functionality against path traversal attacks. These attacks exploit vulnerabilities in the file system by providing a file path with malicious components, potentially enabling attackers to access unauthorized areas of the server's file hierarchy. To mitigate this risk, ensure user-supplied file paths are properly sanitized by extracting only the essential parts — specifically, the base name — before saving files to the server.
The relevant lines of code in the HandleImageUpload
function defined above are the following:
Go1fileName := filepath.Base(file.Filename) 2filePath := filepath.Join("uploads", fileName)
- Utilizing
filepath.Base
: The function usesfilepath.Base
to ensure that files are stored using only their base names, effectively mitigating directory component manipulation that could lead to path traversal. For example, iffilename
is"images/photo.jpg"
,filepath.Base(filename)
returns"photo.jpg"
, stripping away potentially harmful directory components. - Generating unique filenames and restricting access permissions: To enhance security against common threats, it's important to generate unique filenames for uploaded files and restrict access permissions to the uploads directory, ensuring that only authorized operations can be performed within this directory.
In this lesson, we expanded on our ToDo app's functionality by adding image support. You learned how to modify the ToDo model, set up routes for image upload and retrieval, and handle file paths effectively using the Gin
framework. This enhancement allows each ToDo item to have a visual representation, enriching user interaction.
Congratulations on completing the final lesson of the course! You've gained valuable knowledge in building and extending a functional web application. Now it’s time to put your skills into practice with the exercises that follow. Well done on reaching this milestone!