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: → // file:/cids/ = "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:/cids/ = "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/ 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/ 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/ 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:/* (cids/ 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/ 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 }