storage

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: May 11, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

Package storage abstracts blob storage for release artifacts.

The interface is intentionally narrow — only the operations the release service needs. Implementations target Cloudflare R2, AWS S3, MinIO, or any S3-compatible API. Direct browser uploads use presigned PUT URLs; license-gated downloads use presigned GET URLs with a short TTL.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrObjectNotFound  = errors.New("storage: object not found")
	ErrStorageDisabled = errors.New("storage: subsystem not configured")
)

Sentinel errors. Implementations MUST return these for the documented conditions so the service layer can map them to user-facing errors.

Functions

This section is empty.

Types

type Disabled

type Disabled struct{}

Disabled is a no-op implementation used when storage credentials are missing. All methods return ErrStorageDisabled. Wiring it up centrally avoids nil-check noise at call sites.

func (Disabled) Delete

func (Disabled) Delete(_ context.Context, _ string) error

func (Disabled) Exists

func (Disabled) Exists(_ context.Context, _ string) (bool, error)

func (Disabled) Get

func (Disabled) Head

func (Disabled) Head(_ context.Context, _ string) (*ObjectInfo, error)

func (Disabled) PresignedGet

func (Disabled) PresignedGet(_ context.Context, _, _ string, _ time.Duration) (string, error)

func (Disabled) PresignedPut

func (Disabled) PresignedPut(_ context.Context, _, _ string, _ int64, _ time.Duration) (string, error)

type ObjectInfo

type ObjectInfo struct {
	Size        int64
	ContentType string
	ETag        string // opaque, used for client-side cache invalidation
}

ObjectInfo is the subset of object metadata the release service consumes.

type S3Config

type S3Config struct {
	Endpoint       string // empty = AWS S3 default endpoint
	Region         string // R2 wants "auto"; AWS wants real region
	Bucket         string
	AccessKey      string
	SecretKey      string
	ForcePathStyle bool
}

S3Config bundles the parameters needed to construct an S3-compatible client. It is decoupled from the application config struct so this package has no dependency on internal/config.

type S3Storage

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

S3Storage is an S3-compatible object store implementation. It is safe for concurrent use; the underlying S3 client is goroutine-safe.

func NewS3

func NewS3(_ context.Context, c S3Config) (*S3Storage, error)

NewS3 constructs an S3Storage. It validates the config eagerly but does NOT reach out to the network — credentials are verified on first real call.

Why no eager network probe: in production we want the server to start even if the storage backend is briefly down, surfacing errors per-request rather than failing fast at boot.

Credentials: this constructor uses ONLY the static credentials passed via S3Config. We do not call awsconfig.LoadDefaultConfig because that would pull from env vars / IMDS / IAM role / shared profile, leading to confusing debug experiences when the runtime environment provides different creds than the operator intended.

func (*S3Storage) Delete

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

Delete removes an object. S3 DeleteObject is idempotent on AWS (calling on a missing key returns success). We normalise non-AWS backends that return NoSuchKey to match.

func (*S3Storage) Exists

func (s *S3Storage) Exists(ctx context.Context, key string) (bool, error)

Exists reports whether the object exists. Equivalent to Head + presence check; provided as a convenience so callers don't allocate ObjectInfo just to discard it.

func (*S3Storage) Get

func (s *S3Storage) Get(ctx context.Context, key string) (io.ReadCloser, error)

Get returns a streaming reader for the object body. Caller MUST close the returned ReadCloser. Used by the release signing pipeline — license-gated downloads should use PresignedGet instead so the bytes flow client → R2 directly without consuming our bandwidth.

func (*S3Storage) Head

func (s *S3Storage) Head(ctx context.Context, key string) (*ObjectInfo, error)

Head fetches object metadata. Returns ErrObjectNotFound on 404.

func (*S3Storage) PresignedGet

func (s *S3Storage) PresignedGet(ctx context.Context, key, filenameHint string, expires time.Duration) (string, error)

PresignedGet returns a short-TTL URL for license-gated downloads. The filenameHint, when non-empty, controls the Content-Disposition header so browsers save the file with a recognisable name.

func (*S3Storage) PresignedPut

func (s *S3Storage) PresignedPut(ctx context.Context, key, contentType string, expectedSize int64, expires time.Duration) (string, error)

PresignedPut returns a URL the client can PUT a file to.

IMPORTANT: contentType and expectedSize are *hints*, not enforced limits. AWS SDK v2's default presigner does not include Content-Type or Content-Length in the SigV4 signed headers, so the storage backend will happily accept mismatched headers. Real size/type enforcement must happen at FinalizeUpload (Head check) or via bucket-side policies (e.g. R2 max-object-size, S3 bucket policy with conditional Content-Length).

type Storage

type Storage interface {
	// PresignedPut returns a URL the client can PUT a file to. The contentType
	// and expectedSize parameters are HINTS only — the AWS SDK's default
	// presigner does not include them in the SigV4 signed headers, so storage
	// will accept mismatched headers. Real validation happens at FinalizeUpload
	// (Head check) or via bucket-side policies.
	PresignedPut(ctx context.Context, key, contentType string, expectedSize int64, expires time.Duration) (string, error)

	// PresignedGet returns a license-gated download URL with a short TTL.
	// The optional filename hint is encoded into Content-Disposition so
	// browsers prompt with a sensible name regardless of the storage key.
	PresignedGet(ctx context.Context, key, filenameHint string, expires time.Duration) (string, error)

	// Head fetches the metadata (size, content-type, etag) for an object.
	// Returns ErrObjectNotFound if the key does not exist.
	Head(ctx context.Context, key string) (*ObjectInfo, error)

	// Exists reports whether the object exists. Returns (false, nil) when
	// the object is absent — distinguishing it from a transport error.
	Exists(ctx context.Context, key string) (bool, error)

	// Get streams the full object body. Caller MUST close the returned
	// ReadCloser. Returns ErrObjectNotFound on 404. Used by the release
	// signing pipeline which needs the artifact bytes server-side; do not
	// use it for license-gated downloads (use PresignedGet instead — that
	// path doesn't consume our bandwidth).
	Get(ctx context.Context, key string) (io.ReadCloser, error)

	// Delete removes an object. Returns nil if the object did not exist
	// (idempotent — matches S3 DeleteObject semantics).
	Delete(ctx context.Context, key string) error
}

Storage is the minimal contract for an object store.

Jump to

Keyboard shortcuts

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