Documentation
¶
Overview ¶
Package objectstorage is the uploads.UploadManager backed by gocloud.dev/blob.
One package covers six backends, selected by Config.Provider: "s3", "gcp", "r2", "backblaze_b2", "filesystem", and "memory". An unrecognized value is errors.ErrUnknownProvider rather than a working-looking default, and the sub-config for a provider must be present when that provider is named and absent otherwise, so a config carrying R2 credentials while naming S3 is refused rather than quietly ignored.
What each choice commits a deployment to ¶
- s3 and gcp take no credentials from Config. They resolve the ambient chain — the AWS default chain, GCP Application Default Credentials — so they commit the deployment to whatever identity the process runs as, and rotate underneath it without a rebuild.
- r2 and backblaze_b2 take static keys from Config and construct the vendor endpoint from an account ID or region. Both speak S3's API; what they cost is a long-lived key pair this module has to be handed.
- filesystem writes under a root directory, creating directories at 0700 rather than gocloud's 0777 default so other users on the host cannot traverse in. DirectoryMode overrides that and parses as octal, because every way anyone writes a Unix mode is octal.
- memory keeps objects in this process. Nothing survives the process and nothing is shared between replicas; it is for tests and local runs.
Capabilities are implemented, not therefore supported ¶
Uploader satisfies every optional interface in uploads — RangeReader, URLSigner, Attributer, Lister — because gocloud exposes all four uniformly. Whether a given backend can honor them is a separate question, and the one that bites is signing: SignedURL fails on memory, and on filesystem, which is opened with no URL signer. A caller that needs signed URLs across environments needs to know that the development backend cannot mint them.
Construction does not touch the network ¶
No provider is probed for reachability at construction, GCP included. What gocloud offers is a list operation, and listing is a distinct permission from reading and writing — the least-privilege policy for a service that only saves and opens grants neither, so a probe would refuse a bucket the Uploader can use perfectly well, and refuse it at startup where the deployment cannot proceed. It would also make a transient blip during a rollout into a service that never comes up.
Unreachability is modeled at runtime instead: every operation runs through a circuit breaker built from Config.CircuitBreaker, and a rejected one returns circuitbreaking.ErrCircuitBroken naming the operation, with the provider's own error behind it on the way in.
BucketPrefix ¶
When set, the prefix gets a trailing "/" whether or not the config supplied one. gocloud concatenates the prefix with the key verbatim, so a prefix of "acme" would turn key "1/x" into "acme1/x" — which is also what tenant "acme1" writes, silently sharing a namespace and returning each other's objects from List.
Writes are all-or-nothing ¶
gocloud commits a write when the writer closes without error, so Save cancels the write's context before closing when the copy fails. Without that, a truncated object would be committed at the path while Save returned an error.
Example ¶
package main
import (
"context"
"fmt"
"github.com/primandproper/platform-go/v11/uploads"
"github.com/primandproper/platform-go/v11/uploads/objectstorage"
)
func newExampleManager() *objectstorage.Uploader {
m, err := objectstorage.NewUploadManager(
context.Background(),
&objectstorage.Config{BucketName: "example", Provider: objectstorage.MemoryProvider},
)
if err != nil {
panic(err)
}
return m
}
func main() {
ctx := context.Background()
mgr := newExampleManager()
if err := uploads.SaveFile(ctx, mgr, "greeting.txt", []byte("hello world")); err != nil {
panic(err)
}
data, err := uploads.ReadFile(ctx, mgr, "greeting.txt")
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
Output: hello world
Index ¶
- Constants
- Variables
- func RegisterUploadManager(i do.Injector)
- type BackblazeB2Config
- type Config
- type DirectoryMode
- type FilesystemConfig
- type Option
- type R2Config
- type Uploader
- func (u *Uploader) Attributes(ctx context.Context, path string) (*uploads.Attributes, error)
- func (u *Uploader) Close() error
- func (u *Uploader) Delete(ctx context.Context, path string) error
- func (u *Uploader) Exists(ctx context.Context, path string) (bool, error)
- func (u *Uploader) List(ctx context.Context, prefix string) iter.Seq2[uploads.ObjectInfo, error]
- func (u *Uploader) Open(ctx context.Context, path string) (io.ReadCloser, error)
- func (u *Uploader) OpenRange(ctx context.Context, path string, offset, length int64) (io.ReadCloser, error)
- func (u *Uploader) Save(ctx context.Context, path string, r io.Reader, opts ...uploads.SaveOption) error
- func (u *Uploader) SignedURL(ctx context.Context, path string, opts *uploads.SignedURLOptions) (string, error)
Examples ¶
Constants ¶
const ( // FilesystemProvider indicates we'd like to use the filesystem adapter for blob. FilesystemProvider = "filesystem" // MemoryProvider indicates we'd like to use the memory adapter for blob. MemoryProvider = "memory" // S3Provider indicates we'd like to use the s3 adapter for blob. S3Provider = "s3" // GCPCloudStorageProvider indicates we'd like to use the GCP adapter for blob objectstorage. GCPCloudStorageProvider = "gcp" // R2Provider indicates we'd like to use the Cloudflare R2 adapter for blob. R2Provider = "r2" // BackblazeB2Provider indicates we'd like to use the Backblaze B2 adapter for blob. BackblazeB2Provider = "backblaze_b2" )
Variables ¶
var ErrNilConfig = platformerrors.New("nil config provided")
ErrNilConfig denotes that the provided configuration is nil.
An unrecognized provider is reported as errors.ErrUnknownProvider rather than a sentinel of this package's own: startup code branches on one thing for "the config named a provider nothing implements", whichever package it reached.
Functions ¶
func RegisterUploadManager ¶
RegisterUploadManager registers both *Uploader and uploads.UploadManager with the injector. Prerequisite: *Config must be registered (e.g. via uploadscfg.RegisterStorageConfig).
Types ¶
type BackblazeB2Config ¶
type BackblazeB2Config struct {
ApplicationKeyID string `env:"APPLICATION_KEY_ID" json:"applicationKeyID,omitempty" yaml:"applicationKeyID,omitempty"`
ApplicationKey string `env:"APPLICATION_KEY" json:"applicationKey,omitempty" yaml:"applicationKey,omitempty"`
Region string `env:"REGION" json:"region,omitempty" yaml:"region,omitempty"`
// contains filtered or unexported fields
}
BackblazeB2Config configures a Backblaze B2-based objectstorage provider.
func (*BackblazeB2Config) ValidateWithContext ¶
func (c *BackblazeB2Config) ValidateWithContext(ctx context.Context) error
ValidateWithContext validates the BackblazeB2Config.
type Config ¶
type Config struct {
FilesystemConfig *FilesystemConfig `env:",init" envPrefix:"FILESYSTEM_" json:"filesystem,omitempty" yaml:"filesystem,omitempty"`
R2Config *R2Config `env:",init" envPrefix:"R2_" json:"r2,omitempty" yaml:"r2,omitempty"`
BackblazeB2Config *BackblazeB2Config `env:",init" envPrefix:"BACKBLAZE_B2_" json:"backblazeB2,omitempty" yaml:"backblazeB2,omitempty"`
BucketPrefix string `env:"BUCKET_PREFIX" json:"bucketPrefix,omitempty" yaml:"bucketPrefix,omitempty"`
BucketName string `env:"BUCKET_NAME" json:"bucketName,omitempty" yaml:"bucketName,omitempty"`
Provider string `env:"PROVIDER" json:"provider,omitempty" yaml:"provider,omitempty"`
CircuitBreaker circuitbreakingcfg.Config `env:",init" envPrefix:"CIRCUIT_BREAKING_" json:"circuitBreakerConfig,omitzero" yaml:"circuitBreakerConfig,omitempty"`
// contains filtered or unexported fields
}
Config configures our UploadManager.
func (*Config) ValidateWithContext ¶
ValidateWithContext validates the Config. It first canonicalizes Provider (trim + lowercase) so validation, the conditional sub-config rules, and dispatch in selectBucket all agree — otherwise a value like "S3" or " s3 " would fail validation yet dispatch cleanly.
type DirectoryMode ¶
DirectoryMode is a Unix file mode that parses as octal from configuration.
It exists because os.FileMode is a uint32, and every config decoder reads a bare integer in base 10 — so DIRECTORY_MODE=0700 parsed to decimal 700, which is 0o1274: the sticky bit plus a permission set nobody asked for. Every way anyone writes a Unix mode is octal, so that is what this parses.
func (DirectoryMode) FileMode ¶
func (m DirectoryMode) FileMode() os.FileMode
FileMode returns the mode as an os.FileMode.
func (DirectoryMode) MarshalText ¶
func (m DirectoryMode) MarshalText() ([]byte, error)
MarshalText renders the mode as octal, so a round trip through config is stable.
func (*DirectoryMode) UnmarshalText ¶
func (m *DirectoryMode) UnmarshalText(text []byte) error
UnmarshalText parses a mode in octal, with or without a leading "0" or "0o".
type FilesystemConfig ¶
type FilesystemConfig struct {
RootDirectory string `env:"ROOT_DIRECTORY" json:"rootDirectory,omitempty" yaml:"rootDirectory,omitempty"`
// DirectoryMode is the mode for directories the backend creates, parsed as
// octal. Defaults to 0700 when unset (zero).
DirectoryMode DirectoryMode `env:"DIRECTORY_MODE" json:"directoryMode,omitempty" yaml:"directoryMode,omitempty"`
// contains filtered or unexported fields
}
FilesystemConfig configures a filesystem-based objectstorage provider.
func (*FilesystemConfig) ValidateWithContext ¶
func (c *FilesystemConfig) ValidateWithContext(ctx context.Context) error
ValidateWithContext validates the FilesystemConfig.
type Option ¶
type Option func(*options)
Option configures the Uploader this package constructs. The zero configuration works: an absent logger logs nowhere, an absent tracer provider traces nowhere, and an absent metrics provider records nothing.
func WithMetricsProvider ¶
WithMetricsProvider attaches a metrics provider for the package's counters and histograms.
func WithTracerProvider ¶
WithTracerProvider attaches a tracer provider, enabling spans on every operation.
type R2Config ¶
type R2Config struct {
AccountID string `env:"ACCOUNT_ID" json:"accountID,omitempty" yaml:"accountID,omitempty"`
AccessKeyID string `env:"ACCESS_KEY_ID" json:"accessKeyID,omitempty" yaml:"accessKeyID,omitempty"`
SecretAccessKey string `env:"SECRET_ACCESS_KEY" json:"secretAccessKey,omitempty" yaml:"secretAccessKey,omitempty"`
// contains filtered or unexported fields
}
R2Config configures a Cloudflare R2-based objectstorage provider.
type Uploader ¶
type Uploader struct {
// contains filtered or unexported fields
}
Uploader implements the uploads.UploadManager interface.
func NewUploadManager ¶
NewUploadManager provides a new uploads.UploadManager.
func (*Uploader) Attributes ¶
Attributes fetches the stored metadata for the object at path.
Example ¶
package main
import (
"context"
"fmt"
"github.com/primandproper/platform-go/v11/uploads"
"github.com/primandproper/platform-go/v11/uploads/objectstorage"
)
func newExampleManager() *objectstorage.Uploader {
m, err := objectstorage.NewUploadManager(
context.Background(),
&objectstorage.Config{BucketName: "example", Provider: objectstorage.MemoryProvider},
)
if err != nil {
panic(err)
}
return m
}
func main() {
ctx := context.Background()
mgr := newExampleManager()
if err := uploads.SaveFile(ctx, mgr, "photo.png", []byte("hello world")); err != nil {
panic(err)
}
attrs, err := mgr.Attributes(ctx, "photo.png")
if err != nil {
panic(err)
}
fmt.Println(attrs.Size)
}
Output: 11
func (*Uploader) Close ¶
Close releases the underlying gocloud bucket, and with it whatever client the provider opened. It does not delete anything.
It is safe to call more than once; gocloud's Close reports an error only on the first call that actually closes.
func (*Uploader) List ¶
List streams the objects stored under prefix. Objects are fetched lazily as the returned iterator is consumed; the caller may stop early by breaking out of the range loop.
Example ¶
package main
import (
"context"
"fmt"
"sort"
"github.com/primandproper/platform-go/v11/uploads"
"github.com/primandproper/platform-go/v11/uploads/objectstorage"
)
func newExampleManager() *objectstorage.Uploader {
m, err := objectstorage.NewUploadManager(
context.Background(),
&objectstorage.Config{BucketName: "example", Provider: objectstorage.MemoryProvider},
)
if err != nil {
panic(err)
}
return m
}
func main() {
ctx := context.Background()
mgr := newExampleManager()
for _, name := range []string{"data/a.txt", "data/b.txt", "other/c.txt"} {
if err := uploads.SaveFile(ctx, mgr, name, []byte("x")); err != nil {
panic(err)
}
}
// List streams objects lazily; break out of the loop to stop early.
var paths []string
for obj, err := range mgr.List(ctx, "data/") {
if err != nil {
panic(err)
}
paths = append(paths, obj.Path)
}
sort.Strings(paths)
fmt.Println(paths)
}
Output: [data/a.txt data/b.txt]
func (*Uploader) Open ¶
Open returns a reader for the object at path. The caller is responsible for closing it.
func (*Uploader) OpenRange ¶
func (u *Uploader) OpenRange(ctx context.Context, path string, offset, length int64) (io.ReadCloser, error)
OpenRange returns a reader over length bytes of the object at path, starting at offset. A negative length reads to the end. The caller is responsible for closing it.
func (*Uploader) Save ¶
func (u *Uploader) Save(ctx context.Context, path string, r io.Reader, opts ...uploads.SaveOption) error
Save writes the contents of r to the object at path.
func (*Uploader) SignedURL ¶
func (u *Uploader) SignedURL(ctx context.Context, path string, opts *uploads.SignedURLOptions) (string, error)
SignedURL mints a signed URL granting temporary, direct access to the object at path. Not all providers support signing (e.g. the in-memory and unsigned filesystem backends return an error).