| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- package s3api
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "github.com/TecharoHQ/anubis/lib/store"
- awsConfig "github.com/aws/aws-sdk-go-v2/config"
- "github.com/aws/aws-sdk-go-v2/service/s3"
- )
- var (
- ErrNoRegion = errors.New("s3api.Config: no region env var name defined")
- ErrNoAccessKeyID = errors.New("s3api.Config: no access key id env var name defined")
- ErrNoSecretAccessKey = errors.New("s3api.Config: no secret access key env var name defined")
- ErrNoBucketName = errors.New("s3api.Config: no bucket name env var name defined")
- )
- func init() {
- store.Register("s3api", Factory{})
- }
- // S3API is the subset of the AWS S3 client used by this store. It enables mocking in tests.
- type S3API interface {
- PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
- GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
- DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
- HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
- }
- // Factory builds an S3-backed store. Tests can inject a Mock via Client.
- // Factory can optionally carry a preconstructed S3 client (e.g., a mock in tests).
- type Factory struct {
- Client S3API
- }
- func (f Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface, error) {
- var config Config
- if err := json.Unmarshal([]byte(data), &config); err != nil {
- return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
- }
- if err := config.Valid(); err != nil {
- return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
- }
- if config.BucketName == "" {
- return nil, fmt.Errorf("%w: %s", store.ErrBadConfig, ErrNoBucketName)
- }
- // If a client was injected (e.g., tests), use it directly.
- if f.Client != nil {
- return &Store{
- s3: f.Client,
- bucket: config.BucketName,
- }, nil
- }
- cfg, err := awsConfig.LoadDefaultConfig(ctx)
- if err != nil {
- return nil, fmt.Errorf("can't load AWS config from environment: %w", err)
- }
- client := s3.NewFromConfig(cfg, func(o *s3.Options) {
- o.UsePathStyle = config.PathStyle
- })
- return &Store{
- s3: client,
- bucket: config.BucketName,
- }, nil
- }
- func (Factory) Valid(data json.RawMessage) error {
- var config Config
- if err := json.Unmarshal([]byte(data), &config); err != nil {
- return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
- }
- if err := config.Valid(); err != nil {
- return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
- }
- return nil
- }
- type Config struct {
- BucketName string `json:"bucketName"`
- PathStyle bool `json:"pathStyle"`
- }
- func (c Config) Valid() error {
- var errs []error
- if c.BucketName == "" {
- errs = append(errs, ErrNoBucketName)
- }
- if len(errs) != 0 {
- return fmt.Errorf("s3api.Config: invalid config: %w", errors.Join(errs...))
- }
- return nil
- }
|