storage

package
v0.3.5 Latest Latest
Warning

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

Go to latest
Published: Feb 17, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package storage provides S3-compatible file storage operations.

It offers a simple interface for uploading, retrieving, and managing files with automatic MIME detection, validation, and multi-tenant support.

Basic Usage

Create a storage client and upload files:

cfg := storage.Config{
	Bucket:    "my-bucket",
	Region:    "us-east-1",
	AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
	SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
}

store, err := storage.New(cfg)
if err != nil {
	log.Fatal(err)
}

// Upload from form
fh, _ := c.FormFile("avatar")
info, err := storage.PutFile(ctx, store, fh,
	storage.WithPrefix("avatars"),
	storage.WithACL(storage.ACLPublicRead),
)

Validation

Use WithValidation for validated uploads:

info, err := storage.PutFile(ctx, store, fh,
	storage.WithValidation(
		storage.MaxSize(5 << 20),  // 5MB
		storage.ImageOnly(),
	),
	storage.WithTenant(tenantID),
	storage.WithPrefix("avatars"),
)
if err != nil {
	var verr *storage.FileValidationError
	if errors.As(err, &verr) {
		// Handle validation error
	}
}

URL Generation

Generate URLs for stored files:

// Auto-detect based on ACL (public vs signed)
url, err := store.URL(ctx, info.Key)

// Force signed URL with custom expiry
url, err := store.URL(ctx, info.Key,
	storage.WithSigned(time.Hour),
)

// Signed URL with download disposition
url, err := store.URL(ctx, info.Key,
	storage.WithDownload("document.pdf"),
)

Multi-Tenant Support

Use WithTenant for tenant isolation:

info, err := storage.PutFile(ctx, store, fh,
	storage.WithTenant(tenantID),
	storage.WithPrefix("documents"),
)
// Key: {tenant}/{prefix}/{ulid}.{ext}

Configuration

The Config struct supports environment variables:

type Config struct {
	Bucket          string // STORAGE_BUCKET
	AccessKey       string // STORAGE_ACCESS_KEY
	SecretKey       string // STORAGE_SECRET_KEY
	Endpoint        string // STORAGE_ENDPOINT (for MinIO/custom S3)
	Region          string // STORAGE_REGION (default: us-east-1)
	PublicURL       string // STORAGE_PUBLIC_URL (CDN URL)
	DefaultACL      string // DEFAULT_ACL (default: private)
	PathStyle       bool   // STORAGE_PATH_STYLE (for MinIO)
	MaxDownloadSize int64  // STORAGE_MAX_DOWNLOAD (default: 50MB)
}

Index

Constants

View Source
const (
	DefaultRegion          = "us-east-1"
	DefaultMaxDownloadSize = 50 << 20 // 50MB to prevent abuse
	DefaultSignedURLExpiry = 15 * 60  // 15 minutes in seconds
)

Default configuration values.

View Source
const (
	ErrCodeFileTooLarge = "file_too_large"
	ErrCodeFileTooSmall = "file_too_small"
	ErrCodeInvalidMIME  = "invalid_mime"
	ErrCodeEmptyFile    = "empty_file"
)

Error codes for FileValidationError.

View Source
const DefaultURLExpiry = 15 * time.Minute

DefaultURLExpiry is the default expiry for signed URLs.

View Source
const (
	MIMEOctetStream = "application/octet-stream"
)

MIME type constants.

Variables

View Source
var (
	ErrNotConfigured    = errors.New("storage: not configured")
	ErrInvalidConfig    = errors.New("storage: invalid configuration")
	ErrEmptyFile        = errors.New("storage: file is empty")
	ErrFileTooLarge     = errors.New("storage: file exceeds size limit")
	ErrFileTooSmall     = errors.New("storage: file below minimum size")
	ErrInvalidMIME      = errors.New("storage: file type not allowed")
	ErrNotFound         = errors.New("storage: file not found")
	ErrAccessDenied     = errors.New("storage: access denied")
	ErrUploadFailed     = errors.New("storage: upload failed")
	ErrDeleteFailed     = errors.New("storage: delete failed")
	ErrPresignFailed    = errors.New("storage: presign failed")
	ErrInvalidURL       = errors.New("storage: invalid URL")
	ErrDownloadFailed   = errors.New("storage: failed to download from URL")
	ErrDownloadTooLarge = errors.New("storage: download exceeds size limit")
)

Sentinel errors for storage operations.

Functions

func DetectMIME

func DetectMIME(fh *multipart.FileHeader) string

DetectMIME detects the MIME type of a multipart file header by reading magic bytes. Returns "application/octet-stream" if detection fails.

func ExtFromMIME

func ExtFromMIME(mimeType string) string

ExtFromMIME returns the file extension for a MIME type. Returns empty string if MIME type is unknown.

func IsAudio

func IsAudio(fh *multipart.FileHeader) bool

IsAudio checks if the file is audio based on magic bytes.

func IsDocument

func IsDocument(fh *multipart.FileHeader) bool

IsDocument checks if the file is a document based on magic bytes.

func IsImage

func IsImage(fh *multipart.FileHeader) bool

IsImage checks if the file is an image based on magic bytes.

func IsVideo

func IsVideo(fh *multipart.FileHeader) bool

IsVideo checks if the file is a video based on magic bytes.

func ValidateFile

func ValidateFile(fh *multipart.FileHeader, mimeType string, rules ...ValidationRule) error

ValidateFile runs all validation rules against a file. Returns the first validation error encountered, or nil if all pass. The mimeType should be pre-detected from magic bytes for accuracy.

func ValidateReader

func ValidateReader(size int64, mimeType string, rules ...ValidationRule) error

ValidateReader runs validation rules against Reader-available data (size and MIME type). Only rules that implement ReaderValidationRule are applied; others are silently skipped. Returns the first validation error encountered, or nil if all pass.

Types

type ACL

type ACL string

ACL represents access control levels for stored files.

const (
	// ACLPrivate makes the file accessible only via signed URLs.
	ACLPrivate ACL = "private"

	// ACLPublicRead makes the file publicly readable.
	ACLPublicRead ACL = "public-read"
)

type Config

type Config struct {
	Bucket    string `env:"BUCKET,required"`
	AccessKey string `env:"ACCESS_KEY,required"`
	SecretKey string `env:"SECRET_KEY,required"`

	// Endpoint is the custom S3 endpoint URL (optional, for MinIO or other S3-compatible services).
	Endpoint string `env:"ENDPOINT"`

	Region string `env:"REGION" envDefault:"us-east-1"`

	// PublicURL is the CDN or public URL prefix for public files (optional).
	// If set, public URLs will use this prefix instead of the S3 URL.
	PublicURL string `env:"PUBLIC_URL"`

	DefaultACL string `env:"DEFAULT_ACL" envDefault:"private"`

	// PathStyle enables path-style URLs (required for MinIO).
	PathStyle bool `env:"PATH_STYLE" envDefault:"false"`

	MaxDownloadSize int64 `env:"MAX_DOWNLOAD_SIZE" envDefault:"52428800"`
}

Config holds S3-compatible storage configuration.

type FileInfo

type FileInfo struct {
	Key         string
	ContentType string
	ACL         ACL
	Size        int64
}

FileInfo contains metadata about an uploaded file.

func PutBytes

func PutBytes(ctx context.Context, s Storage, data []byte, filename string, opts ...Option) (*FileInfo, error)

PutBytes uploads byte data to storage. The filename is used to help with key generation but MIME type is detected from content.

func PutFile

func PutFile(ctx context.Context, s Storage, fh *multipart.FileHeader, opts ...Option) (*FileInfo, error)

PutFile uploads a multipart file header to storage. MIME type is detected from magic bytes, not the filename extension. Returns ErrEmptyFile if the file is nil or has zero size. If WithValidation is used and any rule fails, returns *FileValidationError.

func PutFromURL

func PutFromURL(ctx context.Context, s Storage, sourceURL string, maxSize int64, opts ...Option) (*FileInfo, error)

PutFromURL downloads a file from a URL and uploads it to storage. maxSize limits the download size (0 uses default from config). Returns ErrInvalidURL for malformed URLs. Returns ErrDownloadTooLarge if the file exceeds maxSize. Returns ErrDownloadFailed for network or HTTP errors.

type FileValidationError

type FileValidationError struct {
	Details map[string]any // Error-specific data
	Field   string         // Form field name (e.g., "file")
	Code    string         // Error code (e.g., "file_too_large", "invalid_mime", "empty_file")
	Message string         // Human-readable message
}

FileValidationError represents a file validation failure.

func (*FileValidationError) Error

func (e *FileValidationError) Error() string

type Option

type Option func(*putOptions)

Option configures Put operations.

func WithACL

func WithACL(acl ACL) Option

WithACL overrides the default ACL for this upload.

func WithContentType

func WithContentType(ct string) Option

WithContentType overrides the auto-detected content type. Use sparingly; auto-detection from magic bytes is preferred.

func WithKey

func WithKey(key string) Option

WithKey sets an explicit storage key, replacing the auto-generated ULID-based key. Use this to overwrite an existing file at a specific location.

func WithPrefix

func WithPrefix(prefix string) Option

WithPrefix sets a path prefix for the uploaded file. The prefix is added after the tenant (if any) and before the filename. Example: WithPrefix("avatars") results in "avatars/{ulid}.{ext}"

func WithTenant

func WithTenant(id string) Option

WithTenant sets a tenant ID for multi-tenant isolation. The tenant ID becomes the first path segment. Example: WithTenant("tenant123") results in "tenant123/{prefix}/{ulid}.{ext}"

func WithValidation

func WithValidation(rules ...ValidationRule) Option

WithValidation adds validation rules to be applied before upload. If any rule fails, the upload is aborted and a *FileValidationError is returned.

type ReaderValidationRule

type ReaderValidationRule interface {
	ValidateReader(size int64, mimeType string) error
}

ReaderValidationRule validates uploads using only Reader-available data (size and MIME type). Rules that implement this interface can be used with Put() to validate uploads from io.Reader.

type S3Storage

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

S3Storage implements Storage using S3-compatible object storage.

func New

func New(cfg Config) (*S3Storage, error)

New creates a new S3Storage with the given configuration.

func (*S3Storage) Copy

func (s *S3Storage) Copy(ctx context.Context, srcKey, dstKey string) error

Copy copies a file from one key to another within the same bucket. S3 CopyObject preserves ACL by default.

func (*S3Storage) Delete

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

Delete removes a file from S3.

func (*S3Storage) Get

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

Get retrieves a file from S3.

func (*S3Storage) HeadObject

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

HeadObject checks if a file exists and returns its metadata without downloading it.

func (*S3Storage) Put

func (s *S3Storage) Put(ctx context.Context, r io.Reader, size int64, opts ...Option) (*FileInfo, error)

Put uploads data from a reader to S3.

func (*S3Storage) URL

func (s *S3Storage) URL(ctx context.Context, key string, opts ...URLOption) (string, error)

URL generates a URL for accessing the file. By default, returns a signed URL. Use WithPublic() to get an unsigned public URL. If both WithPublic() and WithDownload() are used, signed URL is returned because Content-Disposition headers require signed URLs.

type Storage

type Storage interface {
	// Put uploads data from a reader to storage.
	// The size parameter is used for content-length header.
	// Options can customize key, prefix, tenant, ACL, and content type.
	Put(ctx context.Context, r io.Reader, size int64, opts ...Option) (*FileInfo, error)

	// Get retrieves a file from storage.
	// The caller is responsible for closing the returned reader.
	Get(ctx context.Context, key string) (io.ReadCloser, error)

	// Delete removes a file from storage.
	Delete(ctx context.Context, key string) error

	// URL generates a URL for accessing the file.
	// For private files, returns a signed URL. For public files, returns the public URL.
	// Use URLOptions to customize expiry, download disposition, or force signed/public.
	URL(ctx context.Context, key string, opts ...URLOption) (string, error)
}

Storage defines the interface for file storage operations.

type URLOption

type URLOption func(*urlOptions)

URLOption configures URL generation.

func WithDownload

func WithDownload(filename string) URLOption

WithDownload sets the filename for Content-Disposition: attachment header. This forces the browser to download the file with the specified name. Also implies a signed URL.

func WithExpiry

func WithExpiry(d time.Duration) URLOption

WithExpiry sets the expiry duration for signed URLs. Default is 15 minutes.

func WithPublic

func WithPublic() URLOption

WithPublic forces a public URL regardless of the file's ACL. Note: This only works if the file was uploaded with ACLPublicRead or if the bucket has public access configured.

func WithSigned

func WithSigned(expiry time.Duration) URLOption

WithSigned forces a signed URL regardless of the file's ACL. Optionally set the expiry duration; if zero, uses default expiry.

type ValidationRule

type ValidationRule interface {
	// Validate checks the file and returns an error if validation fails.
	Validate(fh *multipart.FileHeader, mimeType string) error
}

ValidationRule defines a validation check for file uploads.

func AllowedTypes

func AllowedTypes(patterns ...string) ValidationRule

AllowedTypes returns a rule that only accepts files matching the given MIME patterns. Supports wildcards like "image/*".

func DocumentsOnly

func DocumentsOnly() ValidationRule

DocumentsOnly returns a rule that only accepts document files. Includes PDF, Word, Excel, PowerPoint, text, and CSV files.

func ImageOnly

func ImageOnly() ValidationRule

ImageOnly returns a rule that only accepts image files. Equivalent to AllowedTypes("image/*").

func MaxSize

func MaxSize(bytes int64) ValidationRule

MaxSize returns a rule that rejects files larger than the specified size.

func MinSize

func MinSize(bytes int64) ValidationRule

MinSize returns a rule that rejects files smaller than the specified size.

func NotEmpty

func NotEmpty() ValidationRule

NotEmpty returns a rule that rejects empty files.

Jump to

Keyboard shortcuts

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