208 lines
5.8 KiB
Go
208 lines
5.8 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
// Directory delegations: the store of granted directory delegations, the
|
||
|
|
// GET_DIR_DELEGATION handler and the notification the server pushes to a
|
||
|
|
// deleg holder when the directory changes under it.
|
||
|
|
|
||
|
|
package nfs4server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"sync"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
|
||
|
|
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
|
||
|
|
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
|
||
|
|
)
|
||
|
|
|
||
|
|
// A dirDeleg is one granted directory delegation.
|
||
|
|
type dirDeleg struct {
|
||
|
|
stateid nfs4.Stateid
|
||
|
|
sessID nfs4.SessionID
|
||
|
|
clientid uint64
|
||
|
|
fh nfsfs.Handle
|
||
|
|
types nfs4.Bitmap
|
||
|
|
}
|
||
|
|
|
||
|
|
// dirDelegStore tracks the live directory delegations. A directory
|
||
|
|
// carries at most one.
|
||
|
|
type dirDelegStore struct {
|
||
|
|
mu sync.Mutex
|
||
|
|
next uint64
|
||
|
|
byDir map[string]*dirDeleg
|
||
|
|
}
|
||
|
|
|
||
|
|
func newDirDelegStore() *dirDelegStore {
|
||
|
|
return &dirDelegStore{byDir: make(map[string]*dirDeleg)}
|
||
|
|
}
|
||
|
|
|
||
|
|
// grant issues the directory delegation; a second client asking for the
|
||
|
|
// same directory is denied, the same client receives its live stateid.
|
||
|
|
func (s *dirDelegStore) grant(sessID nfs4.SessionID, clientid uint64, fh nfsfs.Handle) (*dirDeleg, uint32) {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
if d, ok := s.byDir[fileKey(fh)]; ok {
|
||
|
|
if d.clientid == clientid {
|
||
|
|
return d, nfs4.ErrOK
|
||
|
|
}
|
||
|
|
return nil, nfs4.ErrDenied
|
||
|
|
}
|
||
|
|
s.next++
|
||
|
|
var st nfs4.Stateid
|
||
|
|
setStateidSeq(&st, 1)
|
||
|
|
copy(st[4:], "DDEL")
|
||
|
|
for i := range 8 {
|
||
|
|
st[15-i] = byte(s.next >> (8 * i))
|
||
|
|
}
|
||
|
|
d := &dirDeleg{stateid: st, sessID: sessID, clientid: clientid, fh: fh}
|
||
|
|
s.byDir[fileKey(fh)] = d
|
||
|
|
return d, nfs4.ErrOK
|
||
|
|
}
|
||
|
|
|
||
|
|
// dropClient and dropSession remove the delegations that die with the
|
||
|
|
// client or the session.
|
||
|
|
func (s *dirDelegStore) dropClient(clientid uint64) {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
for key, d := range s.byDir {
|
||
|
|
if d.clientid == clientid {
|
||
|
|
delete(s.byDir, key)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *dirDelegStore) dropSession(sessID nfs4.SessionID) {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
for key, d := range s.byDir {
|
||
|
|
if d.sessID == sessID {
|
||
|
|
delete(s.byDir, key)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// count reports how many directory delegations are live.
|
||
|
|
func (s *dirDelegStore) count() int {
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
return len(s.byDir)
|
||
|
|
}
|
||
|
|
|
||
|
|
// dirDelegs returns the directory delegation store, made once per
|
||
|
|
// handler.
|
||
|
|
func (h *Handler) dirDelegs() *dirDelegStore {
|
||
|
|
h.mu.Lock()
|
||
|
|
defer h.mu.Unlock()
|
||
|
|
if h.dirDelegSt == nil {
|
||
|
|
h.dirDelegSt = newDirDelegStore()
|
||
|
|
}
|
||
|
|
return h.dirDelegSt
|
||
|
|
}
|
||
|
|
|
||
|
|
// getDirDelegationOp serves GET_DIR_DELEGATION over the current
|
||
|
|
// directory: the stateid hangs from an open of the directory and the
|
||
|
|
// notification types are echoed back, this server can produce them all.
|
||
|
|
func (h *Handler) getDirDelegationOp(d *xdr.Decoder, reg *fhreg, sessID nfs4.SessionID, clientid uint64) ([]byte, uint32, error) {
|
||
|
|
signalAvail, err := d.Bool()
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
notifyTypes, err := nfs4.ReadBitmap(d)
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
for range 2 {
|
||
|
|
if _, err = d.Int64(); err != nil { // attr delay seconds
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
if _, err = d.Uint32(); err != nil { // attr delay nseconds
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err = nfs4.ReadBitmap(d); err != nil { // child attributes
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
if _, err = nfs4.ReadBitmap(d); err != nil { // dir attributes
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
_ = signalAvail
|
||
|
|
if !reg.haveCur {
|
||
|
|
return nil, nfs4.ErrNoFileHandle, nil
|
||
|
|
}
|
||
|
|
// Only directories carry directory delegations.
|
||
|
|
if info, gerr := h.FS.Getattr(reg.cur); gerr == nil && !info.IsDir() {
|
||
|
|
return nil, nfs4.ErrWrongType, nil
|
||
|
|
}
|
||
|
|
dg, status := h.dirDelegs().grant(sessID, clientid, reg.cur)
|
||
|
|
if status != nfs4.ErrOK {
|
||
|
|
return nil, status, nil
|
||
|
|
}
|
||
|
|
verf := h.writeVerifier()
|
||
|
|
return nfs4.AppendGetDirDelegationResOK(nil, verf, dg.stateid, notifyTypes, nfs4.Bitmap{}, nfs4.Bitmap{}), nfs4.ErrOK, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// notifyDir pushes one CB_NOTIFY to the holder of the directory
|
||
|
|
// delegation, if the changed directory is delegated to somebody else.
|
||
|
|
// The notification is advisory and queued onto the holder's callback
|
||
|
|
// worker, so a mutation served on the holder's own connection never
|
||
|
|
// waits for the reply only that connection's read loop can route.
|
||
|
|
func (h *Handler) notifyDir(dir nfsfs.Handle, change nfs4.Notify4) {
|
||
|
|
dd, ok := h.dirDelegByFH(dir)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
args := nfs4.AppendCBNotifyArgs(nil, dd.stateid, dir, []nfs4.Notify4{change})
|
||
|
|
_ = h.queueCB(dd.sessID, "dir-notify", [][]byte{args})
|
||
|
|
}
|
||
|
|
|
||
|
|
// dirDelegByFH finds the live delegation of a directory.
|
||
|
|
func (h *Handler) dirDelegByFH(dir nfsfs.Handle) (*dirDeleg, bool) {
|
||
|
|
h.mu.Lock()
|
||
|
|
store := h.dirDelegSt
|
||
|
|
h.mu.Unlock()
|
||
|
|
if store == nil {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
store.mu.Lock()
|
||
|
|
defer store.mu.Unlock()
|
||
|
|
d, ok := store.byDir[fileKey(dir)]
|
||
|
|
return d, ok
|
||
|
|
}
|
||
|
|
|
||
|
|
// notifyDirOf is the helper the mutating operations call: it reads the
|
||
|
|
// parent through the backend and pushes the change. nameCookie carries
|
||
|
|
// the cookie semantics of the event; zero suffices for the wire shape
|
||
|
|
// this build answers with.
|
||
|
|
func (h *Handler) notifyDirOf(dir nfsfs.Handle, mask nfs4.Bitmap, name string) {
|
||
|
|
vals := buildNotifyBody(mask, name)
|
||
|
|
if vals == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
h.notifyDir(dir, nfs4.Notify4{Mask: mask, Vals: vals})
|
||
|
|
}
|
||
|
|
|
||
|
|
// buildNotifyBody encodes the change body the mask names.
|
||
|
|
func buildNotifyBody(mask nfs4.Bitmap, name string) []byte {
|
||
|
|
if mask.Has(nfs4.NotifyAddEntry) {
|
||
|
|
return nfs4.AppendNotifyAdd(nil, name, 0, true)
|
||
|
|
}
|
||
|
|
if mask.Has(nfs4.NotifyRemoveEntry) {
|
||
|
|
return nfs4.AppendNotifyRemove(nil, name, 0)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// notifyAppendCB walks the decoded notify body for the tests: it splits
|
||
|
|
// the name out of a notify_entry4 payload.
|
||
|
|
func notifyNameOf(body []byte) string {
|
||
|
|
d := xdr.NewDecoder(body)
|
||
|
|
if _, err := d.Uint32(); err != nil { // nad_old_entry count
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
name, err := d.String()
|
||
|
|
if err != nil {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return name
|
||
|
|
}
|