572 lines
14 KiB
Go
572 lines
14 KiB
Go
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"
|
|
is "merch-parser-api/proto/imageStorage"
|
|
"mime/multipart"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type service struct {
|
|
repo repository
|
|
media interfaces.MediaStorage
|
|
bucketName string
|
|
expires time.Duration
|
|
imageStorage is.ImageStorageClient
|
|
}
|
|
|
|
type serviceDeps struct {
|
|
repo repository
|
|
media interfaces.MediaStorage
|
|
bucketName string
|
|
expires time.Duration
|
|
imageStorage is.ImageStorageClient
|
|
}
|
|
|
|
func newService(deps serviceDeps) *service {
|
|
return &service{
|
|
repo: deps.repo,
|
|
media: deps.media,
|
|
bucketName: deps.bucketName,
|
|
expires: deps.expires,
|
|
imageStorage: deps.imageStorage,
|
|
}
|
|
}
|
|
|
|
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()
|
|
|
|
bundle := merchBundle{
|
|
&Merch{
|
|
CreatedAt: time.Time{},
|
|
UpdatedAt: sql.NullTime{Valid: false},
|
|
DeletedAt: sql.NullTime{Valid: false},
|
|
MerchUuid: merchUuid,
|
|
UserUuid: userUuid,
|
|
Name: payload.Name,
|
|
},
|
|
|
|
&Surugaya{
|
|
DeletedAt: sql.NullTime{},
|
|
MerchUuid: merchUuid,
|
|
Link: payload.OriginSurugaya.Link,
|
|
},
|
|
|
|
&Mandarake{
|
|
DeletedAt: sql.NullTime{},
|
|
MerchUuid: merchUuid,
|
|
Link: payload.OriginMandarake.Link,
|
|
},
|
|
}
|
|
return s.repo.addMerch(bundle)
|
|
}
|
|
|
|
func (s *service) getSingleMerch(userUuid, merchUuid string) (MerchDTO, error) {
|
|
bundle, err := s.repo.getSingleMerch(userUuid, merchUuid)
|
|
if err != nil {
|
|
return MerchDTO{}, err
|
|
}
|
|
|
|
return MerchDTO{
|
|
MerchUuid: bundle.Merch.MerchUuid,
|
|
Name: bundle.Merch.Name,
|
|
OriginSurugaya: SurugayaDTO{
|
|
Link: bundle.Surugaya.Link,
|
|
},
|
|
OriginMandarake: MandarakeDTO{
|
|
Link: bundle.Mandarake.Link,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *service) getAllMerch(userUuid string) ([]ListResponse, error) {
|
|
return s.repo.getAllMerch(userUuid)
|
|
}
|
|
|
|
func (s *service) updateMerch(payload UpdateMerchDTO, userUuid string) error {
|
|
if payload.MerchUuid == "" {
|
|
return errors.New("no merch uuid provided")
|
|
}
|
|
|
|
if payload.Origin == "" {
|
|
return errors.New("no origin provided")
|
|
}
|
|
|
|
return s.repo.updateMerch(payload, userUuid)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (s *service) getPrices(userUuid string, days string) ([]PricesResponse, error) {
|
|
merchList, err := s.repo.getAllUserMerch(userUuid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(merchList) == 0 {
|
|
return nil, errors.New("no merch found")
|
|
}
|
|
|
|
var response []PricesResponse
|
|
for _, item := range merchList {
|
|
response = append(response, PricesResponse{
|
|
MerchUuid: item.MerchUuid,
|
|
Name: item.Name,
|
|
Origins: []OriginWithPrices{},
|
|
})
|
|
}
|
|
|
|
pricesList, err := s.repo.getPricesWithDays(userUuid, getPeriod(days))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pricesMap := make(map[string]map[Origin][]PriceEntry)
|
|
for _, item := range pricesList {
|
|
if _, ok := pricesMap[item.MerchUuid]; !ok {
|
|
pricesMap[item.MerchUuid] = make(map[Origin][]PriceEntry)
|
|
}
|
|
|
|
pricesMap[item.MerchUuid][item.Origin] = append(pricesMap[item.MerchUuid][item.Origin], PriceEntry{
|
|
CreatedAt: item.CreatedAt.Unix(),
|
|
Value: item.Price,
|
|
})
|
|
}
|
|
|
|
for i := range response {
|
|
originSurugaya := OriginWithPrices{
|
|
Origin: surugaya,
|
|
Prices: pricesMap[response[i].MerchUuid][surugaya],
|
|
}
|
|
response[i].Origins = append(response[i].Origins, originSurugaya)
|
|
|
|
originMandarake := OriginWithPrices{
|
|
Origin: mandarake,
|
|
Prices: pricesMap[response[i].MerchUuid][mandarake],
|
|
}
|
|
response[i].Origins = append(response[i].Origins, originMandarake)
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (s *service) getDistinctPrices(userUuid, merchUuid, days string) (PricesResponse, error) {
|
|
result, err := s.repo.getDistinctPrices(userUuid, merchUuid, getPeriod(days))
|
|
if err != nil {
|
|
return PricesResponse{}, err
|
|
}
|
|
|
|
if result == nil {
|
|
return PricesResponse{}, errors.New("no prices found")
|
|
}
|
|
|
|
originSurugaya := OriginWithPrices{
|
|
Origin: surugaya,
|
|
Prices: []PriceEntry{},
|
|
}
|
|
|
|
originMandarake := OriginWithPrices{
|
|
Origin: mandarake,
|
|
Prices: []PriceEntry{},
|
|
}
|
|
|
|
for _, item := range result {
|
|
switch item.Origin {
|
|
case surugaya:
|
|
originSurugaya.Prices = append(originSurugaya.Prices, PriceEntry{
|
|
CreatedAt: item.CreatedAt.Unix(),
|
|
Value: item.Price,
|
|
})
|
|
case mandarake:
|
|
originMandarake.Prices = append(originMandarake.Prices, PriceEntry{
|
|
CreatedAt: item.CreatedAt.Unix(),
|
|
Value: item.Price,
|
|
})
|
|
}
|
|
}
|
|
|
|
return PricesResponse{
|
|
MerchUuid: merchUuid,
|
|
Origins: []OriginWithPrices{originSurugaya, originMandarake},
|
|
}, 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 {
|
|
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
|
|
}
|
|
|
|
// getPublicImageLink
|
|
// Deprecated.
|
|
// Use only with MinIO storage.
|
|
func (s *service) getPublicImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
|
|
object, err := s.makeObject(userUuid, merchUuid, imageType)
|
|
if err != nil {
|
|
return ImageLink{}, err
|
|
}
|
|
|
|
link, etag, err := s.media.GetPublicLink(ctx, s.bucketName, object)
|
|
if err != nil {
|
|
return ImageLink{}, err
|
|
}
|
|
|
|
return ImageLink{
|
|
Link: link,
|
|
ETag: etag,
|
|
}, nil
|
|
}
|
|
|
|
// getPresignedImageLink
|
|
// Deprecated.
|
|
// Use only with MinIO storage.
|
|
func (s *service) getPresignedImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
|
|
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
|
|
if err != nil {
|
|
return ImageLink{}, err
|
|
}
|
|
|
|
if !exists {
|
|
return ImageLink{}, fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
|
|
}
|
|
|
|
object, err := s.makeObject(userUuid, merchUuid, imageType)
|
|
if err != nil {
|
|
return ImageLink{}, err
|
|
}
|
|
|
|
link, err := s.media.GetPresignedLink(ctx, s.bucketName, object, s.expires, nil)
|
|
if err != nil {
|
|
return ImageLink{}, err
|
|
}
|
|
|
|
etag, err := s.media.GetObjectEtag(ctx, s.bucketName, object)
|
|
if err != nil {
|
|
return ImageLink{}, err
|
|
}
|
|
|
|
return ImageLink{
|
|
Link: link,
|
|
ETag: etag,
|
|
}, nil
|
|
}
|
|
|
|
// deleteMerchImage
|
|
// Deprecated.
|
|
// Use only with MinIO storage.
|
|
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": params.imageType,
|
|
}).Error("Merch | Failed to upload file to media storage")
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *service) createLabel(label LabelDTO, userUuid string) error {
|
|
now := time.Now().UTC()
|
|
|
|
if label.Name == "" {
|
|
return fmt.Errorf("label name is required")
|
|
}
|
|
|
|
newLabel := Label{
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
DeletedAt: sql.NullTime{Time: time.Time{}, Valid: false},
|
|
LabelUuid: uuid.NewString(),
|
|
UserUuid: userUuid,
|
|
Name: label.Name,
|
|
Color: label.Color,
|
|
BgColor: label.BgColor,
|
|
}
|
|
|
|
return s.repo.createLabel(newLabel)
|
|
}
|
|
func (s *service) getLabels(userUuid string) ([]LabelsList, error) {
|
|
stored, err := s.repo.getLabels(userUuid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response := make([]LabelsList, 0, len(stored))
|
|
for _, label := range stored {
|
|
response = append(response, LabelsList{
|
|
LabelUuid: label.LabelUuid,
|
|
Name: label.Name,
|
|
Color: label.Color,
|
|
BgColor: label.BgColor,
|
|
})
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
func (s *service) updateLabel(userUuid, labelUuid string, label LabelDTO) error {
|
|
updateMap := make(map[string]any, 3)
|
|
|
|
if label.Name != "" {
|
|
updateMap["name"] = label.Name
|
|
}
|
|
|
|
if label.Color != "" {
|
|
updateMap["color"] = label.Color
|
|
}
|
|
|
|
if label.BgColor != "" {
|
|
updateMap["bgcolor"] = label.BgColor
|
|
}
|
|
|
|
return s.repo.updateLabel(userUuid, labelUuid, updateMap)
|
|
}
|
|
|
|
func (s *service) deleteLabel(userUuid, labelUuid string) error {
|
|
return s.repo.deleteLabel(userUuid, labelUuid)
|
|
}
|
|
|
|
func (s *service) attachLabel(userUuid string, label LabelLink) error {
|
|
if label.LabelUuid == "" || label.MerchUuid == "" {
|
|
return fmt.Errorf("both label and merch uuid-s are required")
|
|
}
|
|
|
|
attach := CardLabel{
|
|
LabelUuid: label.LabelUuid,
|
|
UserUuid: userUuid,
|
|
MerchUuid: label.MerchUuid,
|
|
}
|
|
|
|
return s.repo.attachLabel(attach)
|
|
}
|
|
|
|
func (s *service) detachLabel(userUuid string, label LabelLink) error {
|
|
if label.LabelUuid == "" || label.MerchUuid == "" {
|
|
return fmt.Errorf("both label and merch uuid-s are required")
|
|
}
|
|
|
|
detach := CardLabel{
|
|
LabelUuid: label.LabelUuid,
|
|
UserUuid: userUuid,
|
|
MerchUuid: label.MerchUuid,
|
|
}
|
|
return s.repo.detachLabel(detach)
|
|
}
|