| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- package storage
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "strings"
- "time"
- "github.com/aws/aws-sdk-go-v2/aws"
- "github.com/aws/aws-sdk-go-v2/config"
- "github.com/aws/aws-sdk-go-v2/credentials"
- "github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/aws/aws-sdk-go-v2/service/s3/types"
- cfgpkg "viewer/internal/config"
- )
- type S3Store struct {
- bucket string
- client *s3.Client
- presigner *s3.PresignClient
- }
- func NewS3Store(ctx context.Context, cfg cfgpkg.Config) (*S3Store, error) {
- awsCfg, err := config.LoadDefaultConfig(
- ctx,
- config.WithRegion(cfg.S3Region),
- config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.S3AccessKey, cfg.S3SecretKey, "")),
- )
- if err != nil {
- return nil, fmt.Errorf("load aws config: %w", err)
- }
- awsCfg.EndpointResolverWithOptions = aws.EndpointResolverWithOptionsFunc(
- func(service, region string, options ...interface{}) (aws.Endpoint, error) {
- if service == s3.ServiceID {
- return aws.Endpoint{URL: cfg.S3Endpoint, HostnameImmutable: true}, nil
- }
- return aws.Endpoint{}, &aws.EndpointNotFoundError{}
- },
- )
- client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
- o.UsePathStyle = cfg.S3UsePathStyle
- })
- return &S3Store{
- bucket: cfg.S3Bucket,
- client: client,
- presigner: s3.NewPresignClient(client),
- }, nil
- }
- func (s *S3Store) PresignPut(ctx context.Context, key string, ttl time.Duration) (string, map[string]string, error) {
- out, err := s.presigner.PresignPutObject(ctx, &s3.PutObjectInput{
- Bucket: aws.String(s.bucket),
- Key: aws.String(key),
- }, s3.WithPresignExpires(ttl))
- if err != nil {
- return "", nil, fmt.Errorf("presign put: %w", err)
- }
- return out.URL, map[string]string{}, nil
- }
- func (s *S3Store) PutObject(ctx context.Context, key string, body io.Reader, contentType string) error {
- _, err := s.client.PutObject(ctx, &s3.PutObjectInput{
- Bucket: aws.String(s.bucket),
- Key: aws.String(key),
- Body: body,
- ContentType: aws.String(contentType),
- })
- if err != nil {
- return fmt.Errorf("put object %s: %w", key, err)
- }
- return nil
- }
- func (s *S3Store) PutJSON(ctx context.Context, key string, v any) error {
- b, err := json.Marshal(v)
- if err != nil {
- return fmt.Errorf("marshal json: %w", err)
- }
- return s.PutObject(ctx, key, bytes.NewReader(b), "application/json")
- }
- func (s *S3Store) GetObject(ctx context.Context, key string) (io.ReadCloser, string, error) {
- out, err := s.client.GetObject(ctx, &s3.GetObjectInput{
- Bucket: aws.String(s.bucket),
- Key: aws.String(key),
- })
- if err != nil {
- return nil, "", fmt.Errorf("get object %s: %w", key, err)
- }
- ct := "application/octet-stream"
- if out.ContentType != nil {
- ct = *out.ContentType
- }
- return out.Body, ct, nil
- }
- func (s *S3Store) ReadJSON(ctx context.Context, key string, out any) error {
- body, _, err := s.GetObject(ctx, key)
- if err != nil {
- return err
- }
- defer body.Close()
- if err := json.NewDecoder(body).Decode(out); err != nil {
- return fmt.Errorf("decode json %s: %w", key, err)
- }
- return nil
- }
- func (s *S3Store) HeadObject(ctx context.Context, key string) (bool, int64, error) {
- o, err := s.client.HeadObject(ctx, &s3.HeadObjectInput{
- Bucket: aws.String(s.bucket),
- Key: aws.String(key),
- })
- if err != nil {
- var noSuch *types.NotFound
- if errors.As(err, &noSuch) || strings.Contains(strings.ToLower(err.Error()), "not found") {
- return false, 0, nil
- }
- return false, 0, fmt.Errorf("head object %s: %w", key, err)
- }
- var size int64
- if o.ContentLength != nil {
- size = *o.ContentLength
- }
- return true, size, nil
- }
- func (s *S3Store) ListAlbumIndexKeys(ctx context.Context) ([]string, error) {
- keys := make([]string, 0, 128)
- var token *string
- for {
- out, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
- Bucket: aws.String(s.bucket),
- Prefix: aws.String("albums/"),
- ContinuationToken: token,
- })
- if err != nil {
- return nil, fmt.Errorf("list objects: %w", err)
- }
- for _, obj := range out.Contents {
- if obj.Key == nil {
- continue
- }
- if strings.HasSuffix(*obj.Key, "/index.json") {
- keys = append(keys, *obj.Key)
- }
- }
- if out.IsTruncated == nil || !*out.IsTruncated {
- break
- }
- token = out.NextContinuationToken
- }
- return keys, nil
- }
|