package storage
import (
"context"
"fmt"
"sync"
"github.com/dolthub/dolt/go/libraries/doltcore/remotesrv"
"github.com/dolthub/dolt/go/store/nbs"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)
// defaultMemTableSize mirrors upstream utils/remotesrv LocalCSCache: the
// in-memory table size handed to nbs.NewLocalStore before chunks spill to disk.
const defaultMemTableSize = 128 * 1024 * 1024
// RepoLookup resolves a repository's owner and database name to its absolute
// on-disk store directory. It returns an error when no repository row exists;
// Cache.Get propagates that error unchanged so the remotesapi interceptor layer
// can map it to a gRPC NotFound (v1 has no push-to-create: databases are made
// explicitly through the web UI).
//
// It is defined here as a function type so storage/ never imports db/; the main
// wiring injects a db-backed lookup.
type RepoLookup func(ctx context.Context, owner, name string) (diskPath string, err error)
// Cache is a remotesrv.DBCache that serves only repositories with an existing
// row, memoizing one nbs.NewLocalStore per absolute disk path. It never creates
// directories: unknown repos are errors, not implicit creations. It is safe for
// concurrent use.
type Cache struct {
lookup RepoLookup
mu sync.Mutex
dbs map[string]remotesrv.RemoteSrvStore
}
var _ remotesrv.DBCache = (*Cache)(nil)
// NewCache builds a Cache backed by lookup. lookup must be non-nil.
func NewCache(lookup RepoLookup) *Cache {
if lookup == nil {
panic("storage: NewCache requires a non-nil RepoLookup")
}
return &Cache{
lookup: lookup,
dbs: make(map[string]remotesrv.RemoteSrvStore),
}
}
// Get resolves the remotesapi repo path to an on-disk store and returns a
// memoized NBS chunk store for it. The path is normalized with
// core.ParseRepoPath (trim slashes, optional "~", reject traversal), then
// resolved to a disk path via the injected RepoLookup. A missing repository row
// surfaces as the lookup's error. Get never creates directories or stores on
// disk; nbs.NewLocalStore opens the existing bare store created by InitStore.
func (c *Cache) Get(ctx context.Context, path, nbfVerStr string) (remotesrv.RemoteSrvStore, error) {
owner, name, err := core.ParseRepoPath(path)
if err != nil {
return nil, fmt.Errorf("storage: parse repo path %q: %w", path, err)
}
diskPath, err := c.lookup(ctx, owner, name)
if err != nil {
return nil, err
}
c.mu.Lock()
defer c.mu.Unlock()
if cs, ok := c.dbs[diskPath]; ok {
return cs, nil
}
cs, err := nbs.NewLocalStore(ctx, nbfVerStr, diskPath, defaultMemTableSize, nbs.NewUnlimitedMemQuotaProvider(), false)
if err != nil {
return nil, fmt.Errorf("storage: open store %q: %w", diskPath, err)
}
c.dbs[diskPath] = cs
return cs, nil
}
// Evict closes and drops the memoized store for diskPath, if any. It is called
// when a repository is deleted so a subsequent recreation at the same path does
// not reuse a stale handle. Evicting an absent path is a no-op. It returns the
// error from closing the store, if one was open.
func (c *Cache) Evict(diskPath string) error {
c.mu.Lock()
defer c.mu.Unlock()
cs, ok := c.dbs[diskPath]
if !ok {
return nil
}
delete(c.dbs, diskPath)
if err := cs.Close(); err != nil {
return fmt.Errorf("storage: close evicted store %q: %w", diskPath, err)
}
return nil
}
// Close closes every memoized store and empties the cache. It is called at
// server shutdown. It closes all stores before returning and reports the first
// error encountered, if any.
func (c *Cache) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
var firstErr error
for path, cs := range c.dbs {
if err := cs.Close(); err != nil && firstErr == nil {
firstErr = fmt.Errorf("storage: close store %q: %w", path, err)
}
delete(c.dbs, path)
}
return firstErr
}