package nfs import ( "crypto/sha256" "encoding/binary" "io/fs" "reflect" "slices" "github.com/willscott/go-nfs" "github.com/go-git/go-billy/v5" "github.com/google/uuid" lru "github.com/hashicorp/golang-lru/v2" ) // NewCachingHandler wraps a handler to provide a basic to/from-file handle cache. func NewCachingHandler(h nfs.Handler, limit int) nfs.Handler { return NewCachingHandlerWithVerifierLimit(h, limit, limit) } // NewCachingHandlerWithVerifierLimit provides a basic to/from-file handle cache that can be tuned with a smaller cache of active directory listings. func NewCachingHandlerWithVerifierLimit(h nfs.Handler, limit int, verifierLimit int) nfs.Handler { if limit < 2 || verifierLimit < 2 { nfs.Log.Warnf("Caching handler created with insufficient cache to support directory listing", "size", limit, "verifiers", verifierLimit) } cache, _ := lru.New[uuid.UUID, entry](limit) reverseCache := make(map[string][]uuid.UUID) verifiers, _ := lru.New[uint64, verifier](verifierLimit) return &CachingHandler{ Handler: h, activeHandles: cache, reverseHandles: reverseCache, activeVerifiers: verifiers, cacheLimit: limit, } } // CachingHandler implements to/from handle via an LRU cache. type CachingHandler struct { nfs.Handler activeHandles *lru.Cache[uuid.UUID, entry] reverseHandles map[string][]uuid.UUID activeVerifiers *lru.Cache[uint64, verifier] cacheLimit int } type entry struct { f billy.Filesystem p []string } // ToHandle takes a file and represents it with an opaque handle to reference it. // In stateless nfs (when it's serving a unix fs) this can be the device + inode // but we can generalize with a stateful local cache of handed out IDs. func (c *CachingHandler) ToHandle(f billy.Filesystem, path []string) []byte { joinedPath := f.Join(path...) if handle := c.searchReverseCache(f, joinedPath); handle != nil { return handle } id := uuid.New() newPath := make([]string, len(path)) copy(newPath, path) evictedKey, evictedPath, ok := c.activeHandles.GetOldest() if evicted := c.activeHandles.Add(id, entry{f, newPath}); evicted && ok { rk := evictedPath.f.Join(evictedPath.p...) c.evictReverseCache(rk, evictedKey) } if _, ok := c.reverseHandles[joinedPath]; !ok { c.reverseHandles[joinedPath] = []uuid.UUID{} } c.reverseHandles[joinedPath] = append(c.reverseHandles[joinedPath], id) b, _ := id.MarshalBinary() return b } // FromHandle converts from an opaque handle to the file it represents func (c *CachingHandler) FromHandle(fh []byte) (billy.Filesystem, []string, error) { id, err := uuid.FromBytes(fh) if err != nil { return nil, []string{}, err } if f, ok := c.activeHandles.Get(id); ok { for _, k := range c.activeHandles.Keys() { candidate, _ := c.activeHandles.Peek(k) if hasPrefix(f.p, candidate.p) { _, _ = c.activeHandles.Get(k) } } return f.f, slices.Clone(f.p), nil } return nil, []string{}, &nfs.NFSStatusError{NFSStatus: nfs.NFSStatusStale} } func (c *CachingHandler) searchReverseCache(f billy.Filesystem, path string) []byte { uuids, exists := c.reverseHandles[path] if !exists { return nil } for _, id := range uuids { if candidate, ok := c.activeHandles.Get(id); ok { if reflect.DeepEqual(candidate.f, f) { return id[:] } } } return nil } func (c *CachingHandler) evictReverseCache(path string, handle uuid.UUID) { uuids, exists := c.reverseHandles[path] if !exists { return } for i, u := range uuids { if u == handle { uuids = append(uuids[:i], uuids[i+1:]...) c.reverseHandles[path] = uuids return } } } func (c *CachingHandler) InvalidateHandle(fs billy.Filesystem, handle []byte) error { //Remove from cache id, _ := uuid.FromBytes(handle) entry, ok := c.activeHandles.Get(id) if ok { rk := entry.f.Join(entry.p...) c.evictReverseCache(rk, id) } c.activeHandles.Remove(id) return nil } // HandleLimit exports how many file handles can be safely stored by this cache. func (c *CachingHandler) HandleLimit() int { return c.cacheLimit } func hasPrefix(path, prefix []string) bool { if len(prefix) > len(path) { return false } for i, e := range prefix { if path[i] != e { return false } } return true } type verifier struct { path string contents []fs.FileInfo } func hashPathAndContents(path string, contents []fs.FileInfo) uint64 { //calculate a cookie-verifier. vHash := sha256.New() // Add the path to avoid collisions of directories with the same content vHash.Write(binary.BigEndian.AppendUint64([]byte{}, uint64(len(path)))) vHash.Write([]byte(path)) for _, c := range contents { vHash.Write([]byte(c.Name())) // Never fails according to the docs } verify := vHash.Sum(nil)[0:8] return binary.BigEndian.Uint64(verify) } func (c *CachingHandler) VerifierFor(path string, contents []fs.FileInfo) uint64 { id := hashPathAndContents(path, contents) c.activeVerifiers.Add(id, verifier{path, contents}) return id } func (c *CachingHandler) DataForVerifier(path string, id uint64) []fs.FileInfo { if cache, ok := c.activeVerifiers.Get(id); ok { return cache.contents } return nil }