From 9da51dc4e872e77b4a1a71c73a3174d620f6c759 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Wed, 22 Jan 2025 17:25:02 +0700 Subject: [PATCH] feat: update magazine thumbnail and article --- app/database/entity/articles.entity.go | 1 + app/database/entity/magazines.entity.go | 1 + app/module/articles/mapper/articles.mapper.go | 1 + .../articles/request/articles.request.go | 5 + .../articles/response/articles.response.go | 1 + .../controller/magazines.controller.go | 42 +++++ app/module/magazines/magazines.module.go | 2 + .../repository/magazines.repository.go | 10 ++ .../magazines/service/magazines.service.go | 160 +++++++++++++++++- docs/swagger/docs.go | 114 +++++++++++++ docs/swagger/swagger.json | 114 +++++++++++++ docs/swagger/swagger.yaml | 73 ++++++++ 12 files changed, 523 insertions(+), 1 deletion(-) diff --git a/app/database/entity/articles.entity.go b/app/database/entity/articles.entity.go index bdc9ba9..67705b8 100644 --- a/app/database/entity/articles.entity.go +++ b/app/database/entity/articles.entity.go @@ -17,6 +17,7 @@ type Articles struct { CreatedById *uint `json:"created_by_id" gorm:"type:int4"` ShareCount *int `json:"share_count" gorm:"type:int4"` ViewCount *int `json:"view_count" gorm:"type:int4"` + AiArticleId *int `json:"ai_article_id" gorm:"type:int4"` DownloadCount *int `json:"download_count" gorm:"type:int4"` StatusId *int `json:"status_id" gorm:"type:int4"` OldId *uint `json:"old_id" gorm:"type:int4"` diff --git a/app/database/entity/magazines.entity.go b/app/database/entity/magazines.entity.go index f5211e2..3cfc206 100644 --- a/app/database/entity/magazines.entity.go +++ b/app/database/entity/magazines.entity.go @@ -6,6 +6,7 @@ type Magazines struct { ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` Title string `json:"title" gorm:"type:varchar"` Description string `json:"description" gorm:"type:varchar"` + ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"` ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"` ThumbnailUrl *string `json:"thumbnail_url" gorm:"type:varchar"` PageUrl *string `json:"page_url" gorm:"type:varchar"` diff --git a/app/module/articles/mapper/articles.mapper.go b/app/module/articles/mapper/articles.mapper.go index b0f14b5..621bef2 100644 --- a/app/module/articles/mapper/articles.mapper.go +++ b/app/module/articles/mapper/articles.mapper.go @@ -60,6 +60,7 @@ func ArticlesResponseMapper( TypeId: articlesReq.TypeId, Tags: articlesReq.Tags, CategoryId: articlesReq.CategoryId, + AiArticleId: articlesReq.AiArticleId, CategoryName: categoryName, PageUrl: articlesReq.PageUrl, CreatedById: articlesReq.CreatedById, diff --git a/app/module/articles/request/articles.request.go b/app/module/articles/request/articles.request.go index a5632c5..d39dc79 100644 --- a/app/module/articles/request/articles.request.go +++ b/app/module/articles/request/articles.request.go @@ -31,6 +31,7 @@ type ArticlesCreateRequest struct { CategoryIds string `json:"categoryIds" validate:"required"` TypeId int `json:"typeId" validate:"required"` Tags string `json:"tags" validate:"required"` + AiArticleId *int `json:"aiArticleId"` OldId *uint `json:"oldId"` } @@ -42,6 +43,7 @@ func (req ArticlesCreateRequest) ToEntity() *entity.Articles { HtmlDescription: req.HtmlDescription, TypeId: req.TypeId, Tags: req.Tags, + AiArticleId: req.AiArticleId, OldId: req.OldId, } } @@ -55,6 +57,7 @@ type ArticlesUpdateRequest struct { TypeId int `json:"typeId" validate:"required"` Tags string `json:"tags" validate:"required"` CreatedById *uint `json:"createdById"` + AiArticleId *int `json:"aiArticleId"` StatusId *int `json:"statusId"` } @@ -68,6 +71,7 @@ func (req ArticlesUpdateRequest) ToEntity() *entity.Articles { TypeId: req.TypeId, Tags: req.Tags, StatusId: req.StatusId, + AiArticleId: req.AiArticleId, UpdatedAt: time.Now(), } } else { @@ -80,6 +84,7 @@ func (req ArticlesUpdateRequest) ToEntity() *entity.Articles { Tags: req.Tags, StatusId: req.StatusId, CreatedById: req.CreatedById, + AiArticleId: req.AiArticleId, UpdatedAt: time.Now(), } } diff --git a/app/module/articles/response/articles.response.go b/app/module/articles/response/articles.response.go index 1d148d8..702f057 100644 --- a/app/module/articles/response/articles.response.go +++ b/app/module/articles/response/articles.response.go @@ -23,6 +23,7 @@ type ArticlesResponse struct { ShareCount *int `json:"shareCount"` ViewCount *int `json:"viewCount"` DownloadCount *int `json:"downloadCount"` + AiArticleId *int `json:"aiArticleId"` StatusId *int `json:"statusId"` IsPublish *bool `json:"isPublish"` PublishedAt *time.Time `json:"publishedAt"` diff --git a/app/module/magazines/controller/magazines.controller.go b/app/module/magazines/controller/magazines.controller.go index c4b41ee..8866402 100644 --- a/app/module/magazines/controller/magazines.controller.go +++ b/app/module/magazines/controller/magazines.controller.go @@ -20,6 +20,8 @@ type MagazinesController interface { Show(c *fiber.Ctx) error Save(c *fiber.Ctx) error Update(c *fiber.Ctx) error + SaveThumbnail(c *fiber.Ctx) error + Viewer(c *fiber.Ctx) error Delete(c *fiber.Ctx) error } @@ -161,6 +163,46 @@ func (_i *magazinesController) Update(c *fiber.Ctx) error { }) } +// SaveThumbnail Magazines +// @Summary Save Thumbnail Magazines +// @Description API for Save Thumbnail of Magazines +// @Tags Articles +// @Security Bearer +// @Produce json +// @Param files formData file true "Upload thumbnail" +// @Param id path int true "Magazine ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /magazines/thumbnail/{id} [post] +func (_i *magazinesController) SaveThumbnail(c *fiber.Ctx) error { + err := _i.magazinesService.SaveThumbnail(c) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Thumbnail of Magazines successfully created"}, + }) +} + +// Viewer Magazines +// @Summary Viewer Magazines Thumbnail +// @Description API for View Thumbnail of Magazines +// @Tags Articles +// @Security Bearer +// @Param thumbnailName path string true "Magazines Thumbnail Name" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /magazines/thumbnail/viewer/{thumbnailName} [get] +func (_i *magazinesController) Viewer(c *fiber.Ctx) error { + return _i.magazinesService.Viewer(c) +} + // Delete Magazines // @Summary Delete Magazines // @Description API for delete Magazines diff --git a/app/module/magazines/magazines.module.go b/app/module/magazines/magazines.module.go index 03f9548..a172b65 100644 --- a/app/module/magazines/magazines.module.go +++ b/app/module/magazines/magazines.module.go @@ -48,6 +48,8 @@ func (_i *MagazinesRouter) RegisterMagazinesRoutes() { router.Get("/:id", magazinesController.Show) router.Post("/", magazinesController.Save) router.Put("/:id", magazinesController.Update) + router.Post("/thumbnail/:id", magazinesController.SaveThumbnail) + router.Get("/thumbnail/viewer/:thumbnailName", magazinesController.Viewer) router.Delete("/:id", magazinesController.Delete) }) } diff --git a/app/module/magazines/repository/magazines.repository.go b/app/module/magazines/repository/magazines.repository.go index 121389f..cc9e885 100644 --- a/app/module/magazines/repository/magazines.repository.go +++ b/app/module/magazines/repository/magazines.repository.go @@ -17,6 +17,7 @@ type magazinesRepository struct { type MagazinesRepository interface { GetAll(req request.MagazinesQueryRequest) (magaziness []*entity.Magazines, paging paginator.Pagination, err error) FindOne(id uint) (magazines *entity.Magazines, err error) + FindByFilename(thumbnailName string) (magazineReturn *entity.Magazines, err error) Create(magazines *entity.Magazines) (magazineReturn *entity.Magazines, err error) Update(id uint, magazines *entity.Magazines) (err error) Delete(id uint) (err error) @@ -83,6 +84,15 @@ func (_i *magazinesRepository) FindOne(id uint) (magazines *entity.Magazines, er return magazines, nil } +func (_i *magazinesRepository) FindByFilename(thumbnailName string) (magazines *entity.Magazines, err error) { + + if err := _i.DB.DB.Where("thumbnail_name = ?", thumbnailName).First(&magazines).Error; err != nil { + return nil, err + } + + return magazines, nil +} + func (_i *magazinesRepository) Create(magazines *entity.Magazines) (magazineReturn *entity.Magazines, err error) { result := _i.DB.DB.Create(magazines) return magazines, result.Error diff --git a/app/module/magazines/service/magazines.service.go b/app/module/magazines/service/magazines.service.go index 669cf70..cca4806 100644 --- a/app/module/magazines/service/magazines.service.go +++ b/app/module/magazines/service/magazines.service.go @@ -1,6 +1,9 @@ package service import ( + "context" + "github.com/gofiber/fiber/v2" + "github.com/minio/minio-go/v7" "github.com/rs/zerolog" "go-humas-be/app/database/entity" magazineFilesRepository "go-humas-be/app/module/magazine_files/repository" @@ -9,8 +12,17 @@ import ( "go-humas-be/app/module/magazines/request" "go-humas-be/app/module/magazines/response" usersRepository "go-humas-be/app/module/users/repository" + minioStorage "go-humas-be/config/config" "go-humas-be/utils/paginator" utilSvc "go-humas-be/utils/service" + "io" + "log" + "math/rand" + "mime" + "path/filepath" + "strconv" + "strings" + "time" ) // MagazinesService @@ -18,6 +30,7 @@ type magazinesService struct { Repo repository.MagazinesRepository UsersRepo usersRepository.UsersRepository MagazineFilesRepo magazineFilesRepository.MagazineFilesRepository + MinioStorage *minioStorage.MinioStorage Log zerolog.Logger } @@ -27,16 +40,19 @@ type MagazinesService interface { Show(id uint) (magazines *response.MagazinesResponse, err error) Save(req request.MagazinesCreateRequest, authToken string) (magazines *entity.Magazines, err error) Update(id uint, req request.MagazinesUpdateRequest) (err error) + SaveThumbnail(c *fiber.Ctx) (err error) + Viewer(c *fiber.Ctx) (err error) Delete(id uint) error } // NewMagazinesService init MagazinesService -func NewMagazinesService(repo repository.MagazinesRepository, magazineFilesRepo magazineFilesRepository.MagazineFilesRepository, usersRepo usersRepository.UsersRepository, log zerolog.Logger) MagazinesService { +func NewMagazinesService(repo repository.MagazinesRepository, magazineFilesRepo magazineFilesRepository.MagazineFilesRepository, usersRepo usersRepository.UsersRepository, minioStorage *minioStorage.MinioStorage, log zerolog.Logger) MagazinesService { return &magazinesService{ Repo: repo, MagazineFilesRepo: magazineFilesRepo, UsersRepo: usersRepo, + MinioStorage: minioStorage, Log: log, } } @@ -79,6 +95,78 @@ func (_i *magazinesService) Save(req request.MagazinesCreateRequest, authToken s return saveMagazineResponse, nil } +func (_i *magazinesService) SaveThumbnail(c *fiber.Ctx) (err error) { + + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:magazinesService", "Methods:SaveThumbnail"). + Interface("id", id).Msg("") + + bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + + form, err := c.MultipartForm() + if err != nil { + return err + } + files := form.File["files"] + + // Create minio connection. + minioClient, err := _i.MinioStorage.ConnectMinio() + if err != nil { + // Return status 500 and minio connection error. + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + // Iterasi semua file yang diunggah + for _, file := range files { + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop1"). + Interface("data", file).Msg("") + + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + filename := filepath.Base(file.Filename) + filename = strings.ReplaceAll(filename, " ", "") + filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) + extension := filepath.Ext(file.Filename)[1:] + + rand.New(rand.NewSource(time.Now().UnixNano())) + randUniqueId := rand.Intn(1000000) + + newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) + newFilename := newFilenameWithoutExt + "." + extension + objectName := "magazines/thumbnail/" + newFilename + + findCategory, err := _i.Repo.FindOne(uint(id)) + findCategory.ThumbnailName = &newFilename + findCategory.ThumbnailPath = &objectName + err = _i.Repo.Update(uint(id), findCategory) + if err != nil { + return err + } + + // Upload file ke MinIO + _, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, file.Size, minio.PutObjectOptions{}) + if err != nil { + return err + } + } + + return +} + func (_i *magazinesService) Update(id uint, req request.MagazinesUpdateRequest) (err error) { _i.Log.Info().Interface("data", req).Msg("") return _i.Repo.Update(id, req.ToEntity()) @@ -87,3 +175,73 @@ func (_i *magazinesService) Update(id uint, req request.MagazinesUpdateRequest) func (_i *magazinesService) Delete(id uint) error { return _i.Repo.Delete(id) } + +func (_i *magazinesService) Viewer(c *fiber.Ctx) (err error) { + thumbnailName := c.Params("thumbnailName") + + emptyImage := "empty-image.jpg" + searchThumbnail := emptyImage + if thumbnailName != emptyImage { + result, err := _i.Repo.FindByFilename(thumbnailName) + if err != nil { + return err + } + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "magazinesService:Viewer"). + Interface("resultThumbnail", result.ThumbnailPath).Msg("") + + if result.ThumbnailPath != nil { + searchThumbnail = *result.ThumbnailPath + } else { + searchThumbnail = "magazines/thumbnail/" + emptyImage + } + } else { + searchThumbnail = "magazines/thumbnail/" + emptyImage + } + + ctx := context.Background() + bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + objectName := searchThumbnail + + // Create minio connection. + minioClient, err := _i.MinioStorage.ConnectMinio() + if err != nil { + // Return status 500 and minio connection error. + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{}) + if err != nil { + log.Fatalln(err) + } + defer fileContent.Close() + + contentType := mime.TypeByExtension("." + getFileExtension(objectName)) + if contentType == "" { + contentType = "application/octet-stream" + } + + c.Set("Content-Type", contentType) + + if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil { + return err + } + + return +} + +func getFileExtension(filename string) string { + // split file name + parts := strings.Split(filename, ".") + + // jika tidak ada ekstensi, kembalikan string kosong + if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") { + return "" + } + + // ambil ekstensi terakhir + return parts[len(parts)-1] +} diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index c6e40cc..52a01ad 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -3275,6 +3275,114 @@ const docTemplate = `{ } } }, + "/magazines/thumbnail/viewer/{thumbnailName}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for View Thumbnail of Magazines", + "tags": [ + "Articles" + ], + "summary": "Viewer Magazines Thumbnail", + "parameters": [ + { + "type": "string", + "description": "Magazines Thumbnail Name", + "name": "thumbnailName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/magazines/thumbnail/{id}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for Save Thumbnail of Magazines", + "produces": [ + "application/json" + ], + "tags": [ + "Articles" + ], + "summary": "Save Thumbnail Magazines", + "parameters": [ + { + "type": "file", + "description": "Upload thumbnail", + "name": "files", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "Magazine ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/magazines/{id}": { "get": { "security": [ @@ -6412,6 +6520,9 @@ const docTemplate = `{ "typeId" ], "properties": { + "aiArticleId": { + "type": "integer" + }, "categoryIds": { "type": "string" }, @@ -6450,6 +6561,9 @@ const docTemplate = `{ "typeId" ], "properties": { + "aiArticleId": { + "type": "integer" + }, "categoryIds": { "type": "string" }, diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 2f20ab4..6bc6edd 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -3264,6 +3264,114 @@ } } }, + "/magazines/thumbnail/viewer/{thumbnailName}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for View Thumbnail of Magazines", + "tags": [ + "Articles" + ], + "summary": "Viewer Magazines Thumbnail", + "parameters": [ + { + "type": "string", + "description": "Magazines Thumbnail Name", + "name": "thumbnailName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/magazines/thumbnail/{id}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for Save Thumbnail of Magazines", + "produces": [ + "application/json" + ], + "tags": [ + "Articles" + ], + "summary": "Save Thumbnail Magazines", + "parameters": [ + { + "type": "file", + "description": "Upload thumbnail", + "name": "files", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "Magazine ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/magazines/{id}": { "get": { "security": [ @@ -6401,6 +6509,9 @@ "typeId" ], "properties": { + "aiArticleId": { + "type": "integer" + }, "categoryIds": { "type": "string" }, @@ -6439,6 +6550,9 @@ "typeId" ], "properties": { + "aiArticleId": { + "type": "integer" + }, "categoryIds": { "type": "string" }, diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 78da3cf..b801057 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -157,6 +157,8 @@ definitions: type: object request.ArticlesCreateRequest: properties: + aiArticleId: + type: integer categoryIds: type: string description: @@ -184,6 +186,8 @@ definitions: type: object request.ArticlesUpdateRequest: properties: + aiArticleId: + type: integer categoryIds: type: string createdById: @@ -2831,6 +2835,75 @@ paths: summary: Update Magazines tags: - Magazines + /magazines/thumbnail/{id}: + post: + description: API for Save Thumbnail of Magazines + parameters: + - description: Upload thumbnail + in: formData + name: files + required: true + type: file + - description: Magazine ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Save Thumbnail Magazines + tags: + - Articles + /magazines/thumbnail/viewer/{thumbnailName}: + get: + description: API for View Thumbnail of Magazines + parameters: + - description: Magazines Thumbnail Name + in: path + name: thumbnailName + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Viewer Magazines Thumbnail + tags: + - Articles /master-menus: get: description: API for getting all MasterMenus