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) }