image storage added

This commit is contained in:
nquidox 2025-10-26 19:54:34 +03:00
parent f5ca21ca68
commit 212ce0a5c4
6 changed files with 190 additions and 65 deletions

View file

@ -9,6 +9,7 @@ import (
"merch-parser-api/internal/api/user" "merch-parser-api/internal/api/user"
"merch-parser-api/internal/app" "merch-parser-api/internal/app"
"merch-parser-api/internal/grpcService" "merch-parser-api/internal/grpcService"
"merch-parser-api/internal/imagesProvider"
"merch-parser-api/internal/interfaces" "merch-parser-api/internal/interfaces"
"merch-parser-api/internal/mediaStorage" "merch-parser-api/internal/mediaStorage"
"merch-parser-api/internal/provider/auth" "merch-parser-api/internal/provider/auth"
@ -62,6 +63,8 @@ func main() {
"provider": mediaProvider, "provider": mediaProvider,
}).Debug("Media storage | Minio client created") }).Debug("Media storage | Minio client created")
imageProvider := imagesProvider.NewClient(c.ImageConf.Host + ":" + c.ImageConf.Port)
//deps providers //deps providers
routerHandler := router.NewRouter(router.Deps{ routerHandler := router.NewRouter(router.Deps{
ApiPrefix: c.AppConf.ApiPrefix, ApiPrefix: c.AppConf.ApiPrefix,
@ -91,9 +94,10 @@ func main() {
}) })
merchModule := merch.NewHandler(merch.Deps{ merchModule := merch.NewHandler(merch.Deps{
DB: database, DB: database,
Utils: utilsProvider, Utils: utilsProvider,
Media: mediaProvider, Media: mediaProvider,
ImageStorage: imageProvider,
}) })
//collect modules //collect modules

View file

@ -8,6 +8,7 @@ type Config struct {
JWTConf JWTConfig JWTConf JWTConfig
GrpcConf GrpcConfig GrpcConf GrpcConfig
MediaConf MediaConfig MediaConf MediaConfig
ImageConf ImageStorageConfig
} }
type AppConfig struct { type AppConfig struct {
@ -48,6 +49,11 @@ type MediaConfig struct {
Secure string Secure string
} }
type ImageStorageConfig struct {
Host string
Port string
}
func NewConfig() *Config { func NewConfig() *Config {
return &Config{ return &Config{
AppConf: AppConfig{ AppConf: AppConfig{
@ -87,5 +93,10 @@ func NewConfig() *Config {
Password: getEnv("MEDIA_STORAGE_PASSWORD", ""), Password: getEnv("MEDIA_STORAGE_PASSWORD", ""),
Secure: getEnv("MEDIA_STORAGE_SECURE", ""), Secure: getEnv("MEDIA_STORAGE_SECURE", ""),
}, },
ImageConf: ImageStorageConfig{
Host: getEnv("IMAGE_STORAGE_HOST", ""),
Port: getEnv("IMAGE_STORAGE_PORT", ""),
},
} }
} }

View file

@ -266,16 +266,15 @@ func (co *controller) getDistinctPrices(c *gin.Context) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
// @Summary Загрузить картинки по merch_uuid и query параметрам // @Summary Загрузить картинку по merch_uuid
// @Description Загрузить картинки по merch_uuid и query параметрам // @Description Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.
// @Tags Merch images // @Tags Merch images
// @Security BearerAuth // @Security BearerAuth
// @Accept multipart/form-data // @Accept multipart/form-data
// @Produce json // @Produce json
// @Param uuid path string true "Merch UUID" // @Param uuid path string true "Merch UUID"
// @Param file formData file true "Image file" // @Param file formData file true "Image file"
// @Param imageType formData string true "Image type: thumbnail, full or all" Enums(thumbnail, full, all) // @Success 200 {object} imageStorage.UploadMerchImageResponse
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400 // @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500 // @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [post] // @Router /merch/images/{uuid} [post]
@ -294,13 +293,14 @@ func (co *controller) uploadMerchImage(c *gin.Context) {
return return
} }
imageType := c.PostForm("imageType") //Uncomment for MinIO use
types := map[string]struct{}{"thumbnail": {}, "full": {}, "all": {}} //imageType := c.PostForm("imageType")
if _, allowed := types[imageType]; !allowed { //types := map[string]struct{}{"thumbnail": {}, "full": {}, "all": {}}
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "imageType must be one of: thumbnail, full, all"}) //if _, allowed := types[imageType]; !allowed {
log.WithError(err).Error("Merch | imageType must be one of: thumbnail, full, all") // c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "imageType must be one of: thumbnail, full, all"})
return // log.WithError(err).Error("Merch | imageType must be one of: thumbnail, full, all")
} // return
//}
file, err := c.FormFile("file") file, err := c.FormFile("file")
if err != nil { if err != nil {
@ -312,14 +312,17 @@ func (co *controller) uploadMerchImage(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires) ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel() defer cancel()
err = co.service.uploadMerchImage(ctx, userUuid, merchUuid, imageType, file) //Uncomment for MinIO use
//err = co.service.uploadMerchImage(ctx, userUuid, merchUuid, imageType, file)
response, err := co.service.mtUploadMerchImage(ctx, userUuid, merchUuid, file)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to upload merch image") log.WithError(err).Error("Merch | Failed to upload merch image")
return return
} }
c.Status(http.StatusOK) //c.Status(http.StatusOK)
c.JSON(http.StatusOK, response)
} }
// @Summary Получить картинки по merch_uuid и query параметрам // @Summary Получить картинки по merch_uuid и query параметрам
@ -333,43 +336,53 @@ func (co *controller) uploadMerchImage(c *gin.Context) {
// @Failure 500 {object} responses.ErrorResponse500 // @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [get] // @Router /merch/images/{uuid} [get]
func (co *controller) getMerchImage(c *gin.Context) { func (co *controller) getMerchImage(c *gin.Context) {
typeQuery := strings.ToLower(c.Query("type")) //Uncomment for MinIO use
if typeQuery == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "Image type query param is empty"})
return
}
userUuid, err := co.utils.GetUserUuidFromContext(c) //typeQuery := strings.ToLower(c.Query("type"))
if err != nil { //if typeQuery == "" {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) // c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "Image type query param is empty"})
log.WithError(err).Error("Merch | Failed to get user uuid from context") // return
return //}
} //
//userUuid, err := co.utils.GetUserUuidFromContext(c)
merchUuid := c.Param("uuid") //if err != nil {
if merchUuid == "" { // c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"}) // log.WithError(err).Error("Merch | Failed to get user uuid from context")
log.WithError(err).Error("Merch | Failed to get single merch") // return
return //}
} //
//merchUuid := c.Param("uuid")
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires) //if merchUuid == "" {
defer cancel() // c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
// log.WithError(err).Error("Merch | Failed to get single merch")
link, err := co.service.getPublicImageLink(ctx, userUuid, merchUuid, typeQuery) // return
if err != nil { //}
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) //
log.WithError(err).Error("Merch | Failed to get merch image") //ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
return //defer cancel()
} //
c.JSON(http.StatusOK, link) //link, err := co.service.getPublicImageLink(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
//}
//
//if link.Link == "" {
// log.Debug("Merch | No image")
// c.Status(http.StatusNoContent)
// return
//}
//
//c.JSON(http.StatusOK, link)
c.JSON(http.StatusNotImplemented, gin.H{"msg": "Method deprecated. Request images from image storage."})
} }
// @Summary Удалить (безвозвратно) картинки по merch_uuid и query параметрам // @Summary Удалить (безвозвратно) картинки по merch_uuid
// @Description Удалить (безвозвратно) картинки по merch_uuid и query параметрам // @Description Удалить (безвозвратно) картинки по merch_uuid
// @Tags Merch images // @Tags Merch images
// @Security BearerAuth // @Security BearerAuth
// @Param uuid path string true "merch_uuid" // @Param uuid path string true "merch_uuid"
// @Success 200 // @Success 200
// @Failure 400 {object} responses.ErrorResponse400 // @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500 // @Failure 500 {object} responses.ErrorResponse500
@ -385,14 +398,17 @@ func (co *controller) deleteMerchImage(c *gin.Context) {
merchUuid := c.Param("uuid") merchUuid := c.Param("uuid")
if merchUuid == "" { if merchUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"}) c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
log.WithError(err).Error("Merch | Failed to get single merch") log.WithError(err).Error("Merch | Failed to get merch uuid")
return return
} }
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires) ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel() defer cancel()
if err := co.service.deleteMerchImage(ctx, userUuid, merchUuid); err != nil { //Uncomment for MinIO use
//if err := co.service.deleteMerchImage(ctx, userUuid, merchUuid); err != nil {
if err := co.service.mtDeleteMerchImage(ctx, userUuid, merchUuid); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to delete merch image") log.WithError(err).Error("Merch | Failed to delete merch image")
return return

View file

@ -4,6 +4,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
"merch-parser-api/internal/interfaces" "merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage"
"time" "time"
) )
@ -14,9 +15,10 @@ type Handler struct {
} }
type Deps struct { type Deps struct {
DB *gorm.DB DB *gorm.DB
Utils interfaces.Utils Utils interfaces.Utils
Media interfaces.MediaStorage Media interfaces.MediaStorage
ImageStorage is.ImageStorageClient
} }
func NewHandler(deps Deps) *Handler { func NewHandler(deps Deps) *Handler {
@ -24,7 +26,13 @@ func NewHandler(deps Deps) *Handler {
expires := time.Minute * 5 expires := time.Minute * 5
r := NewRepo(deps.DB) r := NewRepo(deps.DB)
s := newService(r, deps.Media, packageBucketName, expires) s := newService(serviceDeps{
repo: r,
media: deps.Media,
bucketName: packageBucketName,
expires: expires,
imageStorage: deps.ImageStorage,
})
c := newController(s, deps.Utils, expires) c := newController(s, deps.Utils, expires)
media := deps.Media media := deps.Media

View file

@ -63,6 +63,7 @@ func (r *Repo) merchRecordExists(userUuid, merchUuid string) (bool, error) {
FROM merch FROM merch
WHERE user_uuid = ? WHERE user_uuid = ?
AND merch_uuid = ? AND merch_uuid = ?
AND deleted_at IS NULL
);`, userUuid, merchUuid).Scan(&exists).Error );`, userUuid, merchUuid).Scan(&exists).Error
return exists, err return exists, err

View file

@ -13,6 +13,7 @@ import (
"image/jpeg" "image/jpeg"
"io" "io"
"merch-parser-api/internal/interfaces" "merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage"
"mime/multipart" "mime/multipart"
"path/filepath" "path/filepath"
"strings" "strings"
@ -20,18 +21,28 @@ import (
) )
type service struct { type service struct {
repo repository repo repository
media interfaces.MediaStorage media interfaces.MediaStorage
bucketName string bucketName string
expires time.Duration expires time.Duration
imageStorage is.ImageStorageClient
} }
func newService(repo repository, media interfaces.MediaStorage, bucketName string, expires time.Duration) *service { type serviceDeps struct {
repo repository
media interfaces.MediaStorage
bucketName string
expires time.Duration
imageStorage is.ImageStorageClient
}
func newService(deps serviceDeps) *service {
return &service{ return &service{
repo: repo, repo: deps.repo,
media: media, media: deps.media,
bucketName: bucketName, bucketName: deps.bucketName,
expires: expires, expires: deps.expires,
imageStorage: deps.imageStorage,
} }
} }
@ -210,6 +221,9 @@ func (s *service) getDistinctPrices(userUuid, merchUuid, days string) (PricesRes
}, nil }, nil
} }
// uploadMerchImage
// Deprecated.
// Use only with MinIO storage. Use mtUploadMerchImage for merch-tracker images storage.
func (s *service) uploadMerchImage(ctx context.Context, userUuid, merchUuid, imageType string, file *multipart.FileHeader) error { func (s *service) uploadMerchImage(ctx context.Context, userUuid, merchUuid, imageType string, file *multipart.FileHeader) error {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid) exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil { if err != nil {
@ -303,6 +317,9 @@ func (s *service) uploadMerchImage(ctx context.Context, userUuid, merchUuid, ima
return nil return nil
} }
// getPublicImageLink
// Deprecated.
// Use only with MinIO storage.
func (s *service) getPublicImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) { func (s *service) getPublicImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
object, err := s.makeObject(userUuid, merchUuid, imageType) object, err := s.makeObject(userUuid, merchUuid, imageType)
if err != nil { if err != nil {
@ -320,6 +337,9 @@ func (s *service) getPublicImageLink(ctx context.Context, userUuid, merchUuid, i
}, nil }, nil
} }
// getPresignedImageLink
// Deprecated.
// Use only with MinIO storage.
func (s *service) getPresignedImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) { func (s *service) getPresignedImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid) exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil { if err != nil {
@ -351,6 +371,9 @@ func (s *service) getPresignedImageLink(ctx context.Context, userUuid, merchUuid
}, nil }, nil
} }
// deleteMerchImage
// Deprecated.
// Use only with MinIO storage.
func (s *service) deleteMerchImage(ctx context.Context, userUuid, merchUuid string) error { func (s *service) deleteMerchImage(ctx context.Context, userUuid, merchUuid string) error {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid) exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil { if err != nil {
@ -398,3 +421,65 @@ func (s *service) _uploadToStorage(params uploadImageParams) error {
return nil return nil
} }
// mtUploadMerchImage
// Upload new/rewrite existing image to merch-tracker images storage
func (s *service) mtUploadMerchImage(ctx context.Context, userUuid, merchUuid string, file *multipart.FileHeader) (*is.UploadMerchImageResponse, error) {
const uploadMerchImage = "Merch service | Upload merch image"
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
if !exists {
err = fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
f, err := file.Open()
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
response, err := s.imageStorage.UploadImage(ctx, &is.UploadMerchImageRequest{
ImageData: data,
UserUuid: userUuid,
MerchUuid: merchUuid,
})
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
return response, nil
}
// mtDeleteMerchImage
// Delete all merch images for given user and merch uuid-s from merch-tracker images storage
func (s *service) mtDeleteMerchImage(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)
}
s.imageStorage.DeleteImage(ctx, &is.DeleteImageRequest{
UserUuid: userUuid,
MerchUuid: merchUuid,
})
return nil
}