256 lines
6.7 KiB
Go
256 lines
6.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"tailly_back_v2/internal/domain"
|
|
"tailly_back_v2/internal/repository"
|
|
"tailly_back_v2/internal/utils"
|
|
"tailly_back_v2/pkg/S3"
|
|
"time"
|
|
|
|
"github.com/99designs/gqlgen/graphql"
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
|
|
"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 string, content graphql.Upload) (*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
|
|
GetPaginated(ctx context.Context, limit, offset int) ([]*domain.Post, 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 string, content graphql.Upload) (*domain.Post, error) {
|
|
const op = "service/postService.Create"
|
|
|
|
// Валидация
|
|
if content.Filename == "" {
|
|
return nil, errors.New("post content cannot be empty")
|
|
}
|
|
|
|
// Сбрасываем позицию чтения на начало
|
|
if seeker, ok := content.File.(io.Seeker); ok {
|
|
seeker.Seek(0, io.SeekStart)
|
|
}
|
|
|
|
// Проверяем тип файла
|
|
contentType := content.ContentType
|
|
if contentType == "" {
|
|
// Определяем ContentType по расширению файла
|
|
ext := strings.ToLower(filepath.Ext(content.Filename))
|
|
switch ext {
|
|
case ".jpg", ".jpeg":
|
|
contentType = "image/jpeg"
|
|
case ".png":
|
|
contentType = "image/png"
|
|
case ".webp":
|
|
contentType = "image/webp"
|
|
case ".heic", ".heif":
|
|
contentType = "image/heic"
|
|
default:
|
|
return nil, errors.New("invalid image format, expected JPEG, PNG, WebP, HEIC")
|
|
}
|
|
}
|
|
|
|
// Проверяем поддерживаемые форматы
|
|
if !isValidImageType(contentType) {
|
|
return nil, fmt.Errorf("invalid image format: %s, expected JPEG, PNG, WebP, HEIC", contentType)
|
|
}
|
|
|
|
// Загрузка в S3
|
|
fileExt := getFileExtension(contentType, content.Filename)
|
|
randName := utils.GenerateId() + fileExt
|
|
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)
|
|
|
|
// Загружаем напрямую из io.ReadSeeker
|
|
_, err := uploader.Upload(&s3manager.UploadInput{
|
|
Bucket: aws.String("tailly"),
|
|
Key: aws.String(s3Key),
|
|
Body: content.File, // используем оригинальный ReadSeeker
|
|
ContentType: aws.String(contentType),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: failed to upload to S3: %w", op, err)
|
|
}
|
|
|
|
imageURL := fmt.Sprintf("https://s3.regru.cloud/tailly/%s", 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 {
|
|
// Если не удалось сохранить пост, удаляем файл из S3
|
|
S3.DeleteFromS3(imageURL)
|
|
return nil, fmt.Errorf("%s: failed to save post: %w", op, err)
|
|
}
|
|
|
|
return post, nil
|
|
}
|
|
|
|
func isValidImageType(contentType string) bool {
|
|
validTypes := []string{
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/webp",
|
|
"image/heic",
|
|
"image/heif",
|
|
}
|
|
for _, t := range validTypes {
|
|
if contentType == t {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getFileExtension(contentType, filename string) string {
|
|
// Пытаемся получить расширение из оригинального имени файла
|
|
if ext := filepath.Ext(filename); ext != "" {
|
|
return ext
|
|
}
|
|
|
|
// Если не получилось, определяем по ContentType
|
|
switch contentType {
|
|
case "image/jpeg":
|
|
return ".jpg"
|
|
case "image/png":
|
|
return ".png"
|
|
case "image/webp":
|
|
return ".webp"
|
|
case "image/heic", "image/heif":
|
|
return ".heic"
|
|
default:
|
|
return ".jpg" // fallback
|
|
}
|
|
}
|
|
|
|
// Получение поста по 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 {
|
|
|
|
post, err := s.postRepo.GetByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrPostNotFound) {
|
|
return errors.New("post not found")
|
|
}
|
|
return err
|
|
}
|
|
|
|
err = S3.DeleteFromS3(post.Content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.postRepo.Delete(ctx, id)
|
|
}
|
|
func (s *postService) GetPaginated(ctx context.Context, limit, offset int) ([]*domain.Post, error) {
|
|
posts, err := s.postRepo.GetPaginated(ctx, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if posts == nil {
|
|
return []*domain.Post{}, nil
|
|
}
|
|
|
|
return posts, nil
|
|
}
|