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,112 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build linux
|
||||
|
||||
package nfsfs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// The whence values of the hole seeking of Linux.
|
||||
const (
|
||||
seekData = 3
|
||||
seekHole = 4
|
||||
)
|
||||
|
||||
// SeekHole finds the next hole at or after the offset. The eof flag
|
||||
// reports that none of the requested content follows: every file
|
||||
// carries a virtual hole at its end, so a dense tail answers the file
|
||||
// size with eof set, RFC 7862 section 15.11. ErrBeyondEOF answers a
|
||||
// request that starts past the end.
|
||||
func (l *Local) SeekHole(h Handle, offset int64) (int64, bool, error) {
|
||||
return l.seek(h, offset, seekHole)
|
||||
}
|
||||
|
||||
// SeekData finds the next data byte at or after the offset. The eof
|
||||
// flag reports that no data follows the offset, and the answer names
|
||||
// the file size; ErrBeyondEOF answers a request that starts past the
|
||||
// end.
|
||||
func (l *Local) SeekData(h Handle, offset int64) (int64, bool, error) {
|
||||
return l.seek(h, offset, seekData)
|
||||
}
|
||||
|
||||
func (l *Local) seek(h Handle, offset int64, whence int) (int64, bool, error) {
|
||||
// The seek opens the same revalidated, unfollowed descriptor every
|
||||
// other data path uses, so a name swapped for a link never serves
|
||||
// bytes from outside the export.
|
||||
f, _, err := l.openVerified(h, syscall.O_RDONLY)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
defer f.Close()
|
||||
fd := int(f.Fd())
|
||||
size, err := fstatSize(fd)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if offset > size {
|
||||
return 0, false, ErrBeyondEOF
|
||||
}
|
||||
at, err := syscall.Seek(fd, offset, whence)
|
||||
if err == syscall.ENXIO {
|
||||
// The range from the offset to the end carries none of the
|
||||
// requested content: the answer is the end of the file with the
|
||||
// eof flag set.
|
||||
return size, true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if at >= size {
|
||||
// The virtual hole at the end of every file: the eof flag tells
|
||||
// the client the search is over.
|
||||
return size, true, nil
|
||||
}
|
||||
return at, false, nil
|
||||
}
|
||||
|
||||
// Allocate reserves a range with fallocate in the default mode, which
|
||||
// grows the file to offset plus length, the behaviour RFC 7862 section
|
||||
// 15.1 requires of ALLOCATE; Deallocate punches a hole into the range and
|
||||
// keeps the size. The handle is opened without following a final symlink
|
||||
// and revalidated against the descriptor before anything is reserved.
|
||||
func (l *Local) Allocate(h Handle, offset, length int64) error {
|
||||
return l.fallocate(h, offset, length, 0)
|
||||
}
|
||||
|
||||
// Deallocate punches a hole into the range and keeps the file size.
|
||||
func (l *Local) Deallocate(h Handle, offset, length int64) error {
|
||||
return l.fallocate(h, offset, length, 0x03) // KEEP_SIZE|PUNCH_HOLE
|
||||
}
|
||||
|
||||
func (l *Local) fallocate(h Handle, offset, length int64, mode uint32) error {
|
||||
kind, _, _, _, err := l.resolve(h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if kind != typeFile {
|
||||
return ErrIsDir
|
||||
}
|
||||
f, _, err := l.openVerified(h, syscall.O_WRONLY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, _, errno := syscall.Syscall6(syscall.SYS_FALLOCATE, f.Fd(), uintptr(mode),
|
||||
uintptr(offset), uintptr(length), 0, 0)
|
||||
if errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fstatSize reads the size of the open file.
|
||||
func fstatSize(fd int) (int64, error) {
|
||||
var st syscall.Stat_t
|
||||
if err := syscall.Fstat(fd, &st); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return st.Size, nil
|
||||
}
|
||||
Reference in New Issue
Block a user