Skip to content

internal/platform/blobstore

internal/platform/blobstore is the sole sanctioned wrapper around S3-compatible object storage for plexsphere. Direct imports of github.com/aws/aws-sdk-go-v2/service/s3 from any bounded context are rejected by the no-direct-persistence-from-contexts depguard rule in .golangci.yml.

blobstore presents one API to both SeaweedFS (self-hosted) and AWS S3 (SaaS) per README § Storage Topology, so the choice of backing store is operationally transparent to bounded contexts.

This document is the authoritative reference for:

See also:

Client Config

FieldTypeDefaultPurpose
Endpointstring""S3-compatible API URL. Empty uses AWS's region-derived default. Set to the SeaweedFS gateway for self-hosted.
Regionstring— (REQUIRED)AWS region or region-alias. Missing → error tagged (REQ-007, PX-0003).
AccessKeystring— (REQUIRED)S3 access-key ID. Analogous to a username; allowed in error messages.
SecretKeystring— (REQUIRED)S3 secret access key. Sensitive — never echoed in errors, logs, or metrics.
UsePathStyleboolfalsePath-style (host/bucket) vs. virtual-host (bucket.host). SeaweedFS requires true. AWS S3 accepts both.
AllowInsecureEndpointboolfalseOpt-in for plaintext http:// Endpoints. Default rejects http:// with ErrInsecureEndpoint so a misconfigured public-S3 endpoint cannot ship credentials in the clear. Set true only for in-cluster SeaweedFS over a mesh that provides mTLS.

Constructing a client

NewClient(ctx, cfg, logger) (*Client, error) validates the config, loads the AWS SDK configuration with static credentials, and binds the S3 client to the optional Endpoint and UsePathStyle flag. It does not make a network call — readiness is asserted via ProbeFunc on /readyz.

go
client, err := blobstore.NewClient(ctx, blobstore.Config{
    Endpoint:  "http://seaweedfs-s3:8333",
    Region:    "us-east-1",
    AccessKey: os.Getenv("BLOBSTORE_ACCESS_KEY"),
    SecretKey: os.Getenv("BLOBSTORE_SECRET_KEY"),
    // SeaweedFS reached over an in-cluster mesh (mTLS at the mesh
    // layer) opts in to plaintext. The default rejects http:// with
    // blobstore.ErrInsecureEndpoint so a misconfiguration cannot
    // ship credentials in the clear (W-002, REQ-005, PX-0003).
    AllowInsecureEndpoint: true,
    UsePathStyle:          true, // required for SeaweedFS
}, logger)
if err != nil { /* ... */ }

HTTPS enforcement

Config.Validate refuses http:// endpoints by default. Callers that legitimately target a plaintext endpoint — typically SeaweedFS over an in-cluster service mesh that already provides mTLS — must opt in by setting AllowInsecureEndpoint: true. The opt-in keeps the guardrail local to each wiring site rather than burying the decision inside the package, so a code review catches a public http://s3.amazonaws.com regression at the call site.

go
// Rejected with blobstore.ErrInsecureEndpoint:
_, err := blobstore.NewClient(ctx, blobstore.Config{
    Endpoint:  "http://s3.example.com",   // plaintext over the public internet
    Region:    "us-east-1",
    AccessKey: "AKID",
    SecretKey: "s3cret",
}, nil)
// errors.Is(err, blobstore.ErrInsecureEndpoint) == true

Put / Get / Head / Delete

The four object-level operations are the sanctioned surface bounded contexts consume. Each returns an ObjectInfo value shaped intentionally narrower than the underlying SDK types so callers cannot depend on vendor-specific fields.

MethodSignatureContract
PutPut(ctx, bucket, key string, body io.Reader, contentType string) (ObjectInfo, error)Uploads body. Empty contentType lets the backend default (usually application/octet-stream).
GetGet(ctx, bucket, key string) (io.ReadCloser, ObjectInfo, error)Downloads the object. Missing key → wrapped ErrNotFound; compare via errors.Is. Caller MUST close the returned ReadCloser.
HeadHead(ctx, bucket, key string) (ObjectInfo, error)Metadata only, no body transfer. Missing key → ErrNotFound.
DeleteDelete(ctx, bucket, key string) errorIdempotent — deleting a non-existent object is not an error (S3 returns 204).

ObjectInfo

FieldTypeSource
ETagstringBackend entity tag.
VersionIDstringObject version when versioning is enabled.
Sizeint64Object size in bytes (zero when unknown).
ContentTypestringBackend-assigned MIME type.

ErrNotFound sentinel

go
_, info, err := client.Get(ctx, bucket, key)
if errors.Is(err, blobstore.ErrNotFound) {
    // translate to HTTP 404
}

The ErrNotFound sentinel is matched from any of the three SDK conditions: *s3types.NotFound, *s3types.NoSuchKey, *s3types.NoSuchBucket — so callers can write a single errors.Is check regardless of which variant the backend returns.

Presign

(*Client).Presign(ctx, bucket, key, expiry) (string, error) returns a pre-signed GET URL.

Expiry constraints

ConstraintBehaviour
expiry <= 0Rejected with error tagged (REQ-005, PX-0003).
expiry >= MaxPresignExpiryRejected with error tagged (REQ-005, PX-0003).
0 < expiry < MaxPresignExpiryAccepted; URL expires at now + expiry.

MaxPresignExpiry = 7 * 24 * time.Hour is the hard ceiling tracking the AWS Signature V4 7-day limit. AWS S3 sigv4 requires X-Amz-Expires to be strictly less than 604800 seconds, so the check uses a strict inequality (>=); exactly 7d is rejected at mint time instead of being accepted locally and rejected by the server at request time. The ceiling is checked before any network round-trip so operators cannot accidentally mint eternally-valid URLs.

go
url, err := client.Presign(ctx, "diagnostics", "node-42/bundle.tar.gz", 15*time.Minute)

SeaweedFS ↔ S3 compatibility

Both backends are exercised under the same S3-API code path — the only adjustments operators make at deploy time are:

ConcernSeaweedFS (self-hosted)AWS S3 (SaaS)
EndpointPoints at the SeaweedFS S3 gateway (e.g. http://seaweedfs-s3:8333).Leave empty — AWS SDK derives from Region.
UsePathStyleMUST be true. SeaweedFS does not support virtual-host style.false by default; true also accepted.
RegionAny string the SeaweedFS gateway accepts (commonly us-east-1).Real AWS region.
VersioningSupported; VersionID surfaces through ObjectInfo.Supported; VersionID surfaces through ObjectInfo.
PresignSignature V4 with 7-day ceiling.Signature V4 with 7-day ceiling.
License rationaleSeaweedFS is Apache-2.0 — chosen over MinIO's AGPL shift.AWS-managed; no operational footprint for SaaS.

See README § Storage Topology — Object store for the full rationale.

Readiness probe

blobstore.ProbeFunc(client, probeBucket) returns a health.ProbeFunc that exercises HeadBucket against the supplied probe bucket.

SymbolValue
blobstore.ProbeName"blobstore"
Returned error on any failureerrProbeNotReady"blobstore: not ready (REQ-006, PX-0003)"
Failure classesnil client, empty probeBucket, underlying HeadBucket error
go
registry.Register(blobstore.ProbeName, blobstore.ProbeFunc(client, "plexsphere-diagnostics"))

Error strings never expose access keys, secret keys, or endpoint URLs; /readyz bodies are served to unauthenticated probes. Backend detail is emitted into structured logs at probe-call time for operator diagnostics, not into the probe return value itself.

Cross-references