S3
S3 providers expose portable object storage to executable apps. Gestalt models the de facto S3-compatible object API rather than a filesystem API, so the same app code can target AWS S3, MinIO, GCS XML interoperability mode, Cloudflare R2, and other compatible backends.
Using S3 from a plugin
The SDK exposes a generated S3 client in every supported language. The
client connects to one configured S3 binding by name; the backing bucket comes
from provider configuration, so object references carry only key and an
optional version_id. Object reads and writes are streamed: writes open with
object metadata and then send byte chunks, and reads return object metadata
plus a byte-chunk stream.
Go
import (
"context"
"io"
"log"
"github.com/valon-technologies/gestalt/sdk/go/client"
)
ctx := context.Background()
s3, err := client.ConnectS3(ctx, "uploads")
if err != nil {
log.Fatal(err)
}
avatar := &client.S3ObjectRef{Key: "avatars/user-123.png"}
stream, err := s3.WriteObject(ctx, &client.WriteObjectOpen{
Ref: avatar,
ContentType: "image/png",
})
if err != nil {
log.Fatal(err)
}
if err := stream.Send(pngBytes); err != nil {
log.Fatal(err)
}
if _, err := stream.CloseAndRecv(); err != nil {
log.Fatal(err)
}
meta, data, err := s3.ReadObject(ctx, &client.ReadObjectRequest{Ref: avatar})
if err != nil {
log.Fatal(err)
}
var body []byte
for {
chunk, err := data.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
body = append(body, chunk...)
}
_ = meta
_ = bodyBuilding your own S3 provider
Manifest
An S3 provider manifest declares kind: s3:
kind: s3
source: github.com/acme/gestalt-providers/s3/minio
version: 0.0.1-alpha.1
displayName: MinIO S3
description: S3-compatible object storage provider backed by MinIO.
spec:
configSchemaPath: ./config.schema.jsonProvider interface
Implement the SDK’s authored S3Provider surface. The SDK adapts this typed
interface to the underlying gRPC service:
| Method | Purpose |
|---|---|
| Head object | Return metadata for one object reference. |
| Read object | Stream one object body with its metadata. |
| Write object | Accept a body stream and return committed metadata. |
| Delete object | Delete one object reference. |
| List objects | List objects in the configured bucket by prefix, delimiter, and continuation token. |
| Copy object | Copy an object server-side between object references. |
| Presign object | Produce a presigned request URL plus required headers. |
Go
package s3provider
import (
"bytes"
"context"
"io"
"time"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
)
type Provider struct{}
func New() *Provider { return &Provider{} }
func (p *Provider) Configure(_ context.Context, _ string, config map[string]any) error {
_ = config
return nil
}
func (p *Provider) HeadObject(ctx context.Context, ref gestalt.ObjectRef) (gestalt.ObjectMeta, error) {
_ = ctx
return gestalt.ObjectMeta{
Ref: ref,
ETag: "",
Size: 0,
ContentType: "application/octet-stream",
LastModified: time.Now(),
Metadata: map[string]string{},
}, nil
}
func (p *Provider) ReadObject(ctx context.Context, req gestalt.ReadRequest) (gestalt.ReadResult, error) {
meta, err := p.HeadObject(ctx, req.Ref)
if err != nil {
return gestalt.ReadResult{}, err
}
return gestalt.ReadResult{
Meta: meta,
Body: io.NopCloser(bytes.NewReader(nil)),
}, nil
}
func (p *Provider) WriteObject(ctx context.Context, req gestalt.WriteRequest) (gestalt.ObjectMeta, error) {
_ = req.Body
return p.HeadObject(ctx, req.Ref)
}
func (p *Provider) DeleteObject(context.Context, gestalt.ObjectRef) error {
return nil
}
func (p *Provider) ListObjects(context.Context, gestalt.ListRequest) (gestalt.ListPage, error) {
return gestalt.ListPage{}, nil
}
func (p *Provider) CopyObject(ctx context.Context, req gestalt.CopyRequest) (gestalt.ObjectMeta, error) {
_ = req.Source
return p.HeadObject(ctx, req.Destination)
}
func (p *Provider) PresignObject(context.Context, gestalt.PresignRequest) (gestalt.PresignResult, error) {
return gestalt.PresignResult{
URL: "https://example.invalid/object",
Method: gestalt.PresignMethodGet,
ExpiresAt: time.Now().Add(time.Minute),
Headers: map[string]string{},
}, nil
}
gestalt.S3Provider adds runtime lifecycle wiring and a serve() helper for
languages that expose the server-side S3 provider surface.
App access
Plugin-side S3 usage is documented in
Using S3 from a plugin. Provider
authors implement the S3 service surface; Gestalt handles configured plugin
bindings, SDK sockets, and host-mediated object access URLs.
Deploying the provider
Custom S3 providers are wired into Gestalt the same way as first-party ones:
providers:
s3:
assets:
source: ./providers/s3/custom/manifest.yaml
config:
endpoint: http://127.0.0.1:9000
region: us-east-1
apps:
media:
source: ./apps/media/manifest.yaml
s3:
- assetsFor packaging and published release URLs, see Releasing provider packages.
Implementation notes
- Treat object references as
{key, version_id}values scoped to the provider’s configured bucket, not filesystem paths. - Keep reads and writes streaming. Do not require the caller to buffer the full object in memory.
- Preserve backend metadata that fits the portable model:
etag,size,content_type,last_modified,metadata, andstorage_class. CopyObjectconditionals apply to the source object, not the destination object.- Map missing objects to
NotFound, conditional failures toFailedPrecondition, and invalid ranges toOutOfRangeso SDK error mapping stays portable. PresignObjectshould return only caller-required headers. Do not leak transport-generated headers such asHost.- Implement multipart upload and multipart copy internally when the backend needs them, or reject above the single-request S3 limit explicitly. Do not silently rely on backend-specific
PutObject/CopyObjectfailures.