// Store — the deployment bucket behind the release plane: S3 in production, // a plain directory in tests. CAS discipline (If-Match / If-None-Match) // ported from Eleven internal/idalloc/idalloc.go (the litestream leaser's // conditional-write pattern); not-found folding from Eleven s3blobstore.go. package release import ( "bytes" "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "sort" "strings" "sync" "time" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" awss3 "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/smithy-go" "github.com/carlosframework/platform/internal/rootaws" "github.com/carlosframework/platform/internal/safefile" "github.com/carlosframework/platform/internal/s3compat" ) // ErrCASConflict is a conditional Put that lost: the object already existed // (ifMatch "") or moved on since the etag was read (ifMatch stale). Callers // re-read and retry; they never overwrite blind. var ErrCASConflict = errors.New("store: compare-and-swap conflict") // Store is the deployment bucket. Get's not-found is os.ErrNotExist-wrapped // so callers use errors.Is; Put with ifMatch "" is create-only // (If-None-Match: *), non-empty is replace-only (If-Match: ), and a // lost race is ErrCASConflict either way. Delete is idempotent: deleting a // missing key is not an error. type Store interface { Get(ctx context.Context, key string) ([]byte, string, error) // body, etag Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) // new etag List(ctx context.Context, prefix string) ([]string, error) // ListDirs returns the immediate child "directories" of prefix — the // delimiter-aware listing, so a caller that only wants the names one // level down does not pay for every object beneath them. Each result is // a FULL prefix ending in "/" (S3 CommonPrefixes semantics), never a // bare name. A prefix that does not end in "/" is treated as though it // did, so the two implementations answer identically. Objects at the // level itself (files, not directories) are never included. ListDirs(ctx context.Context, prefix string) ([]string, error) Delete(ctx context.Context, key string) error } // ---- prefixStore ------------------------------------------------------------- // prefixStore rebases every key a caller works with onto a fixed base before // it ever reaches the underlying store — the actual enforcement point for a // namespacing env var like CARLOS_STATUS_PREFIX (customer-fleets plan, Task // 5: a fleet box's own heartbeat, written through one of these with base // "fleets//", lands at "fleets//status/boxes/