185 lines
5.7 KiB
Go
185 lines
5.7 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/metazla/meta-core/internal/cid"
|
|
"github.com/metazla/meta-core/internal/kv"
|
|
)
|
|
|
|
// The cid → record resolution layer, read side. A record (rid) is a group of
|
|
// cids known to be the same content; the group lives in truth's cid_group and
|
|
// is mirrored into the cache as
|
|
//
|
|
// cid:<bareCid> → <rid>
|
|
// file:<rid>/cids/<bareCid> = "true" (synthesised membership, one per cid)
|
|
//
|
|
// by the apply loop (apply.go). A CID is self-describing (its algorithm is in
|
|
// the CIDv1 multicodec, see internal/cid); the canonical externally-visible
|
|
// CID is derived by rank on read (GetMetadataDocument), never stored.
|
|
|
|
// CIDIndexPrefix is the key prefix for the reverse index. Combined
|
|
// with a bare CID, it forms the full key: "cid:bafk…".
|
|
const CIDIndexPrefix = "cid:"
|
|
|
|
// CIDsKeyPrefix is the field-name prefix for the bare-CID key-set. Sibling
|
|
// CIDs live as flat keys file:<uuid>/cids/<cid> = "true"; the set IS the
|
|
// keyset, the value is the sentinel. Read by GetMetadataDocument (hoisted
|
|
// into the typed CIDs array) and by DeleteRoot/DeleteMetadata (to find the
|
|
// reverse-index entries that need cleanup).
|
|
const CIDsKeyPrefix = "cids/"
|
|
|
|
// ResolveRoot maps a hash supplied by a caller (a meta-sort write, an editor
|
|
// lookup, an SSE consumer) to the actual storage root key.
|
|
//
|
|
// Callers send a bare CID (e.g. the midhash "bafk…" the watcher publishes on
|
|
// file:events, or any sibling CID). Roots are kept opaque and every CID is
|
|
// aliased into the reverse index:
|
|
//
|
|
// cid:bafk… → 01KSBHM*
|
|
//
|
|
// Without resolution, every /meta/<cid> write creates a parallel cid-rooted
|
|
// entry that the watcher's UUID never sees.
|
|
//
|
|
// Behaviour:
|
|
// - empty hash → empty (caller's problem)
|
|
// - hash matches a reverse-index entry → return UUID
|
|
// - otherwise return hash unchanged (legacy / explicit-UUID writes)
|
|
//
|
|
// Errors are converted to "no resolution" — if the store is down or the
|
|
// lookup fails we let the caller proceed with the bare hash and surface the
|
|
// failure on the actual operation.
|
|
func (c *Client) ResolveRoot(hash string) string {
|
|
if hash == "" {
|
|
return hash
|
|
}
|
|
uuid, err := c.GetByCID(hash)
|
|
if err != nil || uuid == "" {
|
|
return hash
|
|
}
|
|
return uuid
|
|
}
|
|
|
|
// GetByCID resolves a bare CID to the UUID of its root. Returns ("", nil)
|
|
// when the CID is unknown; the empty string is the "not found" signal rather
|
|
// than a typed error so callers can branch without errors.As.
|
|
func (c *Client) GetByCID(cidStr string) (string, error) {
|
|
s, err := c.kv()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if cidStr == "" {
|
|
return "", nil
|
|
}
|
|
ctx, cancel := opCtx()
|
|
defer cancel()
|
|
uuid, _, err := s.Get(ctx, CIDIndexPrefix+cidStr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("get cid index: %w", err)
|
|
}
|
|
return uuid, nil
|
|
}
|
|
|
|
// cidsForRoot returns a root's bare-CID key-set members (cids/ prefix
|
|
// stripped).
|
|
func cidsForRoot(ctx context.Context, r kv.Reader, uuid string) ([]string, error) {
|
|
prefix := recordPrefix(uuid) + CIDsKeyPrefix
|
|
rows, err := r.Range(ctx, prefix, "", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]string, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, row.K[len(prefix):])
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// cidFromKeysetField returns the bare CID encoded in a cids/<cid> field name,
|
|
// or "" if the field isn't a key-set member.
|
|
func cidFromKeysetField(field string) string {
|
|
if !strings.HasPrefix(field, CIDsKeyPrefix) {
|
|
return ""
|
|
}
|
|
return strings.TrimPrefix(field, CIDsKeyPrefix)
|
|
}
|
|
|
|
// MetadataDocument is the complete state of one file as the public API
|
|
// returns it: every flat string field (the cids/<cid> members included),
|
|
// plus the CIDs hoisted into a typed array, the canonical CID derived by
|
|
// rank, and the duplicates set. Distinct from GetMetadataFlat (which only
|
|
// returns the raw string keys).
|
|
type MetadataDocument struct {
|
|
Flat map[string]string `json:"metadata"`
|
|
CIDs []string `json:"cids"`
|
|
Canonical string `json:"canonical_cid,omitempty"`
|
|
Duplicates []string `json:"duplicates"`
|
|
}
|
|
|
|
// GetMetadataDocument returns the full document for a root: every string
|
|
// property under file:<uuid>/* (cids/<cid> members included in Flat), the
|
|
// CIDs hoisted into a typed array, the canonical CID computed by rank, and
|
|
// the duplicates SET. Returns nil if the root has no keys at all (deleted or
|
|
// never existed).
|
|
func (c *Client) GetMetadataDocument(uuid string) (*MetadataDocument, error) {
|
|
s, err := c.kv()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if uuid == "" {
|
|
return nil, nil
|
|
}
|
|
ctx, cancel := opCtx()
|
|
defer cancel()
|
|
|
|
flat, err := readRecord(ctx, s, uuid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if flat == nil {
|
|
flat = make(map[string]string)
|
|
}
|
|
|
|
// Hoist the cids/<cid> key-set members into a typed array and derive the
|
|
// canonical CID by rank. The members remain in Flat too — that's how
|
|
// flat-map consumers (meta-share ingest) see them.
|
|
cids := make([]string, 0)
|
|
canonical := ""
|
|
for k := range flat {
|
|
if !strings.HasPrefix(k, CIDsKeyPrefix) {
|
|
continue
|
|
}
|
|
cidStr := strings.TrimPrefix(k, CIDsKeyPrefix)
|
|
cids = append(cids, cidStr)
|
|
if canonical == "" {
|
|
canonical = cidStr
|
|
} else {
|
|
canonical = cid.Better(canonical, cidStr)
|
|
}
|
|
}
|
|
|
|
// Duplicates: every other local path holding the same content.
|
|
paths, err := localPaths(ctx, s.DB(), cids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read duplicates: %w", err)
|
|
}
|
|
dups := make([]string, 0, len(paths))
|
|
for _, p := range paths {
|
|
if p != flat["filePath"] {
|
|
dups = append(dups, p)
|
|
}
|
|
}
|
|
|
|
if len(flat) == 0 && len(cids) == 0 && len(dups) == 0 {
|
|
return nil, nil
|
|
}
|
|
return &MetadataDocument{
|
|
Flat: flat,
|
|
CIDs: cids,
|
|
Canonical: canonical,
|
|
Duplicates: dups,
|
|
}, nil
|
|
}
|