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.
go get github.com/gasmod/gas/storageWiring
Section titled “Wiring”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.
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()),)Consuming it
Section titled “Consuming it”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}}Operations
Section titled “Operations”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"), )}Config
Section titled “Config”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 |
Readiness
Section titled “Readiness”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.
Errors and testing
Section titled “Errors and testing”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.