factory.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. package s3api
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "github.com/TecharoHQ/anubis/lib/store"
  8. awsConfig "github.com/aws/aws-sdk-go-v2/config"
  9. "github.com/aws/aws-sdk-go-v2/service/s3"
  10. )
  11. var (
  12. ErrNoRegion = errors.New("s3api.Config: no region env var name defined")
  13. ErrNoAccessKeyID = errors.New("s3api.Config: no access key id env var name defined")
  14. ErrNoSecretAccessKey = errors.New("s3api.Config: no secret access key env var name defined")
  15. ErrNoBucketName = errors.New("s3api.Config: no bucket name env var name defined")
  16. )
  17. func init() {
  18. store.Register("s3api", Factory{})
  19. }
  20. // S3API is the subset of the AWS S3 client used by this store. It enables mocking in tests.
  21. type S3API interface {
  22. PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
  23. GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
  24. DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
  25. HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
  26. }
  27. // Factory builds an S3-backed store. Tests can inject a Mock via Client.
  28. // Factory can optionally carry a preconstructed S3 client (e.g., a mock in tests).
  29. type Factory struct {
  30. Client S3API
  31. }
  32. func (f Factory) Build(ctx context.Context, data json.RawMessage) (store.Interface, error) {
  33. var config Config
  34. if err := json.Unmarshal([]byte(data), &config); err != nil {
  35. return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
  36. }
  37. if err := config.Valid(); err != nil {
  38. return nil, fmt.Errorf("%w: %w", store.ErrBadConfig, err)
  39. }
  40. if config.BucketName == "" {
  41. return nil, fmt.Errorf("%w: %s", store.ErrBadConfig, ErrNoBucketName)
  42. }
  43. // If a client was injected (e.g., tests), use it directly.
  44. if f.Client != nil {
  45. return &Store{
  46. s3: f.Client,
  47. bucket: config.BucketName,
  48. }, nil
  49. }
  50. cfg, err := awsConfig.LoadDefaultConfig(ctx)
  51. if err != nil {
  52. return nil, fmt.Errorf("can't load AWS config from environment: %w", err)
  53. }
  54. client := s3.NewFromConfig(cfg, func(o *s3.Options) {
  55. o.UsePathStyle = config.PathStyle
  56. })
  57. return &Store{
  58. s3: client,
  59. bucket: config.BucketName,
  60. }, nil
  61. }
  62. func (Factory) Valid(data json.RawMessage) error {
  63. var config Config
  64. if err := json.Unmarshal([]byte(data), &config); err != nil {
  65. return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
  66. }
  67. if err := config.Valid(); err != nil {
  68. return fmt.Errorf("%w: %w", store.ErrBadConfig, err)
  69. }
  70. return nil
  71. }
  72. type Config struct {
  73. BucketName string `json:"bucketName"`
  74. PathStyle bool `json:"pathStyle"`
  75. }
  76. func (c Config) Valid() error {
  77. var errs []error
  78. if c.BucketName == "" {
  79. errs = append(errs, ErrNoBucketName)
  80. }
  81. if len(errs) != 0 {
  82. return fmt.Errorf("s3api.Config: invalid config: %w", errors.Join(errs...))
  83. }
  84. return nil
  85. }