Skip to content

Commit

Permalink
Image endpoints (#64)
Browse files Browse the repository at this point in the history
`GET` endpoint:
- Added new handler for fetching previously uploaded images from Urchin.
- Added structs to allow handling of responses with compiler support.
- Added required functions for the database to fetch images.

`DELETE` endpoint:
- Added new handler for deleting previously uploaded images from Urchin.
- Added required functions for the database to delete images.

General update:
- Added structs to allow handling of responses with compiler support.
- Restructured API to used path variables for the delete endpoints.

---------

Co-authored-by: matheusgomes28 <[email protected]>
  • Loading branch information
AlDu2407 and matheusgomes28 authored Mar 29, 2024
1 parent cf04c2f commit 5372b61
Show file tree
Hide file tree
Showing 26 changed files with 781 additions and 235 deletions.
32 changes: 32 additions & 0 deletions admin-app/admin_requests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package admin_app

import "github.com/matheusgomes28/urchin/common"

// Extracted all bindings and requests structs into a single package to
// organize the data in a simpler way. Every domain object supporting
// CRUD endpoints has their own structures to handle the http methods.

type DeletePostBinding struct {
common.IntIdBinding
}

type AddPostRequest struct {
Title string `json:"title"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
}

type ChangePostRequest struct {
Id int `json:"id"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
}

type AddImageRequest struct {
Alt string `json:"alt"`
}

type DeleteImageBinding struct {
common.StringIdBinding
}
23 changes: 23 additions & 0 deletions admin-app/admin_responses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package admin_app

type PostIdResponse struct {
Id int `json:"id"`
}

type GetPostResponse struct {
Id int `json:"id"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
}

type ImageIdResponse struct {
Id string `json:"id"`
}

type GetImageResponse struct {
Id string `json:"id"`
Name string `json:"name"`
AltText string `json:"alt_text"`
Extension string `json:"extension"`
}
28 changes: 3 additions & 25 deletions admin-app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,6 @@ import (
"github.com/matheusgomes28/urchin/database"
)

type PostBinding struct {
Id string `uri:"id" binding:"required"`
}

type AddPostRequest struct {
Title string `json:"title"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
}

type ChangePostRequest struct {
Id int `json:"id"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
}

type DeletePostRequest struct {
Id int `json:"id"`
}

func SetupRoutes(app_settings common.AppSettings, database database.Database) *gin.Engine {

r := gin.Default()
Expand All @@ -35,12 +14,11 @@ func SetupRoutes(app_settings common.AppSettings, database database.Database) *g
r.GET("/posts/:id", getPostHandler(database))
r.POST("/posts", postPostHandler(database))
r.PUT("/posts", putPostHandler(database))
r.DELETE("/posts", deletePostHandler(database))
r.DELETE("/posts/:id", deletePostHandler(database))

// CRUD images
// r.GET("/images/:id", getImageHandler(&database))
r.GET("/images/:id", getImageHandler(database))
r.POST("/images", postImageHandler(app_settings, database))
// r.DELETE("/images", deleteImageHandler(&database))
r.DELETE("/images/:id", deleteImageHandler(app_settings, database))

return r
}
114 changes: 75 additions & 39 deletions admin-app/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,37 @@ import (
"github.com/rs/zerolog/log"
)

type AddImageRequest struct {
Alt string `json:"alt"`
}

// TODO : need these endpoints
// r.GET("/images/:id", getImageHandler(&database))
// r.POST("/images", postImageHandler(&database))
// r.DELETE("/images", deleteImageHandler(&database))
// func getImageHandler(database *database.Database) func(*gin.Context) {
// return func(c *gin.Context) {
// // Get the image from database
// }
// }

func getImageHandler(database database.Database) func(*gin.Context) {
return func(c *gin.Context) {
var get_image_binding common.ImageIdBinding
if err := c.ShouldBindUri(&get_image_binding); err != nil {
c.JSON(http.StatusBadRequest, common.ErrorRes("could not get image id", err))
return
}

image, err := database.GetImage(get_image_binding.Id)
if err != nil {
log.Error().Msgf("failed to get image: %v", err)
c.JSON(http.StatusNotFound, common.ErrorRes("could not get image", err))
return
}

c.JSON(http.StatusOK, image)
}
}

func postImageHandler(app_settings common.AppSettings, database database.Database) func(*gin.Context) {
return func(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10*1000000)
form, err := c.MultipartForm()
if err != nil {
log.Error().Msgf("could not create multipart form: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "request type must be multipart form",
"msg": err.Error(),
})
c.JSON(http.StatusBadRequest, common.ErrorRes("request type must be `multipart-form`", err))
return
}

Expand All @@ -58,34 +65,27 @@ func postImageHandler(app_settings common.AppSettings, database database.Databas
})
return
}
file := file_array[0]

file := file_array[0]
allowed_types := []string{"image/jpeg", "image/png", "image/gif"}
file_content_type := file.Header.Get("content-type")
if !slices.Contains(allowed_types, file_content_type) {
log.Error().Msgf("file type not supported")
c.JSON(http.StatusBadRequest, gin.H{
"error": "file type not supported",
})
c.JSON(http.StatusBadRequest, common.MsgErrorRes("file type not supported"))
return
}

detected_content_type, err := checkContentTypeMatchesData(file)
detected_content_type, err := getContentType(file)
if err != nil || detected_content_type != file_content_type {
log.Error().Msgf("the provided file does not match the provided content type")
c.JSON(http.StatusBadRequest, gin.H{
"error": "provided file content is not allowed",
})
c.JSON(http.StatusBadRequest, common.MsgErrorRes("provided file content is not allowed"))
return
}

uuid, err := uuid.New()
if err != nil {
log.Error().Msgf("could not create the UUID: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "cannot create unique identifier",
"msg": err.Error(),
})
c.JSON(http.StatusInternalServerError, common.ErrorRes("cannot create unique identifier", err))
return
}

Expand All @@ -94,45 +94,77 @@ func postImageHandler(app_settings common.AppSettings, database database.Databas
// check ext is supported
if ext == "" && slices.Contains(allowed_extensions, ext) {
log.Error().Msgf("file extension is not supported %v", err)
c.JSON(http.StatusBadRequest, common.ErrorRes("file extension is not supported", err))
return
}

filename := fmt.Sprintf("%s.%s", uuid.String(), ext)
filename := fmt.Sprintf("%s%s", uuid.String(), ext)
image_path := filepath.Join(app_settings.ImageDirectory, filename)
err = c.SaveUploadedFile(file, image_path)
if err != nil {
log.Error().Msgf("could not save file: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to upload image",
"msg": err.Error(),
})
c.JSON(http.StatusInternalServerError, common.ErrorRes("failed to upload image", err))
return
}
// End saving to filesystem

// Save metadata into the DB
err = database.AddImage(uuid.String(), file.Filename, alt_text)
err = database.AddImage(uuid.String(), file.Filename, alt_text, ext)
if err != nil {
log.Error().Msgf("could not add image metadata to db: %v", err)
os_err := os.Remove(image_path)
if os_err != nil {
log.Error().Msgf("could not remove image: %v", err)
err = errors.Join(err, os_err)
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to save image",
"msg": err.Error(),
})
c.JSON(http.StatusInternalServerError, common.ErrorRes("failed to save image", err))
return
}

c.JSON(http.StatusOK, gin.H{
"id": uuid.String(),
c.JSON(http.StatusOK, ImageIdResponse{
Id: uuid.String(),
})
}
}

func checkContentTypeMatchesData(file_header *multipart.FileHeader) (string, error) {
func deleteImageHandler(app_settings common.AppSettings, database database.Database) func(*gin.Context) {
return func(c *gin.Context) {
var delete_image_binding DeleteImageBinding
err := c.ShouldBindUri(&delete_image_binding)
if err != nil {
c.JSON(http.StatusBadRequest, common.ErrorRes("no id provided to delete image", err))
return
}

image, err := database.GetImage(delete_image_binding.Id)
if err != nil {
log.Error().Msgf("failed to delete image: %v", err)
c.JSON(http.StatusNotFound, common.ErrorRes("could not delete image", err))
return
}

filename := fmt.Sprintf("%s%s", image.Uuid, image.Ext)
image_path := filepath.Join(app_settings.ImageDirectory, filename)
err = os.Remove(image_path)
if err != nil {
log.Warn().Msgf("could not delete stored image file: %v", err)
// No return because we have to remove the database entry nonetheless.
}

err = database.DeleteImage(delete_image_binding.Id)
if err != nil {
log.Error().Msgf("failed to delete image with id %s: %v", delete_image_binding.Id, err)
c.JSON(http.StatusNotFound, common.ErrorRes("could not delete image", err))
return
}

c.JSON(http.StatusOK, ImageIdResponse{
delete_image_binding.Id,
})
}
}

func getContentType(file_header *multipart.FileHeader) (string, error) {
// Check if the content matches the provided type.
image_file, err := file_header.Open()
if err != nil {
Expand All @@ -147,5 +179,9 @@ func checkContentTypeMatchesData(file_header *multipart.FileHeader) (string, err
log.Error().Msgf("could not read into temp buffer")
return "", read_err
}
return http.DetectContentType(tmp_buffer), nil
return getContentTypeFromData(tmp_buffer), nil
}

func getContentTypeFromData(data []byte) string {
return http.DetectContentType(data[:512])
}
Loading

0 comments on commit 5372b61

Please sign in to comment.