Skip to content

Store and serve files

gas/storage implements gas.StorageProvider on AWS S3 and on S3-compatible services such as MinIO, LocalStack, and DigitalOcean Spaces. Your services depend on the interface, so the same code runs against LocalStack in development and S3 in production.

Terminal window
go get github.com/gasmod/gas/storage

s3.New() returns a DI constructor that takes gas.ConfigProvider and gas.Logger, so register both alongside it. Register the result under gas.StorageProvider, the interface your services inject.

main.go
app := gas.NewApp(
// Infrastructure every module draws on.
gas.WithServiceInstance[gas.ConfigProvider](cfg),
gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()),
// Modules, registered under the provider interface your services inject.
gas.WithSingletonService[gas.StorageProvider](storages3.New()),
)

A service depends on the interface, never on the backend package, so it can be tested against storagetest.MockStorage with no network.

// A consuming service depends on the interface, never on a backend package,
// so it can be tested against storagetest.MockStorage.
type Service struct {
storage gas.StorageProvider
}
func New(provider gas.StorageProvider) *Service {
return &Service{storage: provider}
}

gas.StorageProvider covers six operations, each accepting optional gas.StorageOption values.

Method Returns Notes
Upload(ctx, key, data, opts...) error Streams the reader to the object store
Download(ctx, key, opts...) *gas.StorageObject Body, content type, size, metadata; caller closes Body
Head(ctx, key, opts...) *gas.ObjectInfo Metadata without the body
Delete(ctx, key, opts...) error
PresignDownloadURL(ctx, key, expires, opts...) string Time-limited URL a browser can GET
PresignUploadURL(ctx, key, expires, opts...) string Time-limited URL a browser can PUT
Option Applies to Effect
gas.InBucket(name) all Overrides the default bucket for one call
gas.WithContentType(ct) Upload, presigning Sets the object’s content type
gas.WithMetadata(m) Upload Attaches user metadata
func (s *Service) Upload(ctx context.Context, key, body string) error {
return s.storage.Upload(ctx, key, strings.NewReader(body),
gas.WithContentType("text/plain"),
gas.WithMetadata(map[string]string{"source": "api"}),
)
}

Download returns an open body the caller must close, and reports a missing key as storage.ErrKeyNotFound.

func (s *Service) Read(ctx context.Context, key string) ([]byte, error) {
obj, err := s.storage.Download(ctx, key)
if errors.Is(err, storage.ErrKeyNotFound) {
return nil, gas.NotFound("file not found").WithCause(err)
}
if err != nil {
return nil, err
}
defer obj.Body.Close() //nolint:errcheck // documentation snippet
buf := make([]byte, obj.Size)
_, err = obj.Body.Read(buf)
return buf, err
}

Presigned URLs let the browser transfer bytes straight to S3, so large files never pass through your server. Signing is local; no network call is made.

// Presigned URLs let the browser transfer bytes straight to S3, so large
// files never pass through your server.
func (s *Service) UploadLink(ctx context.Context, key string) (string, error) {
return s.storage.PresignUploadURL(ctx, key, 15*time.Minute,
gas.WithContentType("image/png"),
)
}

Without WithConfig, settings bind from the registered gas.ConfigProvider, so they can come from environment variables or a config file. Build custom configs from s3.DefaultConfig() rather than a bare &s3.Config{} literal, so unset fields keep their defaults.

Field Description
Storage.Region AWS region (required)
Storage.Bucket Default bucket. Leave unset to require gas.InBucket(...) on every call
Storage.AccessKeyID Static AWS access key. Empty uses the default credential chain
Storage.SecretAccessKey Static AWS secret key
Storage.Endpoint Custom endpoint for S3-compatible services. Enables path-style addressing

The service implements gas.ReadyReporter. With a default bucket configured, CheckReady issues a HeadBucket against it, so a Kubernetes readiness probe passes only once credentials are valid and the bucket is reachable.

With no default bucket, readiness succeeds once Init completes; there is no single bucket to probe. gas.HealthReporter is intentionally not implemented, since the S3 client is stateless and has no broken state a restart would clear.

Classify failures with errors.Is against storage.ErrKeyNotFound, storage.ErrClosed, and storage.ErrBucketRequired. The storagetest package provides MockStorage, a recording mock of gas.StorageProvider.

Full API reference on pkg.go.dev.