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
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  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