1
This commit is contained in:
156
internal/file_manager/s3_storage/storage.go
Normal file
156
internal/file_manager/s3_storage/storage.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package s3_storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
|
||||
filemanager "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/file_manager"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/object_storage"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type S3Storage struct {
|
||||
objectStorage object_storage.Client
|
||||
cacheClient cache.Client
|
||||
}
|
||||
|
||||
func NewS3Storage(objectStorage object_storage.Client, cacheClient cache.Client) *S3Storage {
|
||||
return &S3Storage{
|
||||
objectStorage: objectStorage,
|
||||
cacheClient: cacheClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *S3Storage) GetFilePaths(
|
||||
ctx context.Context,
|
||||
fileType filemanager.FileType,
|
||||
parameters filemanager.ParameterTable,
|
||||
) ([]string, error) {
|
||||
objectKey, category, err := s.buildObjectKey(fileType, parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Сначала пытаемся получить URL из кэша
|
||||
cachedURL, err := s.getCachedURL(ctx, objectKey, category)
|
||||
if err == nil && cachedURL != "" {
|
||||
return []string{cachedURL}, nil
|
||||
}
|
||||
|
||||
// Получаем линк из s3
|
||||
url, err := s.objectStorage.GetPresignedLink(ctx, objectKey, category,
|
||||
object_storage.LinkOptions{
|
||||
TTL: ptr.Duration(constants.DefaultFileTTL),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, object_storage.ErrObjectNotFound) {
|
||||
return nil, filemanager.ErrFileNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s: %w", filemanager.ErrMsgGetPresignedURL, err)
|
||||
}
|
||||
|
||||
// Кэшируем URL
|
||||
if err := s.cacheURL(ctx, objectKey, category, url); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", filemanager.ErrFailedToCacheUrl, err)
|
||||
}
|
||||
|
||||
return []string{url}, nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) SaveFile(
|
||||
ctx context.Context,
|
||||
fileType filemanager.FileType,
|
||||
file multipart.File,
|
||||
fileHeader *multipart.FileHeader,
|
||||
parameters filemanager.ParameterTable,
|
||||
) error {
|
||||
objectKey, category, err := s.buildObjectKey(fileType, parameters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(file); err != nil {
|
||||
return fmt.Errorf("%s: %w", filemanager.ErrMsgReadFile, err)
|
||||
}
|
||||
|
||||
// Определяем тип контента
|
||||
contentType := s.getContentType(fileHeader.Filename)
|
||||
|
||||
// Загружаем файл в S3
|
||||
err = s.objectStorage.PutNewObject(ctx, objectKey, category,
|
||||
bytes.NewReader(buf.Bytes()),
|
||||
object_storage.PutOptions{
|
||||
ContentType: contentType,
|
||||
})
|
||||
if err != nil {
|
||||
// Обрабатываем случай существующего файла
|
||||
if errors.Is(err, object_storage.ErrObjectAlreadyExists) {
|
||||
return filemanager.ErrFileAlreadyExists
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s: %w", filemanager.ErrFailedUploadFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Создаем ключ объекта S3 на основе типа файла и параметров
|
||||
func (s *S3Storage) buildObjectKey(fileType filemanager.FileType, parameters filemanager.ParameterTable) (string, object_storage.Category, error) {
|
||||
switch fileType {
|
||||
case filemanager.AvatarFileType:
|
||||
userID, ok := parameters[filemanager.UserIdParam]
|
||||
if !ok {
|
||||
return "", "", filemanager.ErrParameterNotFound
|
||||
}
|
||||
|
||||
return fmt.Sprintf(objectKeyAvatarTemplate, userID), object_storage.LogoCategory, nil
|
||||
|
||||
case filemanager.CVFileType:
|
||||
submissionID, ok := parameters[filemanager.SubmissionIdParam]
|
||||
if !ok {
|
||||
return "", "", filemanager.ErrParameterNotFound
|
||||
}
|
||||
|
||||
return fmt.Sprintf(objectKeyCVTemplate, submissionID), object_storage.DocumentCategory, nil
|
||||
|
||||
default:
|
||||
return "", "", filemanager.ErrInvalidFileType
|
||||
}
|
||||
}
|
||||
|
||||
// getContentType определяет тип контента по расширению
|
||||
func (s *S3Storage) getContentType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
if ct, ok := ContentTypes[ext]; ok {
|
||||
return ct
|
||||
}
|
||||
|
||||
if ct := mime.TypeByExtension(ext); ct != "" {
|
||||
return ct
|
||||
}
|
||||
|
||||
return defaultContentType
|
||||
}
|
||||
|
||||
func (s *S3Storage) getCachedURL(ctx context.Context, objectKey string, category object_storage.Category) (string, error) {
|
||||
cacheKey := fmt.Sprintf(cacheKeyTemplate, category, objectKey)
|
||||
return s.cacheClient.Get(ctx, cacheKey, cache.DocumentsValueType)
|
||||
}
|
||||
|
||||
func (s *S3Storage) cacheURL(ctx context.Context, objectKey string, category object_storage.Category, url string) error {
|
||||
cacheKey := fmt.Sprintf(cacheKeyTemplate, category, objectKey)
|
||||
cacheDuration := constants.DefaultFileTTL - time.Hour
|
||||
|
||||
return s.cacheClient.Set(ctx, cacheKey, cache.DocumentsValueType, url, cacheDuration)
|
||||
}
|
22
internal/file_manager/s3_storage/types.go
Normal file
22
internal/file_manager/s3_storage/types.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package s3_storage
|
||||
|
||||
var ContentTypes = map[string]string{
|
||||
".pdf": "application/pdf",
|
||||
".txt": "text/plain",
|
||||
".doc": "application/msword",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".rtf": "application/rtf",
|
||||
".odt": "application/vnd.oasis.opendocument.text",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
}
|
||||
|
||||
// Константы для ключей
|
||||
const (
|
||||
objectKeyAvatarTemplate = "%s_avatar"
|
||||
objectKeyCVTemplate = "%s_cv"
|
||||
cacheKeyTemplate = "%s_%s"
|
||||
defaultContentType = "application/octet-stream"
|
||||
)
|
Reference in New Issue
Block a user