merch images crud
This commit is contained in:
parent
e708c92d18
commit
dec89435a3
5 changed files with 387 additions and 13 deletions
|
|
@ -1,23 +1,27 @@
|
|||
package merch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"merch-parser-api/internal/interfaces"
|
||||
"merch-parser-api/pkg/responses"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type controller struct {
|
||||
service *service
|
||||
utils interfaces.Utils
|
||||
expires time.Duration
|
||||
}
|
||||
|
||||
func newController(service *service, utils interfaces.Utils) *controller {
|
||||
func newController(service *service, utils interfaces.Utils, expires time.Duration) *controller {
|
||||
return &controller{
|
||||
service: service,
|
||||
utils: utils,
|
||||
expires: expires,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +38,10 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, ref
|
|||
chartsGroup.GET("", h.controller.getChartsPrices)
|
||||
chartsGroup.GET("/:uuid", h.controller.getDistinctPrices)
|
||||
|
||||
imagesGroup := merchGroup.Group("/images")
|
||||
imagesGroup.POST("/:uuid", h.controller.uploadMerchImage)
|
||||
imagesGroup.GET("/:uuid", h.controller.getMerchImage)
|
||||
imagesGroup.DELETE("/:uuid", h.controller.deleteMerchImage)
|
||||
}
|
||||
|
||||
// @Summary Добавить новый мерч
|
||||
|
|
@ -134,10 +142,10 @@ func (co *controller) getAllMerch(c *gin.Context) {
|
|||
// @Description Обновить информацию про мерч по его uuid в json-е
|
||||
// @Tags Merch
|
||||
// @Security BearerAuth
|
||||
// @Param body body UpdateMerchDTO true "merch_uuid"
|
||||
// @Param body body UpdateMerchDTO true "merch_uuid"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} responses.ErrorResponse400
|
||||
// @Failure 500 {object} responses.ErrorResponse500
|
||||
// @Failure 400 {object} responses.ErrorResponse400
|
||||
// @Failure 500 {object} responses.ErrorResponse500
|
||||
// @Router /merch/ [put]
|
||||
func (co *controller) updateMerch(c *gin.Context) {
|
||||
var payload UpdateMerchDTO
|
||||
|
|
@ -227,10 +235,10 @@ func (co *controller) getChartsPrices(c *gin.Context) {
|
|||
// @Tags Merch
|
||||
// @Security BearerAuth
|
||||
// @Param uuid path string true "merch_uuid"
|
||||
// @Param days query string false "period in days"
|
||||
// @Success 200 {object} PricesResponse
|
||||
// @Failure 400 {object} responses.ErrorResponse400
|
||||
// @Failure 500 {object} responses.ErrorResponse500
|
||||
// @Param days query string false "period in days"
|
||||
// @Success 200 {object} PricesResponse
|
||||
// @Failure 400 {object} responses.ErrorResponse400
|
||||
// @Failure 500 {object} responses.ErrorResponse500
|
||||
// @Router /prices/{uuid} [get]
|
||||
func (co *controller) getDistinctPrices(c *gin.Context) {
|
||||
daysQuery := strings.ToLower(c.DefaultQuery("days", ""))
|
||||
|
|
@ -257,3 +265,138 @@ func (co *controller) getDistinctPrices(c *gin.Context) {
|
|||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// @Summary Загрузить картинки по merch_uuid и query параметрам
|
||||
// @Description Загрузить картинки по merch_uuid и query параметрам
|
||||
// @Tags Merch images
|
||||
// @Security BearerAuth
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param uuid path string true "Merch UUID"
|
||||
// @Param file formData file true "Image file"
|
||||
// @Param imageType formData string true "Image type: thumbnail, full or all" Enums(thumbnail, full, all)
|
||||
// @Success 200
|
||||
// @Failure 400 {object} responses.ErrorResponse400
|
||||
// @Failure 500 {object} responses.ErrorResponse500
|
||||
// @Router /merch/images/{uuid} [post]
|
||||
func (co *controller) uploadMerchImage(c *gin.Context) {
|
||||
userUuid, err := co.utils.GetUserUuidFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
|
||||
log.WithError(err).Error("Merch | Failed to get user uuid from context")
|
||||
return
|
||||
}
|
||||
|
||||
merchUuid := c.Param("uuid")
|
||||
if merchUuid == "" {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
|
||||
log.Error("Merch | Failed to get single merch")
|
||||
return
|
||||
}
|
||||
|
||||
imageType := c.PostForm("imageType")
|
||||
types := map[string]struct{}{"thumbnail": {}, "full": {}, "all": {}}
|
||||
if _, allowed := types[imageType]; !allowed {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "imageType must be one of: thumbnail, full, all"})
|
||||
log.WithError(err).Error("Merch | imageType must be one of: thumbnail, full, all")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "file is required"})
|
||||
log.WithError(err).Error("Merch | File is required")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
|
||||
defer cancel()
|
||||
|
||||
err = co.service.uploadMerchImage(ctx, userUuid, merchUuid, imageType, file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
|
||||
log.WithError(err).Error("Merch | Failed to upload merch image")
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// @Summary Получить картинки по merch_uuid и query параметрам
|
||||
// @Description Получить картинки по merch_uuid и query параметрам
|
||||
// @Tags Merch images
|
||||
// @Security BearerAuth
|
||||
// @Param uuid path string true "merch_uuid"
|
||||
// @Param type query string true "image type"
|
||||
// @Success 200 {object} ImageLink
|
||||
// @Failure 400 {object} responses.ErrorResponse400
|
||||
// @Failure 500 {object} responses.ErrorResponse500
|
||||
// @Router /merch/images/{uuid} [get]
|
||||
func (co *controller) getMerchImage(c *gin.Context) {
|
||||
typeQuery := strings.ToLower(c.Query("type"))
|
||||
if typeQuery == "" {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "Image type query param is empty"})
|
||||
return
|
||||
}
|
||||
|
||||
userUuid, err := co.utils.GetUserUuidFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
|
||||
log.WithError(err).Error("Merch | Failed to get user uuid from context")
|
||||
return
|
||||
}
|
||||
|
||||
merchUuid := c.Param("uuid")
|
||||
if merchUuid == "" {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
|
||||
log.WithError(err).Error("Merch | Failed to get single merch")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
|
||||
defer cancel()
|
||||
|
||||
link, err := co.service.getMerchImage(ctx, userUuid, merchUuid, typeQuery)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
|
||||
log.WithError(err).Error("Merch | Failed to get merch image")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, ImageLink{Link: link.String()})
|
||||
}
|
||||
|
||||
// @Summary Удалить (безвозвратно) картинки по merch_uuid и query параметрам
|
||||
// @Description Удалить (безвозвратно) картинки по merch_uuid и query параметрам
|
||||
// @Tags Merch images
|
||||
// @Security BearerAuth
|
||||
// @Param uuid path string true "merch_uuid"
|
||||
// @Param type query string true "image type"
|
||||
// @Success 200 {object} PricesResponse
|
||||
// @Failure 400 {object} responses.ErrorResponse400
|
||||
// @Failure 500 {object} responses.ErrorResponse500
|
||||
// @Router /merch/images/{uuid} [delete]
|
||||
func (co *controller) deleteMerchImage(c *gin.Context) {
|
||||
userUuid, err := co.utils.GetUserUuidFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
|
||||
log.WithError(err).Error("Merch | Failed to get user uuid from context")
|
||||
return
|
||||
}
|
||||
|
||||
merchUuid := c.Param("uuid")
|
||||
if merchUuid == "" {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
|
||||
log.WithError(err).Error("Merch | Failed to get single merch")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
|
||||
defer cancel()
|
||||
|
||||
if err := co.service.deleteMerchImage(ctx, userUuid, merchUuid); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
|
||||
log.WithError(err).Error("Merch | Failed to delete merch image")
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,3 +53,7 @@ type UpdateMerchDTO struct {
|
|||
Origin string `json:"origin"`
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
||||
type ImageLink struct {
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package merch
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"merch-parser-api/internal/interfaces"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
|
@ -14,12 +16,25 @@ type Handler struct {
|
|||
type Deps struct {
|
||||
DB *gorm.DB
|
||||
Utils interfaces.Utils
|
||||
Media interfaces.MediaStorage
|
||||
}
|
||||
|
||||
func NewHandler(deps Deps) *Handler {
|
||||
packageBucketName := "user-merch-images"
|
||||
expires := time.Minute * 1
|
||||
|
||||
r := NewRepo(deps.DB)
|
||||
s := newService(r)
|
||||
c := newController(s, deps.Utils)
|
||||
s := newService(r, deps.Media, packageBucketName, expires)
|
||||
c := newController(s, deps.Utils, expires)
|
||||
|
||||
media := deps.Media
|
||||
log.WithFields(log.Fields{
|
||||
"addr": media,
|
||||
}).Debug("Merch handler constructor | Media provider")
|
||||
|
||||
if err := media.СreateBucketIfNotExists(packageBucketName); err != nil {
|
||||
log.WithError(err).Fatal("Merch handler constructor | Failed to ensure bucket exists")
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
repo: r,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func NewRepo(db *gorm.DB) *Repo {
|
|||
type repository interface {
|
||||
addMerch(bundle merchBundle) error
|
||||
|
||||
merchRecordExists(userUuid, merchUuid string) (bool, error)
|
||||
getSingleMerch(userUuid, merchUuid string) (merchBundle, error)
|
||||
getAllMerch(userUuid string) ([]ListResponse, error)
|
||||
|
||||
|
|
@ -54,6 +55,19 @@ func (r *Repo) addMerch(bundle merchBundle) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) merchRecordExists(userUuid, merchUuid string) (bool, error) {
|
||||
var exists bool
|
||||
err := r.db.Raw(`
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM merch
|
||||
WHERE user_uuid = ?
|
||||
AND merch_uuid = ?
|
||||
);`, userUuid, merchUuid).Scan(&exists).Error
|
||||
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (r *Repo) getSingleMerch(userUuid, merchUuid string) (merchBundle, error) {
|
||||
var merch Merch
|
||||
if err := r.db.
|
||||
|
|
|
|||
|
|
@ -1,22 +1,49 @@
|
|||
package merch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"merch-parser-api/internal/interfaces"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
repo repository
|
||||
repo repository
|
||||
media interfaces.MediaStorage
|
||||
bucketName string
|
||||
expires time.Duration
|
||||
}
|
||||
|
||||
func newService(repo repository) *service {
|
||||
func newService(repo repository, media interfaces.MediaStorage, bucketName string, expires time.Duration) *service {
|
||||
return &service{
|
||||
repo: repo,
|
||||
repo: repo,
|
||||
media: media,
|
||||
bucketName: bucketName,
|
||||
expires: expires,
|
||||
}
|
||||
}
|
||||
|
||||
type uploadImageParams struct {
|
||||
ctx context.Context
|
||||
src io.Reader
|
||||
imageType string
|
||||
object string
|
||||
quality int
|
||||
}
|
||||
|
||||
func (s *service) addMerch(payload MerchDTO, userUuid string) error {
|
||||
merchUuid := uuid.NewString()
|
||||
|
||||
|
|
@ -80,6 +107,13 @@ func (s *service) updateMerch(payload UpdateMerchDTO, userUuid string) error {
|
|||
}
|
||||
|
||||
func (s *service) deleteMerch(userUuid, merchUuid string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), s.expires)
|
||||
defer cancel()
|
||||
|
||||
if err := s.deleteMerchImage(ctx, userUuid, merchUuid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repo.deleteMerch(userUuid, merchUuid)
|
||||
}
|
||||
|
||||
|
|
@ -176,3 +210,167 @@ func (s *service) getDistinctPrices(userUuid, merchUuid, days string) (PricesRes
|
|||
Origins: []OriginWithPrices{originSurugaya, originMandarake},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *service) uploadMerchImage(ctx context.Context, userUuid, merchUuid, imageType string, file *multipart.FileHeader) error {
|
||||
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
|
||||
}
|
||||
|
||||
rawExt := filepath.Ext(file.Filename)
|
||||
if rawExt == "" {
|
||||
return errors.New("no file extension")
|
||||
}
|
||||
|
||||
ext := strings.ToLower(rawExt[1:])
|
||||
allowedTypes := map[string]struct{}{"jpeg": {}, "jpg": {}, "png": {}, "gif": {}}
|
||||
if _, ok := allowedTypes[ext]; !ok {
|
||||
return errors.New("invalid file type")
|
||||
}
|
||||
|
||||
getSrc := func() (io.ReadCloser, error) {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Merch | Failed to open file")
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
switch imageType {
|
||||
case "thumbnail":
|
||||
src, err := getSrc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s._uploadToStorage(uploadImageParams{
|
||||
ctx: ctx,
|
||||
src: src,
|
||||
imageType: "thumbnail",
|
||||
object: fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid),
|
||||
quality: 80,
|
||||
})
|
||||
|
||||
case "full":
|
||||
src, err := getSrc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s._uploadToStorage(uploadImageParams{
|
||||
ctx: ctx,
|
||||
src: src,
|
||||
imageType: "full",
|
||||
object: fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid),
|
||||
quality: 90,
|
||||
})
|
||||
|
||||
case "all":
|
||||
src, err := getSrc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s._uploadToStorage(uploadImageParams{
|
||||
ctx: ctx,
|
||||
src: src,
|
||||
imageType: "thumbnail",
|
||||
object: fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid),
|
||||
quality: 80,
|
||||
}); err != nil {
|
||||
log.WithError(err).Error("Merch | Upload thumbnail and full image")
|
||||
return err
|
||||
}
|
||||
|
||||
src2, err := getSrc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s._uploadToStorage(uploadImageParams{
|
||||
ctx: ctx,
|
||||
src: src2,
|
||||
imageType: "full",
|
||||
object: fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid),
|
||||
quality: 90,
|
||||
}); err != nil {
|
||||
log.WithError(err).Error("Merch | Upload thumbnail and full image")
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("invalid file type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) getMerchImage(ctx context.Context, userUuid, merchUuid, imageType string) (*url.URL, error) {
|
||||
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
|
||||
}
|
||||
|
||||
var object string
|
||||
switch imageType {
|
||||
case "thumbnail":
|
||||
object = fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid)
|
||||
case "full":
|
||||
object = fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown image type %s", imageType)
|
||||
}
|
||||
|
||||
return s.media.Get(ctx, s.bucketName, object, s.expires, nil)
|
||||
}
|
||||
|
||||
func (s *service) deleteMerchImage(ctx context.Context, userUuid, merchUuid string) error {
|
||||
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
|
||||
}
|
||||
|
||||
if err = s.media.Delete(ctx, s.bucketName, fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.media.Delete(ctx, s.bucketName, fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) _uploadToStorage(params uploadImageParams) error {
|
||||
img, _, err := image.Decode(params.src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
if params.imageType == "thumbnail" {
|
||||
img = imaging.Resize(img, 300, 300, imaging.Lanczos)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: params.quality}); err != nil {
|
||||
return fmt.Errorf("failed to encode full image: %w", err)
|
||||
}
|
||||
|
||||
err = s.media.Upload(params.ctx, s.bucketName, params.object, &buf, -1)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"img type": "full",
|
||||
}).Error("Merch | Failed to upload file to media storage")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue