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 | — (REQUIRED) | S3 access-key ID. Analogous to a username; allowed in error messages. |
SecretKey | string | — (REQUIRED) | S3 secret access key. 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 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) == truePut / 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.