tailly_back_v2/internal/service/post_service.go
madipo2611 7509cb1cdf
All checks were successful
continuous-integration/drone/push Build is passing
v0.0.8 Интеграция с S3 для загрузки изображений постов в хранилище
2025-07-20 16:47:17 +03:00

196 lines
5.0 KiB
Go

package service
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/aws/aws-sdk-go/aws/credentials"
"image"
"image/jpeg"
"log"
"strings"
"tailly_back_v2/internal/domain"
"tailly_back_v2/internal/repository"
"tailly_back_v2/internal/utils"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
)
// Интерфейс сервиса постов
type PostService interface {
Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error)
GetByID(ctx context.Context, id int) (*domain.Post, error)
GetAll(ctx context.Context) ([]*domain.Post, error)
GetByAuthorID(ctx context.Context, authorID int) ([]*domain.Post, error)
Update(ctx context.Context, id int, title, content string) (*domain.Post, error)
Delete(ctx context.Context, id int) error
}
// Реализация сервиса постов
type postService struct {
postRepo repository.PostRepository
}
// Конструктор сервиса
func NewPostService(postRepo repository.PostRepository) PostService {
return &postService{postRepo: postRepo}
}
// Создание нового поста
func (s *postService) Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error) {
const op = "service/postService.Create"
// Валидация данных
if title == "" {
return nil, errors.New("post title cannot be empty")
}
if content == "" {
return nil, errors.New("post content cannot be empty")
}
if strings.HasPrefix(content, "data:image/jpeg;base64,") {
content = strings.TrimPrefix(content, "data:image/jpeg;base64,")
}
imgBase64, err := base64.StdEncoding.DecodeString(content)
if err != nil {
log.Println("failed to decode image base64", op, err)
return nil, err
}
// Проверяем, что это изображение
img, format, err := image.Decode(bytes.NewReader(imgBase64))
if err != nil {
return nil, err
}
// Проверяем формат (например, только JPEG/PNG)
if format != "jpeg" && format != "png" {
return nil, errors.New("only JPEG/PNG allowed")
}
// Проверяем размер (например, не больше 10MB)
if len(imgBase64) > 10_000_000 {
return nil, errors.New("image too large (max 10MB)")
}
randName := utils.GenerateId() + ".jpg"
buf := new(bytes.Buffer)
if err := jpeg.Encode(buf, img, nil); err != nil {
return nil, err
}
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String("ru-central1"),
Endpoint: aws.String("https://s3.regru.cloud"),
S3ForcePathStyle: aws.Bool(true),
Credentials: credentials.NewStaticCredentials(
"TJ946G2S1Z5FEI3I7DQQ",
"C2H2aITHRDpek8H921yhnrINZwDoADsjW3F6HURl",
"",
),
}))
uploader := s3manager.NewUploader(sess)
s3Key := fmt.Sprintf("posts/%d/%s", authorID, randName)
_, err = uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String("tailly"),
Key: aws.String(s3Key),
Body: bytes.NewReader(buf.Bytes()),
ContentType: aws.String("image/jpeg"),
})
if err != nil {
return nil, fmt.Errorf("failed to upload to S3: %v", err)
}
imageURL := fmt.Sprintf("https://s3.regru.cloud/%s/%s", "tailly", s3Key)
post := &domain.Post{
Title: title,
Content: imageURL,
AuthorID: authorID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.postRepo.Create(ctx, post); err != nil {
return nil, err
}
return post, nil
}
// Получение поста по ID
func (s *postService) GetByID(ctx context.Context, id int) (*domain.Post, error) {
post, err := s.postRepo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrPostNotFound) {
return nil, errors.New("post not found")
}
return nil, err
}
return post, nil
}
// Получение всех постов
func (s *postService) GetAll(ctx context.Context) ([]*domain.Post, error) {
posts, err := s.postRepo.GetAll(ctx)
if err != nil {
return nil, err
}
// Возвращаем пустой слайс вместо nil
if posts == nil {
return []*domain.Post{}, nil
}
return posts, nil
}
// Получение постов автора
func (s *postService) GetByAuthorID(ctx context.Context, authorID int) ([]*domain.Post, error) {
posts, err := s.postRepo.GetByAuthorID(ctx, authorID)
if err != nil {
return nil, err
}
if posts == nil {
return []*domain.Post{}, nil
}
return posts, nil
}
// Обновление поста
func (s *postService) Update(ctx context.Context, id int, title, content string) (*domain.Post, error) {
// Получаем существующий пост
post, err := s.postRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
// Обновляем поля
post.Title = title
post.Content = content
post.UpdatedAt = time.Now()
if err := s.postRepo.Update(ctx, post); err != nil {
return nil, err
}
return post, nil
}
// Удаление поста
func (s *postService) Delete(ctx context.Context, id int) error {
return s.postRepo.Delete(ctx, id)
}