git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/storage/s3/s3.go (about)

     1  package s3
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"net/http"
     8  	"path/filepath"
     9  
    10  	"github.com/aws/aws-sdk-go/aws"
    11  	"github.com/aws/aws-sdk-go/aws/credentials"
    12  	"github.com/aws/aws-sdk-go/aws/endpoints"
    13  	"github.com/aws/aws-sdk-go/aws/session"
    14  	"github.com/aws/aws-sdk-go/service/s3"
    15  )
    16  
    17  type S3Storage struct {
    18  	basePath string
    19  	s3Client *s3.S3
    20  	bucket   string
    21  }
    22  
    23  type Config struct {
    24  	AccessKeyID     string
    25  	SecretAccessKey string
    26  	Endpoint        string
    27  	Region          string
    28  	BaseDirectory   string
    29  	Bucket          string
    30  	HttpClient      *http.Client
    31  }
    32  
    33  func NewS3Storage(config Config) (*S3Storage, error) {
    34  	var endpointResolver endpoints.Resolver
    35  
    36  	if config.Endpoint != "" {
    37  		// see https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/endpoints/
    38  		// https://stackoverflow.com/questions/67575681/is-aws-go-sdk-v2-integrated-with-local-minio-server
    39  		// https://stackoverflow.com/questions/71088064/how-can-i-use-the-aws-sdk-v2-for-go-with-digitalocean-spaces
    40  		endpointResolver = endpoints.ResolverFunc(func(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
    41  			return endpoints.ResolvedEndpoint{
    42  				SigningRegion: config.Region,
    43  				URL:           config.Endpoint,
    44  			}, nil
    45  		})
    46  	}
    47  
    48  	awsSession, err := session.NewSession(&aws.Config{
    49  		Endpoint:         aws.String(config.Endpoint),
    50  		Region:           aws.String(config.Region),
    51  		Credentials:      credentials.NewStaticCredentials(config.AccessKeyID, config.SecretAccessKey, ""),
    52  		HTTPClient:       config.HttpClient,
    53  		EndpointResolver: endpointResolver,
    54  	})
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	// Create S3 service client
    60  	s3Client := s3.New(awsSession)
    61  
    62  	return &S3Storage{
    63  		basePath: config.BaseDirectory,
    64  		s3Client: s3Client,
    65  		bucket:   config.Bucket,
    66  	}, nil
    67  }
    68  
    69  func (storage *S3Storage) BasePath() string {
    70  	return storage.basePath
    71  }
    72  
    73  func (storage *S3Storage) CopyObject(ctx context.Context, from string, to string) error {
    74  	from = filepath.Join(storage.bucket, storage.basePath, from)
    75  	to = filepath.Join(storage.basePath, from)
    76  
    77  	_, err := storage.s3Client.CopyObject(&s3.CopyObjectInput{
    78  		Bucket:     aws.String(storage.bucket),
    79  		Key:        aws.String(to),
    80  		CopySource: aws.String(from),
    81  	})
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	return nil
    87  }
    88  
    89  func (storage *S3Storage) DeleteObject(ctx context.Context, key string) error {
    90  	objectKey := filepath.Join(storage.basePath, key)
    91  
    92  	_, err := storage.s3Client.DeleteObject(&s3.DeleteObjectInput{
    93  		Bucket: aws.String(storage.bucket),
    94  		Key:    aws.String(objectKey),
    95  	})
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	return nil
   101  }
   102  
   103  func (storage *S3Storage) GetObject(ctx context.Context, key string) (io.ReadCloser, error) {
   104  	objectKey := filepath.Join(storage.basePath, key)
   105  
   106  	result, err := storage.s3Client.GetObject(&s3.GetObjectInput{
   107  		Bucket: aws.String(storage.bucket),
   108  		Key:    aws.String(objectKey),
   109  	})
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	return result.Body, nil
   115  }
   116  
   117  func (storage *S3Storage) GetObjectSize(ctx context.Context, key string) (int64, error) {
   118  	objectKey := filepath.Join(storage.basePath, key)
   119  
   120  	result, err := storage.s3Client.HeadObject(&s3.HeadObjectInput{
   121  		Bucket: aws.String(storage.bucket),
   122  		Key:    aws.String(objectKey),
   123  	})
   124  	if err != nil {
   125  		return 0, err
   126  	}
   127  
   128  	if result.ContentLength == nil {
   129  		return 0, errors.New("s3: object size is null")
   130  	}
   131  
   132  	return *result.ContentLength, nil
   133  }
   134  
   135  // func (storage *S3Storage) GetPresignedUploadUrl(ctx context.Context, key string, size uint64) (string, error) {
   136  // 	objectKey := filepath.Join(storage.basePath, key)
   137  
   138  // 	req, _ := storage.s3Client.PutObjectRequest(&s3.PutObjectInput{
   139  // 		Bucket:        aws.String(storage.bucket),
   140  // 		Key:           aws.String(objectKey),
   141  // 		ContentLength: aws.Int64(int64(size)),
   142  // 	})
   143  
   144  // 	url, err := req.Presign(2 * time.Hour)
   145  // 	if err != nil {
   146  // 		return "", err
   147  // 	}
   148  
   149  // 	return url, nil
   150  // }
   151  
   152  // TODO?: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html
   153  func (storage *S3Storage) PutObject(ctx context.Context, key string, contentType string, size int64, object io.Reader) error {
   154  	objectKey := filepath.Join(storage.basePath, key)
   155  
   156  	_, err := storage.s3Client.PutObject(&s3.PutObjectInput{
   157  		Bucket:        aws.String(storage.bucket),
   158  		Key:           aws.String(objectKey),
   159  		Body:          aws.ReadSeekCloser(object),
   160  		ContentType:   aws.String(contentType),
   161  		ContentLength: aws.Int64(int64(size)),
   162  	})
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  func (storage *S3Storage) DeleteObjectsWithPrefix(ctx context.Context, prefix string) (err error) {
   171  	s3Prefix := filepath.Join(storage.basePath, prefix)
   172  	var continuationToken *string
   173  
   174  	for {
   175  		var res *s3.ListObjectsV2Output
   176  
   177  		res, err = storage.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
   178  			Bucket:            aws.String(storage.bucket),
   179  			Prefix:            aws.String(s3Prefix),
   180  			ContinuationToken: continuationToken,
   181  		})
   182  
   183  		for _, object := range res.Contents {
   184  			err = storage.DeleteObject(ctx, *object.Key)
   185  			if err != nil {
   186  				return
   187  			}
   188  		}
   189  
   190  		continuationToken = res.ContinuationToken
   191  
   192  		if continuationToken == nil {
   193  			break
   194  		}
   195  	}
   196  
   197  	return
   198  }