api/internal/api/merch/service.go
2025-10-17 23:47:48 +03:00

388 lines
9.1 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"
"mime/multipart"
"path/filepath"
"strings"
"time"
)
type service struct {
repo repository
media interfaces.MediaStorage
bucketName string
expires time.Duration
}
func newService(repo repository, media interfaces.MediaStorage, bucketName string, expires time.Duration) *service {
return &service{
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()
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
}
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) (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)
}
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 ImageLink{}, fmt.Errorf("unknown image type %s", imageType)
}
link, err := s.media.Get(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.String(),
ETag: etag,
}, 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": params.imageType,
}).Error("Merch | Failed to upload file to media storage")
return err
}
return nil
}