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 ¶
- Variables
- type Disabled
- func (Disabled) Delete(_ context.Context, _ string) error
- func (Disabled) Exists(_ context.Context, _ string) (bool, error)
- func (Disabled) Get(_ context.Context, _ string) (io.ReadCloser, error)
- func (Disabled) Head(_ context.Context, _ string) (*ObjectInfo, error)
- func (Disabled) PresignedGet(_ context.Context, _, _ string, _ time.Duration) (string, error)
- func (Disabled) PresignedPut(_ context.Context, _, _ string, _ int64, _ time.Duration) (string, error)
- type ObjectInfo
- type S3Config
- type S3Storage
- func (s *S3Storage) Delete(ctx context.Context, key string) error
- func (s *S3Storage) Exists(ctx context.Context, key string) (bool, error)
- func (s *S3Storage) Get(ctx context.Context, key string) (io.ReadCloser, error)
- func (s *S3Storage) Head(ctx context.Context, key string) (*ObjectInfo, error)
- func (s *S3Storage) PresignedGet(ctx context.Context, key, filenameHint string, expires time.Duration) (string, error)
- func (s *S3Storage) PresignedPut(ctx context.Context, key, contentType string, expectedSize int64, ...) (string, error)
- type Storage
Constants ¶
This section is empty.
Variables ¶
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) PresignedGet ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) 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.