Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Storage

Storage

go.putnami.dev/storage provides object storage with pluggable backends for files, images, documents, and other binary data.

Buckets

Declare storage buckets with constraints:

import "go.putnami.dev/storage"

var Avatars = storage.Bucket("avatars",
    storage.WithMaxFileSize(10 * 1024 * 1024),            // 10 MB
    storage.WithAllowedMimeTypes("image/png", "image/jpeg"),
)

var Documents = storage.Bucket("documents",
    storage.WithMaxFileSize(50 * 1024 * 1024),            // 50 MB
    storage.WithAllowedMimeTypes("application/pdf", "text/plain"),
    storage.WithPublic(true),
)

Bucket options

Option Description
WithMaxFileSize(bytes) Maximum file size in bytes
WithAllowedMimeTypes(types...) Restrict to specific MIME types
WithPublic(bool) Mark bucket as publicly accessible

Bucket registry

All declared buckets register in a global registry:

registry := storage.GetRegistry()

bucket, ok := registry.Get("avatars")
all := registry.All()

Backend interface

All backends implement the same interface:

type Backend interface {
    Put(ctx context.Context, bucket, key string, data io.Reader, meta *ObjectMetadata) (*PutResult, error)
    Get(ctx context.Context, bucket, key string) (*GetResult, error)
    Delete(ctx context.Context, bucket, key string) error
    List(ctx context.Context, bucket string, opts *ListOptions) (*ListResult, error)
    Exists(ctx context.Context, bucket, key string) (bool, error)
    Copy(ctx context.Context, bucket, source, destination string) error
    Close() error
}

Backends

Memory backend

In-memory storage for testing:

backend := storage.NewMemoryBackend()

File backend

Filesystem storage for local development:

backend := storage.NewFileBackend("./data")
// Objects stored at ./data/{bucket}/{key}
// Metadata sidecar at ./data/{bucket}/{key}.meta.json

S3 backend

S3-compatible storage for production:

backend := storage.NewS3Backend(storage.S3Config{
    Endpoint:  "https://s3.amazonaws.com",
    Region:    "us-east-1",
    AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
    SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
    Bucket:    "my-app-storage",
})

Operations

Put

result, err := backend.Put(ctx, "avatars", "user-123/photo.png", file, &storage.ObjectMetadata{
    ContentType: "image/png",
    Metadata:    map[string]string{"uploadedBy": "user-123"},
})

Get

obj, err := backend.Get(ctx, "avatars", "user-123/photo.png")
if err != nil {
    return err
}
defer obj.Body.Close()

// Read the object
data, err := io.ReadAll(obj.Body)

Delete

err := backend.Delete(ctx, "avatars", "user-123/photo.png")

List

result, err := backend.List(ctx, "avatars", &storage.ListOptions{
    Prefix: "user-123/",
})

for _, obj := range result.Objects {
    fmt.Printf("%s (%d bytes)\n", obj.Key, obj.Size)
}

Exists

exists, err := backend.Exists(ctx, "avatars", "user-123/photo.png")

Copy

err := backend.Copy(ctx, "avatars", "user-123/old.png", "user-123/new.png")

Usage patterns

Upload handler

func uploadAvatar(backend storage.Backend) fhttp.Handler {
    return func(ctx *fhttp.Context) *fhttp.Response {
        body, err := ctx.RawBody()
        if err != nil {
            return fhttp.JSONStatus(400, map[string]string{"error": "invalid body"})
        }

        key := fmt.Sprintf("%s/avatar.png", ctx.Param("userId"))
        _, err = backend.Put(ctx.Context(), "avatars", key, bytes.NewReader(body), &storage.ObjectMetadata{
            ContentType: ctx.ContentType(),
        })
        if err != nil {
            return fhttp.InternalError(err.Error())
        }
        return fhttp.JSONStatus(201, map[string]string{"key": key})
    }
}

Choosing backends per environment

func newStorageBackend(env string) storage.Backend {
    switch env {
    case "test":
        return storage.NewMemoryBackend()
    case "development":
        return storage.NewFileBackend("./data")
    default:
        return storage.NewS3Backend(s3Config)
    }
}

Related guides

  • HTTP & Middleware — file upload handlers
  • Configuration — backend configuration
  • TypeScript storage — TypeScript equivalent

On this page

  • Storage
  • Buckets
  • Bucket options
  • Bucket registry
  • Backend interface
  • Backends
  • Memory backend
  • File backend
  • S3 backend
  • Operations
  • Put
  • Get
  • Delete
  • List
  • Exists
  • Copy
  • Usage patterns
  • Upload handler
  • Choosing backends per environment
  • Related guides