storage

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

README

storage

File storage abstraction with presigned URL support. Backends are pluggable — swap between local filesystem (development) and AWS S3 (production) without changing application code.

Usage

import (
    "github.com/OpenNSW/core/storage"
    "github.com/OpenNSW/core/storage/drivers"
)

driver, err := storage.NewStorageFromConfig(ctx, storage.Config{
    Type: storage.TypeS3,
    S3: drivers.S3Config{
        Bucket: "my-uploads",
        Region: "ap-southeast-2",
    },
    PresignTTLSeconds: 900,
})
svc := storage.NewService(driver)

Use storage.TypeLocal for development. It stores files under Config.Local.BaseDir and stands in for S3's presigned URLs with URLs of its own, signed by Config.Local.PutSecret. They point at {RoutePrefix}/{key}/content on Config.Local.PublicURL; the prefix defaults to /api/v1/storage. Serve those routes with storage.LocalContentHandler on the server at that origin:

if local, ok := driver.(*drivers.LocalFSDriver); ok {
    storage.NewLocalContentHandler(local).RegisterRoutes(mux) // PUT and GET local.ContentPattern()
}

The routes need no auth middleware. Like an S3 presigned URL, each request has to carry the token the driver signed for that key.

Config embeds each driver's own config type (drivers.LocalConfig, drivers.S3Config) verbatim, rather than flattening every backend's settings into one struct — so each driver keeps ownership of its config shape and validation. All three carry yaml struct tags, so Config can be embedded in a larger application config struct and populated generically (e.g. via yaml.Unmarshal, or configyaml.LoadAndExpand for {{env:}}/{{file:}} secret placeholders):

storage:
  type: s3
  s3:
    bucket: my-uploads
    region: ap-southeast-2
    accessKey: "{{env:AWS_ACCESS_KEY_ID}}"
    secretKey: "{{env:AWS_SECRET_ACCESS_KEY}}"
  presignTTLSeconds: 900

Operations

Upload (presigned)
meta, err := svc.Upload(ctx, "passport.pdf", fileSize, "application/pdf")
// meta.Key       — opaque storage key; persist this to your database
// meta.UploadURL — presigned PUT URL; return to the client for direct upload

The client uploads directly to the storage backend — the file never passes through your application server.

Save (server-side)

For content your service already holds, such as a document it generated or a file it fetched from another system:

meta, err := svc.Save(ctx, "certificate.pdf", "application/pdf", bytes.NewReader(pdf), int64(len(pdf)))
// meta.Key — opaque storage key; persist this to your database. Save stores only the
//            bytes and content type, so the key is the only way to find the file again.
  • There's no upload URL. The bytes go straight to the backend through the driver.
  • The upload limits (WithAllowedUploadTypes, WithMaxUploadSize) don't apply. They govern what clients may upload, and a service decides for itself what it stores.
  • size is recorded in the returned metadata as given; Save doesn't count or check it.
  • For S3 over plain HTTP (e.g. a local MinIO), pass a seekable reader such as *bytes.Reader or *os.File.
Download
// Stream the file contents
content, mimeType, err := svc.Download(ctx, fileKey)

// Or get a presigned download URL for the client
url, err := svc.GetDownloadURL(ctx, fileKey)
// url — presigned GET URL, valid for PresignTTLSeconds
Delete
err := svc.Delete(ctx, fileKey)
Upload limits

By default, Upload accepts any MIME type and caps files at 32MB. Pass options to NewService to restrict the types or change the cap:

svc := storage.NewService(driver,
    storage.WithAllowedUploadTypes("application/pdf", "image/png"), // others: ErrContentTypeNotAllowed
    storage.WithMaxUploadSize(10<<20),                              // bytes; default 32MB
)
Option Default Effect
WithAllowedUploadTypes(types...) any type accepted Upload returns ErrContentTypeNotAllowed for any other type. Called with no types, it rejects every upload
WithMaxUploadSize(n) 32MB Upload returns *FileTooLargeError for a larger size. Panics if n is not positive

Upload also returns ErrInvalidSize for a size that isn't positive. The limits are checked when the upload URL is issued, and the URL is signed for that size and type, so a client can't upload something else with it. storage.NewHTTPHandler(svc) maps these errors to 415 and 400. The limits apply to Upload only, not to Save.

Implementing a custom driver

type StorageDriver interface {
    Save(ctx context.Context, key string, body io.Reader, contentType string) error
    Get(ctx context.Context, key string) (io.ReadCloser, string, error)
    Delete(ctx context.Context, key string) error
    GetDownloadURL(ctx context.Context, key string) (string, error)
    GetUploadURL(ctx context.Context, key string, contentType string, maxSizeBytes int64) (string, error)
}

Register your driver by passing it directly to storage.NewService(driver).

Config reference

Config.Type selects the backend (storage.TypeS3 / storage.TypeLocal); only the matching nested config is read. PresignTTLSeconds (top-level, required for both backends) controls how long a presigned upload/download URL stays valid.

S3 (Config.S3, drivers.S3Config, yaml:"s3")
Field YAML key Description
Endpoint endpoint Optional custom endpoint URL for S3-compatible stores (e.g. MinIO or LocalStack). Empty targets AWS S3
Bucket bucket S3 bucket name
Region region AWS region (e.g. ap-southeast-2)
AccessKey / SecretKey accessKey / secretKey Static credentials; must be set together. Empty uses the default AWS credential chain
PublicURL publicURL Optional base URL files are served from (e.g. a CDN in front of the bucket). Currently unused: download URLs are always presigned against the bucket
Local filesystem (Config.Local, drivers.LocalConfig, yaml:"local")
Field YAML key Description
BaseDir baseDir Directory to store files under (created if absent)
PublicURL publicURL Origin of the server that serves LocalContentHandler (e.g. http://localhost:8080)
RoutePrefix routePrefix Path the content routes sit under, e.g. /files. Optional; defaults to /api/v1/storage. Routes are {RoutePrefix}/{key}/content
PutSecret putSecret Signs the driver's upload and download URLs
Upgrading from the flattened Config

Config used to carry every backend's fields flattened at the top level. If you're updating existing construction code:

Old field New field
LocalBaseDir Local.BaseDir
LocalPublicURL Local.PublicURL
LocalPutSecret Local.PutSecret
S3Endpoint S3.Endpoint
S3Bucket S3.Bucket
S3Region S3.Region
S3AccessKey S3.AccessKey
S3SecretKey S3.SecretKey
S3PublicURL S3.PublicURL
S3UseSSL Removed — it was never read anywhere
PresignTTL (time.Duration) PresignTTLSeconds (int, whole seconds)
Upgrading: the local content routes have their own handler

HTTPHandler.UploadContentLocal and HTTPHandler.DownloadContent are gone. storage.LocalContentHandler serves them instead, on the path the local driver builds its URLs from:

// Before
mux.HandleFunc("PUT /api/v1/storage/{key}/content", handler.UploadContentLocal)
mux.HandleFunc("GET /api/v1/storage/{key}/content", handler.DownloadContent)

// After
storage.NewLocalContentHandler(localDriver).RegisterRoutes(mux)

The routes stay at /api/v1/storage/{key}/content unless you set Local.RoutePrefix. GetDownloadURL on the local driver now returns an error when PublicURL is empty, as GetUploadURL already did. Before, it returned the bare key.

Upgrading: upload content types are no longer restricted by default

The HTTP handler used to accept only these upload types: PDF, JPEG, PNG, GIF, WebP and XLSX. The limits now belong to the Service, and it accepts any type by default. To keep the old behavior, pass that list to NewService:

svc := storage.NewService(driver, storage.WithAllowedUploadTypes(
    "application/pdf",
    "image/jpeg",
    "image/png",
    "image/gif",
    "image/webp",
    // .xlsx only: OOXML workbooks cannot carry VBA macros, unlike legacy .xls.
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
))

Documentation

Index

Constants

View Source
const (
	TypeLocal = "local"
	TypeS3    = "s3"
)

Supported Config.Type values.

Variables

View Source
var (
	// ErrInvalidSize is returned by Upload for a size that is not positive.
	ErrInvalidSize = errors.New("storage: size must be greater than 0")
	// ErrContentTypeNotAllowed is returned by Upload for a MIME type outside
	// the list set with WithAllowedUploadTypes.
	ErrContentTypeNotAllowed = errors.New("storage: content type not allowed")
)
View Source
var ErrInvalidKey = errors.New("storage: invalid key")

ErrInvalidKey is returned when a key is not one Upload could have created. Callers can use errors.Is(err, storage.ErrInvalidKey) to detect it.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Type is the storage backend: "local" or "s3".
	Type string `yaml:"type"`

	// Local is used when Type == "local".
	Local drivers.LocalConfig `yaml:"local"`
	// S3 is used when Type == "s3".
	S3 drivers.S3Config `yaml:"s3"`

	// PresignTTLSeconds is how long a presigned upload/download URL stays
	// valid, in seconds. Required for both backends. Expressed in seconds
	// (rather than time.Duration) so it decodes cleanly from YAML.
	PresignTTLSeconds int `yaml:"presignTTLSeconds"`
}

Config selects a storage backend via Type and carries each backend's own settings. Only the config for the selected Type is read. Rather than flattening every backend's settings into one struct, Config embeds each driver's own Config (drivers.LocalConfig, drivers.S3Config) verbatim — so each driver keeps ownership of its config shape and validation.

Config and the backend Config types it embeds carry yaml struct tags, so it can be embedded in a larger application config struct and populated generically (e.g. via yaml.Unmarshal) rather than constructed by hand.

func (Config) Validate

func (c Config) Validate() error

Validate reports misconfiguration before NewStorageFromConfig is called, delegating to the selected backend's own Validate.

type FileMetadata

type FileMetadata struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Key       string `json:"key"`
	URL       string `json:"url,omitempty"`
	UploadURL string `json:"upload_url,omitempty"`
	Size      int64  `json:"size"`
	MimeType  string `json:"mime_type"`
}

FileMetadata represents the metadata of an uploaded file

type FileTooLargeError added in v0.3.0

type FileTooLargeError struct {
	Limit int64
}

FileTooLargeError is returned by Upload for a file larger than the limit.

func (*FileTooLargeError) Error added in v0.3.0

func (e *FileTooLargeError) Error() string

type HTTPHandler

type HTTPHandler struct {
	Service *Service
}

func NewHTTPHandler

func NewHTTPHandler(service *Service) *HTTPHandler

func (*HTTPHandler) Delete

func (h *HTTPHandler) Delete(w http.ResponseWriter, r *http.Request)

func (*HTTPHandler) Download

func (h *HTTPHandler) Download(w http.ResponseWriter, r *http.Request)

func (*HTTPHandler) Upload

func (h *HTTPHandler) Upload(w http.ResponseWriter, r *http.Request)

type LocalContentHandler added in v0.3.0

type LocalContentHandler struct {
	// contains filtered or unexported fields
}

LocalContentHandler serves the routes the local driver's upload and download URLs point at, standing in for S3 in local development. It is separate from HTTPHandler because it only exists for the local driver, and the app builds it only when that is the driver it chose.

The routes take no other auth: like an S3 presigned URL, each request must carry the token the driver signed for that key and operation.

func NewLocalContentHandler added in v0.3.0

func NewLocalContentHandler(driver *drivers.LocalFSDriver) *LocalContentHandler

func (*LocalContentHandler) RegisterRoutes added in v0.3.0

func (h *LocalContentHandler) RegisterRoutes(mux *http.ServeMux)

RegisterRoutes serves PUT and GET on the driver's ContentPattern, the path the driver builds its URLs from.

type Service

type Service struct {
	Driver StorageDriver
	// contains filtered or unexported fields
}

Service coordinates file storage operations and manages metadata

func NewService

func NewService(driver StorageDriver, opts ...ServiceOption) *Service

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, key string) error

Delete removes a file from storage

func (*Service) Download

func (s *Service) Download(ctx context.Context, key string) (io.ReadCloser, string, error)

Download retrieves the file content and its MIME type

func (*Service) DownloadURL added in v0.3.0

func (s *Service) DownloadURL(ctx context.Context, key string) (string, int64, error)

DownloadURL is GetDownloadURL plus the time, in unix seconds, at which the URL stops working.

func (*Service) GetDownloadURL

func (s *Service) GetDownloadURL(ctx context.Context, key string) (string, error)

GetDownloadURL generates a time-limited or presigned URL for the given key

func (*Service) Save added in v0.3.0

func (s *Service) Save(ctx context.Context, filename, mime string, body io.Reader, size int64) (*FileMetadata, error)

Save stores content the caller already holds under a new key and returns its metadata. Persist the returned Key: Save stores only the bytes and the content type, so the key is the only way to find the file again.

Unlike Upload, there is no URL for anyone to upload to, and the Service's upload limits (WithAllowedUploadTypes, WithMaxUploadSize) don't apply: they govern what clients may upload, and a service decides for itself what it stores.

size is recorded in the returned metadata as given; Save doesn't count or check it. body is passed to the driver as is. For S3 over plain HTTP (e.g. a local MinIO), pass a seekable reader such as *bytes.Reader or *os.File.

func (*Service) Upload

func (s *Service) Upload(ctx context.Context, filename string, size int64, mime string) (*FileMetadata, error)

Upload handles the preparation of a file upload by generating a unique key and a presigned/upload URL via the storage driver. The size and type are checked against the Service's limits here, because the presigned URL is what binds the upload to them.

type ServiceOption added in v0.3.0

type ServiceOption func(*Service)

ServiceOption configures a Service.

func WithAllowedUploadTypes added in v0.3.0

func WithAllowedUploadTypes(types ...string) ServiceOption

WithAllowedUploadTypes limits Upload to the given MIME types; any other type is rejected with ErrContentTypeNotAllowed. Called with no types, it rejects every upload. Without it, Upload accepts any type. It does not apply to Save.

func WithMaxUploadSize added in v0.3.0

func WithMaxUploadSize(n int64) ServiceOption

WithMaxUploadSize sets the largest file, in bytes, that Upload accepts. Without it the limit is 32MB. It panics if n is not positive. It does not apply to Save.

type StorageDriver

type StorageDriver interface {
	// Save writes the content to the storage under key
	Save(ctx context.Context, key string, body io.Reader, contentType string) error

	// Get returns a ReadCloser to stream the file back and its content type
	Get(ctx context.Context, key string) (io.ReadCloser, string, error)

	// Delete removes the file
	Delete(ctx context.Context, key string) error

	// GetDownloadURL returns a presigned or time-limited URL for downloading
	GetDownloadURL(ctx context.Context, key string) (string, error)

	// GetUploadURL returns a presigned URL for uploading a file directly to storage
	GetUploadURL(ctx context.Context, key string, contentType string, maxSizeBytes int64) (string, error)
}

StorageDriver defines how we interact with the binary storage

func NewStorageFromConfig

func NewStorageFromConfig(ctx context.Context, cfg Config) (StorageDriver, error)

NewStorageFromConfig creates a storage instance based on the provided configuration. It validates cfg first, so the type switch below only ever sees a supported Type.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL