package storage import ( "context" "fmt" "sync" "github.com/dolthub/dolt/go/libraries/doltcore/remotesrv" "github.com/dolthub/dolt/go/store/nbs" "go.bigb.es/sourcehut-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 }