Appearance
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:
- Client Config
- Put / Get / Head / Delete
- Presign contract and 7-day ceiling
- SeaweedFS ↔ S3 compatibility notes
- Readiness probe
See also:
Client Config
| Field | Type | Default | Purpose |
|---|---|---|---|
Endpoint | string | "" | S3-compatible API URL. Empty uses AWS's region-derived default. Set to the SeaweedFS gateway for self-hosted. |
Region | string | — (REQUIRED) | AWS region or region-alias. Missing → error tagged (REQ-007, PX-0003). |
AccessKey | string | "" | 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. |
SecretKey | string | "" | S3 secret access key. Set together with AccessKey (see above). Sensitive — never echoed in errors, logs, or metrics. |
UsePathStyle | bool | false | Path-style (host/bucket) vs. virtual-host (bucket.host). SeaweedFS requires true. AWS S3 accepts both. |
AllowInsecureEndpoint | bool | false | Opt-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) == trueCredential resolution
The key pair is all-or-nothing:
- Static credentials — set both
AccessKeyandSecretKey. This is the only path SeaweedFS supports, and the default for any deployment that provisions a long-lived S3 key.Validaterequires both fields. - SDK default credential chain — leave both empty.
NewClientthen 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 anINFObreadcrumb 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.
| Method | Signature | Contract |
|---|---|---|
Put | Put(ctx, bucket, key string, body io.Reader, contentType string) (ObjectInfo, error) | Uploads body. Empty contentType lets the backend default (usually application/octet-stream). |
Get | Get(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. |
Head | Head(ctx, bucket, key string) (ObjectInfo, error) | Metadata only, no body transfer. Missing key → ErrNotFound. |
Delete | Delete(ctx, bucket, key string) error | Idempotent — deleting a non-existent object is not an error (S3 returns 204). |
ObjectInfo
| Field | Type | Source |
|---|---|---|
ETag | string | Backend entity tag. |
VersionID | string | Object version when versioning is enabled. |
Size | int64 | Object size in bytes (zero when unknown). |
ContentType | string | Backend-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
| Constraint | Behaviour |
|---|---|
expiry <= 0 | Rejected with error tagged (REQ-005, PX-0003). |
expiry >= MaxPresignExpiry | Rejected with error tagged (REQ-005, PX-0003). |
0 < expiry < MaxPresignExpiry | Accepted; 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:
| Concern | SeaweedFS (self-hosted) | AWS S3 (SaaS) |
|---|---|---|
Endpoint | Points at the SeaweedFS S3 gateway (e.g. http://seaweedfs-s3:8333). | Leave empty — AWS SDK derives from Region. |
UsePathStyle | MUST be true. SeaweedFS does not support virtual-host style. | false by default; true also accepted. |
Region | Any string the SeaweedFS gateway accepts (commonly us-east-1). | Real AWS region. |
| Versioning | Supported; VersionID surfaces through ObjectInfo. | Supported; VersionID surfaces through ObjectInfo. |
| Presign | Signature V4 with 7-day ceiling. | Signature V4 with 7-day ceiling. |
| License rationale | SeaweedFS 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.
| Symbol | Value |
|---|---|
blobstore.ProbeName | "blobstore" |
| Returned error on any failure | errProbeNotReady — "blobstore: not ready (REQ-006, PX-0003)" |
| Failure classes | nil 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
../../contributing/layout.md— the bounded-context map locating this package in the codebase.../../../internal/platform/blobstore/— the package source.../index.md— the Reference quadrant index.