5eef13b615
方案A:所有对象写入经 loopback 网关,持久计划/grant/intent/逐版本 receipt 与 attempt→grant delivery 落在治理 PG;scope 由 provider+endpoint+bucket 决定, 同一物理桶只允许一个写域。版本 append 不等于 CAS,未锚定计划在首笔业务写前拒绝。 本提交只含网关与其适配层接线、协议文档;生产独占、凭据切换与桶设置未改动。
110 lines
3.8 KiB
Go
110 lines
3.8 KiB
Go
// Package objectgateway implements non-preemptive object write ownership.
|
|
package objectgateway
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
const Protocol = "kg-object-gateway-v1"
|
|
|
|
var (
|
|
ErrHeld = errors.New("gateway_held_unresolved_intent")
|
|
ErrEpoch = errors.New("gateway_epoch_or_plan_mismatch")
|
|
ErrConflict = errors.New("gateway_identity_conflict")
|
|
ErrPersistence = errors.New("gateway_persistence_unavailable")
|
|
ErrAuthority = errors.New("gateway_control_authority_unavailable")
|
|
ErrAuthorityPrivileges = errors.New("gateway_control_reader_privileges_invalid")
|
|
ErrIsolation = errors.New("gateway_exclusive_writer_evidence_unverified")
|
|
ErrRejected = errors.New("gateway_create_rejected_existing_object")
|
|
)
|
|
|
|
type Grant struct {
|
|
Epoch int64 `json:"epoch"`
|
|
Owner string `json:"owner"`
|
|
Plan string `json:"plan"`
|
|
}
|
|
type Version struct {
|
|
ID string `json:"version_id"`
|
|
Owner string `json:"owner"`
|
|
SHA256 string `json:"sha256"`
|
|
Bytes int64 `json:"bytes"`
|
|
ETag string `json:"etag"`
|
|
}
|
|
type Request struct {
|
|
Grant Grant `json:"grant"`
|
|
Operation string `json:"operation"`
|
|
Key string `json:"key"`
|
|
Action string `json:"action"`
|
|
SHA256 string `json:"sha256"`
|
|
Bytes int64 `json:"bytes"`
|
|
CreatedBy string `json:"created_by,omitempty"`
|
|
Version Version `json:"version,omitempty"`
|
|
}
|
|
type Receipt struct {
|
|
Request Request `json:"request"`
|
|
State string `json:"state"`
|
|
Version Version `json:"version"`
|
|
}
|
|
|
|
// Backend never retries mutations. List is complete and scoped to exactly key.
|
|
// Not finding a PUT is never evidence that its request cannot still arrive.
|
|
type Backend interface {
|
|
Ready(context.Context) error
|
|
Observe(context.Context, string, string) (Version, bool, error)
|
|
List(context.Context, string) ([]Version, error)
|
|
Put(context.Context, string, []byte, string) (Version, error)
|
|
Delete(context.Context, string, Version) error
|
|
}
|
|
type IsolationVerifier interface{ Verify(context.Context) error }
|
|
|
|
func Digest(b []byte) string { s := sha256.Sum256(b); return hex.EncodeToString(s[:]) }
|
|
func validHash(s string) bool {
|
|
b, e := hex.DecodeString(s)
|
|
return e == nil && len(b) == 32 && strings.ToLower(s) == s
|
|
}
|
|
func (g Grant) valid() bool { return g.Epoch > 0 && validHash(g.Owner) && validHash(g.Plan) }
|
|
func (r Request) validate() error {
|
|
if !r.Grant.valid() || !validHash(r.Operation) || r.Key == "" || len(r.Key) > 2048 || strings.ContainsAny(r.Key, "\x00\r\n") || strings.HasPrefix(r.Key, "/") || !validHash(r.SHA256) || r.Bytes < 0 {
|
|
return ErrConflict
|
|
}
|
|
if r.Action != "create" && r.Action != "delete" {
|
|
return ErrConflict
|
|
}
|
|
if r.Action == "create" && (r.CreatedBy != "" || r.Version != (Version{})) {
|
|
return ErrConflict
|
|
}
|
|
if r.Action == "delete" && (!validHash(r.CreatedBy) || !validVersion(r.Version) || r.Version.SHA256 != r.SHA256 || r.Version.Bytes != r.Bytes) {
|
|
return ErrConflict
|
|
}
|
|
return nil
|
|
}
|
|
func validVersion(v Version) bool {
|
|
return v.ID != "" && v.ID != "null" && validHash(v.Owner) && validHash(v.SHA256) && v.Bytes >= 0 && v.ETag != ""
|
|
}
|
|
|
|
// Claim binds backend ownership to the complete immutable gateway request.
|
|
func (r Request) Claim() string { return r.token() }
|
|
func (r Request) token() string { raw, _ := json.Marshal(r); return Digest(raw) }
|
|
func (r Receipt) valid() bool {
|
|
if r.Request.validate() != nil {
|
|
return false
|
|
}
|
|
switch r.State {
|
|
case "committed":
|
|
return r.Request.Action == "create" && r.Request.matches(r.Version)
|
|
case "rejected":
|
|
return r.Request.Action == "create" && r.Version == (Version{})
|
|
case "removed":
|
|
return r.Request.Action == "delete" && r.Version == r.Request.Version
|
|
}
|
|
return false
|
|
}
|
|
func (r Request) matches(v Version) bool {
|
|
return validVersion(v) && v.Owner == r.token() && v.Bytes == r.Bytes && v.SHA256 == r.SHA256
|
|
}
|