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""S3 access-key ID. Set together with SecretKey for the static-credential path (SeaweedFS). Leave both empty to use the AWS SDK default credential chain. Analogous to a username; allowed in error messages.
SecretKeystring""S3 secret access key. Set together with AccessKey (see above). 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 the resolved credential source (see credential resolution), 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

Credential resolution

The key pair is all-or-nothing:

  • Static credentials — set both AccessKey and SecretKey. This is the only path SeaweedFS supports, and the default for any deployment that provisions a long-lived S3 key. Validate requires both fields.
  • SDK default credential chain — leave both empty. NewClient then omits the static provider and lets the AWS SDK resolve credentials from its default chain: environment variables, the shared config/credentials files, IRSA / EKS Pod Identity, and the EC2/ECS instance role. This is the production path on a workload-identity cluster where no long-lived S3 key exists. Construction logs an INFO breadcrumb noting the default chain is in use.

Setting exactly one of the two is rejected by Validate as a misconfiguration, so a typo (for example an unset PLEXSPHERE_S3_SECRET_KEY environment variable) fails fast at startup with a clear config error rather than falling silently to the default chain and surfacing an opaque AccessDenied at the first request. Operators select the default chain by leaving PLEXSPHERE_S3_ACCESS_KEY and PLEXSPHERE_S3_SECRET_KEY unset.

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