Documentation
¶
Overview ¶
Package storage holds the outbound adapters behind core/port/storage, and the upload guard in front of them: the component the upload matrix tests (SG-12, security.md T-11/T-17).
The guard is here rather than in the domain because sniffing is: the signature table lives in net/http, which the domain may not import. The *policy* over the sniffed answer - what may render inline, which claims are lies - is the domain's (core/domain/model/media), so the split is mechanics here, judgement there.
Index ¶
- func Inspect(content io.Reader, claimedType string, limit int64) (port.Inspection, error)
- func MediaOrigin(cfg env.StorageConfig) (string, error)
- type LocalStorage
- type LocalTransfers
- type Probe
- type ResilientStore
- type S3Storage
- func (s *S3Storage) CreateBucket(ctx context.Context) error
- func (s *S3Storage) Delete(ctx context.Context, key string) error
- func (s *S3Storage) Get(ctx context.Context, key string) (port.Object, error)
- func (s *S3Storage) IssueDownload(object media.Object, expiresAt time.Time) (port.Transfer, error)
- func (s *S3Storage) IssueUpload(object media.Object, expiresAt time.Time) (port.Transfer, error)
- func (s *S3Storage) Put(ctx context.Context, upload port.Upload) error
- type UploadGuard
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Inspect ¶
Inspect judges one upload before a byte of it reaches a store.
It reads at most 512 bytes to sniff, asks the domain to reconcile the claim, and hands back a stream that enforces the size limit while it is consumed. The limit refusal happens at the boundary byte: an upload larger than the limit costs the bytes already streamed and nothing held in memory - never an allocation of the object (T-17, observability-reliability.md §6).
func MediaOrigin ¶
func MediaOrigin(cfg env.StorageConfig) (string, error)
MediaOrigin is the origin a browser reaches for this installation's media, for the interface's content security policy (ADR-0047): scheme, host and port, with any path, query or trailing slash removed, because a policy source with a path is one most of a browser ignores. Empty under local, whose media URLs are this server's own.
Resolved from the same configuration and the same way as the adapter's endpoint, and shaped the way objectURL shapes a presigned URL, so that the origin the policy names is the origin the URLs carry: the endpoint's own under path-style addressing, and the bucket's subdomain of it under virtual-hosted addressing - which is a different origin, and the one the browser is asked to reach. Still exactly one: an installation has one bucket.
Types ¶
type LocalStorage ¶
type LocalStorage struct {
// contains filtered or unexported fields
}
LocalStorage keeps objects in a directory: the self-hosting default (ADR-0003 - a Raspberry Pi has a disk, not a bucket).
The content type travels in a sidecar file beside the object, because a filesystem has no metadata slot for it and guessing it back at read time would undo the sniff the upload guard did (T-11). The sidecar is written after the content and read before it, so a crash between the two leaves an object without a type - which reads as an absent object rather than one with an invented type.
func NewLocalStorage ¶
func NewLocalStorage(root string) LocalStorage
NewLocalStorage takes the configured directory (HUBTASK_STORAGE_LOCAL_PATH).
func (LocalStorage) Delete ¶
func (s LocalStorage) Delete(_ context.Context, key string) error
Delete removes the object and its sidecar. Removing what is not there succeeds - deletion is the state the caller asked for.
type LocalTransfers ¶
type LocalTransfers struct {
// contains filtered or unexported fields
}
LocalTransfers issues the content-route URLs of a local-storage installation: the same three-step flow as a presigned bucket, with this server's /media/{id}:content standing in for the bucket and an HMAC token standing in for the signature.
func NewLocalTransfers ¶
func NewLocalTransfers(tokens security.MediaTokenIssuer, baseURL string) LocalTransfers
func (LocalTransfers) IssueDownload ¶
func (LocalTransfers) IssueUpload ¶
type Probe ¶
type Probe struct {
// contains filtered or unexported fields
}
Probe reports the object storage's health to the registry, by reading the breaker rather than dialling the endpoint: a probe that called a target the breaker has cut off would undo the breaker's whole purpose - and would make /meta/health as slow as the outage it reports.
Optional by contract (core/port/health): the failure of the object storage restricts media and nothing else, and must never block the write path or readiness (QS-11).
func NewProbe ¶
func NewProbe(breaker *resilience.Breaker) Probe
NewProbe takes the breaker the resilient store trips.
type ResilientStore ¶
type ResilientStore struct {
// contains filtered or unexported fields
}
ResilientStore composes an ObjectStore with the resilience blocks (ADR-0016): a bulkhead so the storage cannot take more of the process than its pool, and a breaker so a dead endpoint costs an immediate answer instead of a blocked thread.
No timeout of its own, and that is deliberate rather than missing (rule 7 is kept elsewhere): every call arrives with the caller's deadline - the API path's request deadline, a worker's job deadline - and a 64 MiB object through a slow link has no single number this wrapper could choose without breaking somebody's legitimate upload. What can hang without progress is bounded in the adapter's transport: dial, TLS, and the wait for headers.
func NewResilientStore ¶
func NewResilientStore( inner port.ObjectStore, breaker *resilience.Breaker, bulkhead *resilience.Bulkhead, ) ResilientStore
NewResilientStore wraps the store. One breaker and one bulkhead per dependency, owned by the composition root - the health probe reads the same breaker.
func (ResilientStore) Delete ¶
func (r ResilientStore) Delete(ctx context.Context, key string) error
type S3Storage ¶
type S3Storage struct {
// contains filtered or unexported fields
}
S3Storage speaks the S3 API to AWS and to S3-compatible services (SeaweedFS, Garage).
It carries its own HTTP client rather than going through GuardedClient, and the exception is recorded where exceptions live (test/architecture/outbound_test.go): the endpoint is operator configuration - the same trust class as the database DSN, not a user-controlled target, so T-07's SSRF guard has nothing to guard - it streams objects of up to HUBTASK_MAX_UPLOAD_BYTES where the guarded port deliberately buffers small payloads, and a self-hosted object store lives on exactly the private network the guard exists to block. What rule 6 actually protects is kept: every call is bounded by a deadline, redirects are refused, and the resilient wrapper adds the breaker and the bulkhead (ADR-0016).
func NewS3Storage ¶
NewS3Storage builds the adapter from the validated configuration (with kind=s3 the bucket and both keys are mandatory at startup).
func (*S3Storage) CreateBucket ¶
CreateBucket makes the configured bucket exist, treating "it already does" as the success it is. For an operator's first run against a fresh bucket, and for the conformance suite; AWS itself is not the audience - a bucket there is infrastructure somebody provisions, and a name taken by another account answers 409 with a different meaning, which still reads as "not yours to create" and fails the first upload honestly.
No location constraint body: the S3-compatible services this serves ignore the region, and the empty body keeps the request signable with the empty payload hash.
func (*S3Storage) Delete ¶
Delete removes one object. S3 answers 204 for present and absent alike, which is exactly the idempotence the port asks for.
func (*S3Storage) IssueDownload ¶
func (s *S3Storage) IssueDownload( object media.Object, expiresAt time.Time, ) (port.Transfer, error)
IssueDownload mints a presigned GET whose disposition is signed into it: the bucket answers `Content-Disposition: attachment`, and a holder cannot strip that without invalidating the signature (T-11 - the bucket origin is not the application's, and the download stays a download there too).
func (*S3Storage) IssueUpload ¶
IssueUpload mints a presigned PUT: the client sends the bytes to the bucket directly, and the server never carries them (arc42 §8.4).
type UploadGuard ¶
type UploadGuard struct{}
UploadGuard is the port's Guard: the judgement, as something a use case can hold without importing this package (core/port/storage).
Empty on purpose. The guard has no state and no configuration - the limit travels with the call, because it is the installation's and the application layer is what knows it.
func NewUploadGuard ¶
func NewUploadGuard() UploadGuard
func (UploadGuard) Inspect ¶
func (UploadGuard) Inspect(content io.Reader, claimedType string, limit int64) (port.Inspection, error)