Files
test_deploy/internal/object_storage/s3_storage.go
Alex Shevchuk d84487d238 1
2025-08-18 17:12:04 +03:00

256 lines
6.9 KiB
Go

package object_storage
import (
"context"
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
"io"
"strings"
"time"
)
type S3StorageConfig struct {
Bucket string
DefaultLinkTTL time.Duration
}
type s3Storage struct {
client *s3.Client
presignedClient *s3.PresignClient
config S3StorageConfig
}
func newS3Storage(c Config) (*s3Storage, error) {
cfg, ok := c.(S3StorageConfig)
if !ok {
return nil, ErrInvalidConfig
}
storageConfig, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, ErrInvalidConfig
}
s := &s3Storage{
client: s3.NewFromConfig(storageConfig),
config: cfg,
}
s.presignedClient = s3.NewPresignClient(s.client)
return s, nil
}
func (s *s3Storage) PutNewObject(ctx context.Context, objectId string, category Category, r io.Reader, opts ...PutOptions) error {
if err := s.checkInstance(); err != nil {
return fmt.Errorf("%w: %v", ErrGetPresignedLink, err)
}
if len(opts) > 1 {
return fmt.Errorf("%w: %v", ErrPutNewObject, ErrToManyOptions)
}
fullKey := strings.Join([]string{string(category), objectId}, keySeparator)
if len(opts) != 0 && opts[0].LocalPrefix != nil && len(opts[0].LocalPrefix) > 0 {
fullKey = strings.Join([]string{string(category), strings.Join(opts[0].LocalPrefix, keySeparator), objectId}, keySeparator)
}
if _, err := s.exists(ctx, fullKey); err != nil {
if !errors.Is(err, ErrObjectNotFound) {
return fmt.Errorf("%w: %v", ErrPutNewObject, err)
}
} else {
return fmt.Errorf("%w: %v", ErrPutNewObject, ErrObjectAlreadyExists)
}
putOptions := &s3.PutObjectInput{
Bucket: aws.String(s.config.Bucket),
Key: aws.String(fullKey),
Body: r,
ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
}
if len(opts) != 0 {
putOptions.ContentType = aws.String(opts[0].ContentType)
}
if _, err := s.client.PutObject(ctx, putOptions); err != nil {
return fmt.Errorf("%w: %v", ErrPutNewObject, err)
}
return nil
}
func (s *s3Storage) GetObject(ctx context.Context, objectId string, category Category, opts ...GetOptions) (io.ReadCloser, error) {
if err := s.checkInstance(); err != nil {
return nil, fmt.Errorf("%w: %v", ErrGetPresignedLink, err)
}
if len(opts) > 1 {
return nil, fmt.Errorf("%w: %v", ErrGetObject, ErrToManyOptions)
}
fullKey := strings.Join([]string{string(category), objectId}, keySeparator)
if len(opts) != 0 && opts[0].LocalPrefix != nil && len(opts[0].LocalPrefix) > 0 {
fullKey = strings.Join([]string{string(category), strings.Join(opts[0].LocalPrefix, keySeparator), objectId}, keySeparator)
}
out, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.config.Bucket),
Key: aws.String(fullKey),
ChecksumMode: types.ChecksumModeEnabled,
})
if err != nil {
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() == s3ErrorPatternNoSuchKey {
return nil, fmt.Errorf("%w: %v", ErrGetObject, ErrObjectNotFound)
}
return nil, fmt.Errorf("%w: %v", ErrGetObject, err)
}
return out.Body, nil
}
func (s *s3Storage) DeleteObject(ctx context.Context, objectId string, category Category, opts ...DeleteOptions) error {
if err := s.checkInstance(); err != nil {
return fmt.Errorf("%w: %v", ErrGetPresignedLink, err)
}
if len(opts) > 1 {
return fmt.Errorf("%w: %v", ErrDeleteObject, ErrToManyOptions)
}
fullKey := strings.Join([]string{string(category), objectId}, keySeparator)
if len(opts) != 0 && opts[0].LocalPrefix != nil && len(opts[0].LocalPrefix) > 0 {
fullKey = strings.Join([]string{string(category), strings.Join(opts[0].LocalPrefix, keySeparator), objectId}, keySeparator)
}
head, err := s.exists(ctx, fullKey)
if err != nil {
return err
}
if _, err = s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.config.Bucket),
Key: aws.String(fullKey),
IfMatch: head.ETag,
}); err != nil {
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
return fmt.Errorf("%w: %v", ErrDeleteObject, ErrObjectNotFound)
}
return fmt.Errorf("%w: %v", ErrDeleteObject, err)
}
return nil
}
func (s *s3Storage) UpdateObject(ctx context.Context, objectId string, category Category, r io.Reader, opts ...UpdateOptions) error {
if err := s.checkInstance(); err != nil {
return fmt.Errorf("%w: %v", ErrGetPresignedLink, err)
}
if len(opts) > 1 {
return fmt.Errorf("%w: %v", ErrUpdateObject, ErrToManyOptions)
}
fullKey := strings.Join([]string{string(category), objectId}, keySeparator)
if len(opts) != 0 && opts[0].LocalPrefix != nil && len(opts[0].LocalPrefix) > 0 {
fullKey = strings.Join([]string{string(category), strings.Join(opts[0].LocalPrefix, keySeparator), objectId}, keySeparator)
}
head, err := s.exists(ctx, fullKey)
if err != nil {
return err
}
input := &s3.PutObjectInput{
Bucket: aws.String(s.config.Bucket),
Key: aws.String(fullKey),
Body: r,
IfMatch: head.ETag,
}
if len(opts) != 0 && opts[0].ContentType != "" {
input.ContentType = aws.String(opts[0].ContentType)
}
if _, err = s.client.PutObject(ctx, input); err != nil {
return fmt.Errorf("%w: %v", ErrUpdateObject, err)
}
return nil
}
func (s *s3Storage) GetPresignedLink(ctx context.Context, objectId string, category Category, opts ...LinkOptions) (string, error) {
if err := s.checkInstance(); err != nil {
return "", fmt.Errorf("%w: %v", ErrGetPresignedLink, err)
}
if len(opts) > 1 {
return "", fmt.Errorf("%w: %v", ErrGetPresignedLink, ErrToManyOptions)
}
fullKey := strings.Join([]string{string(category), objectId}, keySeparator)
if len(opts) != 0 && opts[0].LocalPrefix != nil && len(opts[0].LocalPrefix) > 0 {
fullKey = strings.Join([]string{string(category), strings.Join(opts[0].LocalPrefix, keySeparator), objectId}, keySeparator)
}
if _, err := s.exists(ctx, fullKey); err != nil {
return "", err
}
duration := s.config.DefaultLinkTTL
if len(opts) != 0 && opts[0].TTL != nil {
duration = *opts[0].TTL
}
presigned, err := s.presignedClient.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.config.Bucket),
Key: aws.String(fullKey),
}, s3.WithPresignExpires(duration))
if err != nil {
return "", fmt.Errorf("%w: %v", ErrGetPresignedLink, err)
}
return presigned.URL, nil
}
func (s *s3Storage) checkInstance() error {
if s == nil || s.client == nil {
return ErrInvalidInstance
}
return nil
}
func (s *s3Storage) exists(ctx context.Context, fullKey string) (*s3.HeadObjectOutput, error) {
head, err := s.client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.config.Bucket),
Key: aws.String(fullKey),
})
if err != nil {
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() == s3ErrorPatternNotFound {
return nil, ErrObjectNotFound
}
return nil, fmt.Errorf("%w: %v", ErrCheckExists, err)
}
return head, nil
}