feat: full NFSv4.2 server and client in pure Go
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user