This commit is contained in:
royalcat 2024-03-28 16:09:42 +03:00
parent 7b1863109c
commit ef751771d2
107 changed files with 9435 additions and 850 deletions

27
pkg/ctxbilly/change.go Normal file
View file

@ -0,0 +1,27 @@
package ctxbilly
import (
"context"
"os"
"time"
)
// Change abstract the FileInfo change related operations in a storage-agnostic
// interface as an extension to the Basic interface
type Change interface {
// Chmod changes the mode of the named file to mode. If the file is a
// symbolic link, it changes the mode of the link's target.
Chmod(ctx context.Context, name string, mode os.FileMode) error
// Lchown changes the numeric uid and gid of the named file. If the file is
// a symbolic link, it changes the uid and gid of the link itself.
Lchown(ctx context.Context, name string, uid, gid int) error
// Chown changes the numeric uid and gid of the named file. If the file is a
// symbolic link, it changes the uid and gid of the link's target.
Chown(ctx context.Context, name string, uid, gid int) error
// Chtimes changes the access and modification times of the named file,
// similar to the Unix utime() or utimes() functions.
//
// The underlying filesystem may truncate or round the values to a less
// precise time unit.
Chtimes(ctx context.Context, name string, atime time.Time, mtime time.Time) error
}

92
pkg/ctxbilly/fs.go Normal file
View file

@ -0,0 +1,92 @@
package ctxbilly
import (
"context"
"io"
"os"
"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
)
type Filesystem interface {
// Create creates the named file with mode 0666 (before umask), truncating
// it if it already exists. If successful, methods on the returned File can
// be used for I/O; the associated file descriptor has mode O_RDWR.
Create(ctx context.Context, filename string) (File, error)
// Open opens the named file for reading. If successful, methods on the
// returned file can be used for reading; the associated file descriptor has
// mode O_RDONLY.
Open(ctx context.Context, filename string) (File, error)
// OpenFile is the generalized open call; most users will use Open or Create
// instead. It opens the named file with specified flag (O_RDONLY etc.) and
// perm, (0666 etc.) if applicable. If successful, methods on the returned
// File can be used for I/O.
OpenFile(ctx context.Context, filename string, flag int, perm os.FileMode) (File, error)
// Stat returns a FileInfo describing the named file.
Stat(ctx context.Context, filename string) (os.FileInfo, error)
// Rename renames (moves) oldpath to newpath. If newpath already exists and
// is not a directory, Rename replaces it. OS-specific restrictions may
// apply when oldpath and newpath are in different directories.
Rename(ctx context.Context, oldpath, newpath string) error
// Remove removes the named file or directory.
Remove(ctx context.Context, filename string) error
// Join joins any number of path elements into a single path, adding a
// Separator if necessary. Join calls filepath.Clean on the result; in
// particular, all empty strings are ignored. On Windows, the result is a
// UNC path if and only if the first path element is a UNC path.
Join(elem ...string) string
// TempFile creates a new temporary file in the directory dir with a name
// beginning with prefix, opens the file for reading and writing, and
// returns the resulting *os.File. If dir is the empty string, TempFile
// uses the default directory for temporary files (see os.TempDir).
// Multiple programs calling TempFile simultaneously will not choose the
// same file. The caller can use f.Name() to find the pathname of the file.
// It is the caller's responsibility to remove the file when no longer
// needed.
TempFile(ctx context.Context, dir, prefix string) (File, error)
// ReadDir reads the directory named by d(irname and returns a list of
// directory entries sorted by filename.
ReadDir(ctx context.Context, path string) ([]os.FileInfo, error)
// MkdirAll creates a directory named path, along with any necessary
// parents, and returns nil, or else returns an error. The permission bits
// perm are used for all directories that MkdirAll creates. If path is/
// already a directory, MkdirAll does nothing and returns nil.
MkdirAll(ctx context.Context, filename string, perm os.FileMode) error
// Lstat returns a FileInfo describing the named file. If the file is a
// symbolic link, the returned FileInfo describes the symbolic link. Lstat
// makes no attempt to follow the link.
Lstat(ctx context.Context, filename string) (os.FileInfo, error)
// Symlink creates a symbolic-link from link to target. target may be an
// absolute or relative path, and need not refer to an existing node.
// Parent directories of link are created as necessary.
Symlink(ctx context.Context, target, link string) error
// Readlink returns the target path of link.
Readlink(ctx context.Context, link string) (string, error)
// // Chroot returns a new filesystem from the same type where the new root is
// // the given path. Files outside of the designated directory tree cannot be
// // accessed.
// Chroot(path string) (Filesystem, error)
// // Root returns the root path of the filesystem.
// Root() string
}
type File interface {
// Name returns the name of the file as presented to Open.
Name() string
ctxio.Writer
ctxio.Reader
ctxio.ReaderAt
io.Seeker
ctxio.Closer
// Lock locks the file like e.g. flock. It protects against access from
// other processes.
Lock() error
// Unlock unlocks the file.
Unlock() error
// Truncate the file.
Truncate(ctx context.Context, size int64) error
}

166
pkg/ctxbilly/mem.go Normal file
View file

@ -0,0 +1,166 @@
package ctxbilly
import (
"context"
"io/fs"
"github.com/go-git/go-billy/v5"
)
func WrapFileSystem(bf billy.Filesystem) Filesystem {
return &wrapFS{
Filesystem: bf,
}
}
type wrapFS struct {
billy.Filesystem
}
var _ Filesystem = (*wrapFS)(nil)
// Create implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Create of MemFS.Filesystem.
func (m *wrapFS) Create(ctx context.Context, filename string) (File, error) {
bf, err := m.Filesystem.Create(filename)
if err != nil {
return nil, err
}
return &wrapFile{bf}, nil
}
// Lstat implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Lstat of MemFS.Filesystem.
func (m *wrapFS) Lstat(ctx context.Context, filename string) (fs.FileInfo, error) {
return m.Filesystem.Lstat(filename)
}
// MkdirAll implements Filesystem.
// Subtle: this method shadows the method (Filesystem).MkdirAll of MemFS.Filesystem.
func (m *wrapFS) MkdirAll(ctx context.Context, filename string, perm fs.FileMode) error {
return m.Filesystem.MkdirAll(filename, perm)
}
// Open implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Open of MemFS.Filesystem.
func (m *wrapFS) Open(ctx context.Context, filename string) (File, error) {
bf, err := m.Filesystem.Open(filename)
if err != nil {
return nil, err
}
return WrapFile(bf), nil
}
// OpenFile implements Filesystem.
// Subtle: this method shadows the method (Filesystem).OpenFile of MemFS.Filesystem.
func (m *wrapFS) OpenFile(ctx context.Context, filename string, flag int, perm fs.FileMode) (File, error) {
bf, err := m.Filesystem.OpenFile(filename, flag, perm)
if err != nil {
return nil, err
}
return WrapFile(bf), nil
}
// ReadDir implements Filesystem.
// Subtle: this method shadows the method (Filesystem).ReadDir of MemFS.Filesystem.
func (m *wrapFS) ReadDir(ctx context.Context, path string) ([]fs.FileInfo, error) {
return m.Filesystem.ReadDir(path)
}
// Readlink implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Readlink of MemFS.Filesystem.
func (m *wrapFS) Readlink(ctx context.Context, link string) (string, error) {
return m.Filesystem.Readlink(link)
}
// Remove implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Remove of MemFS.Filesystem.
func (m *wrapFS) Remove(ctx context.Context, filename string) error {
return m.Filesystem.Remove(filename)
}
// Rename implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Rename of MemFS.Filesystem.
func (m *wrapFS) Rename(ctx context.Context, oldpath string, newpath string) error {
return m.Filesystem.Rename(oldpath, newpath)
}
// Stat implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Stat of MemFS.Filesystem.
func (m *wrapFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
return m.Filesystem.Stat(filename)
}
// Symlink implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Symlink of MemFS.Filesystem.
func (m *wrapFS) Symlink(ctx context.Context, target string, link string) error {
return m.Filesystem.Symlink(target, link)
}
// TempFile implements Filesystem.
// Subtle: this method shadows the method (Filesystem).TempFile of MemFS.Filesystem.
func (m *wrapFS) TempFile(ctx context.Context, dir string, prefix string) (File, error) {
file, err := m.Filesystem.TempFile(dir, prefix)
if err != nil {
return nil, err
}
return WrapFile(file), nil
}
func WrapFile(bf billy.File) File {
return &wrapFile{File: bf}
}
type wrapFile struct {
billy.File
}
var _ File = (*wrapFile)(nil)
// Close implements File.
// Subtle: this method shadows the method (File).Close of MemFile.File.
func (m *wrapFile) Close(ctx context.Context) error {
return m.File.Close()
}
// Lock implements File.
// Subtle: this method shadows the method (File).Lock of MemFile.File.
func (m *wrapFile) Lock() error {
return m.File.Lock()
}
// Name implements File.
// Subtle: this method shadows the method (File).Name of MemFile.File.
func (m *wrapFile) Name() string {
return m.File.Name()
}
// Truncate implements File.
// Subtle: this method shadows the method (File).Truncate of memFile.File.
func (m *wrapFile) Truncate(ctx context.Context, size int64) error {
return m.File.Truncate(size)
}
// Read implements File.
// Subtle: this method shadows the method (File).Read of MemFile.File.
func (m *wrapFile) Read(ctx context.Context, p []byte) (n int, err error) {
return m.File.Read(p)
}
// ReadAt implements File.
// Subtle: this method shadows the method (File).ReadAt of MemFile.File.
func (m *wrapFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
return m.File.ReadAt(p, off)
}
// Unlock implements File.
// Subtle: this method shadows the method (File).Unlock of MemFile.File.
func (m *wrapFile) Unlock() error {
return m.File.Unlock()
}
// Write implements File.
// Subtle: this method shadows the method (File).Write of MemFile.File.
func (m *wrapFile) Write(ctx context.Context, p []byte) (n int, err error) {
return m.File.Write(p)
}

63
pkg/ctxio/cachereader.go Normal file
View file

@ -0,0 +1,63 @@
package ctxio
import (
"context"
"errors"
"io"
"sync"
)
type CacheReader struct {
m sync.Mutex
fo int64
fr *FileBuffer
to int64
tr Reader
}
var _ FileReader = (*CacheReader)(nil)
func NewCacheReader(r Reader) (FileReader, error) {
fr := NewFileBuffer(nil)
tr := TeeReader(r, fr)
return &CacheReader{fr: fr, tr: tr}, nil
}
func (dtr *CacheReader) ReadAt(ctx context.Context, p []byte, off int64) (int, error) {
dtr.m.Lock()
defer dtr.m.Unlock()
tb := off + int64(len(p))
if tb > dtr.fo {
w, err := CopyN(ctx, Discard, dtr.tr, tb-dtr.fo)
dtr.to += w
if err != nil && err != io.EOF {
return 0, err
}
}
n, err := dtr.fr.ReadAt(ctx, p, off)
dtr.fo += int64(n)
return n, err
}
func (dtr *CacheReader) Read(ctx context.Context, p []byte) (n int, err error) {
dtr.m.Lock()
defer dtr.m.Unlock()
// use directly tee reader here
n, err = dtr.tr.Read(ctx, p)
dtr.to += int64(n)
return
}
func (dtr *CacheReader) Close(ctx context.Context) error {
frcloser := dtr.fr.Close(ctx)
var closeerr error
if rc, ok := dtr.tr.(ReadCloser); ok {
closeerr = rc.Close(ctx)
}
return errors.Join(frcloser, closeerr)
}

89
pkg/ctxio/copy.go Normal file
View file

@ -0,0 +1,89 @@
package ctxio
// // CopyN copies n bytes (or until an error) from src to dst.
// // It returns the number of bytes copied and the earliest
// // error encountered while copying.
// // On return, written == n if and only if err == nil.
// //
// // If dst implements [ReaderFrom], the copy is implemented using it.
// func CopyN(ctx context.Context, dst Writer, src Reader, n int64) (written int64, err error) {
// written, err = Copy(ctx, dst, LimitReader(src, n))
// if written == n {
// return n, nil
// }
// if written < n && err == nil {
// // src stopped early; must have been EOF.
// err = io.EOF
// }
// return
// }
// // Copy copies from src to dst until either EOF is reached
// // on src or an error occurs. It returns the number of bytes
// // copied and the first error encountered while copying, if any.
// //
// // A successful Copy returns err == nil, not err == EOF.
// // Because Copy is defined to read from src until EOF, it does
// // not treat an EOF from Read as an error to be reported.
// //
// // If src implements [WriterTo],
// // the copy is implemented by calling src.WriteTo(dst).
// // Otherwise, if dst implements [ReaderFrom],
// // the copy is implemented by calling dst.ReadFrom(src).
// func Copy(ctx context.Context, dst Writer, src Reader) (written int64, err error) {
// return copyBuffer(ctx, dst, src, nil)
// }
// // copyBuffer is the actual implementation of Copy and CopyBuffer.
// // if buf is nil, one is allocated.
// func copyBuffer(ctx context.Context, dst Writer, src Reader, buf []byte) (written int64, err error) {
// // If the reader has a WriteTo method, use it to do the copy.
// // Avoids an allocation and a copy.
// if wt, ok := src.(WriterTo); ok {
// return wt.WriteTo(dst)
// }
// // Similarly, if the writer has a ReadFrom method, use it to do the copy.
// if rt, ok := dst.(ReaderFrom); ok {
// return rt.ReadFrom(src)
// }
// if buf == nil {
// size := 32 * 1024
// if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
// if l.N < 1 {
// size = 1
// } else {
// size = int(l.N)
// }
// }
// buf = make([]byte, size)
// }
// for {
// nr, er := src.Read(ctx, buf)
// if nr > 0 {
// nw, ew := dst.Write(ctx, buf[0:nr])
// if nw < 0 || nr < nw {
// nw = 0
// if ew == nil {
// ew = errInvalidWrite
// }
// }
// written += int64(nw)
// if ew != nil {
// err = ew
// break
// }
// if nr != nw {
// err = io.ErrShortWrite
// break
// }
// }
// if er != nil {
// if er != io.EOF {
// err = er
// }
// break
// }
// }
// return written, err
// }

180
pkg/ctxio/filebuffer.go Normal file
View file

@ -0,0 +1,180 @@
package ctxio
import (
"bytes"
"context"
"errors"
"io"
"os"
)
// FileBuffer implements interfaces implemented by files.
// The main purpose of this type is to have an in memory replacement for a
// file.
type FileBuffer struct {
// buff is the backing buffer
buff *bytes.Buffer
// index indicates where in the buffer we are at
index int64
isClosed bool
}
var _ FileReader = (*FileBuffer)(nil)
var _ Writer = (*FileBuffer)(nil)
// NewFileBuffer returns a new populated Buffer
func NewFileBuffer(b []byte) *FileBuffer {
return &FileBuffer{buff: bytes.NewBuffer(b)}
}
// NewFileBufferFromReader is a convenience method that returns a new populated Buffer
// whose contents are sourced from a supplied reader by loading it entirely
// into memory.
func NewFileBufferFromReader(ctx context.Context, reader Reader) (*FileBuffer, error) {
data, err := ReadAll(ctx, reader)
if err != nil {
return nil, err
}
return NewFileBuffer(data), nil
}
// NewFileBufferFromReader is a convenience method that returns a new populated Buffer
// whose contents are sourced from a supplied reader by loading it entirely
// into memory.
func NewFileBufferFromIoReader(reader io.Reader) (*FileBuffer, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return NewFileBuffer(data), nil
}
// Bytes returns the bytes available until the end of the buffer.
func (f *FileBuffer) Bytes() []byte {
if f.isClosed || f.index >= int64(f.buff.Len()) {
return []byte{}
}
return f.buff.Bytes()[f.index:]
}
// String implements the Stringer interface
func (f *FileBuffer) String() string {
return string(f.buff.Bytes()[f.index:])
}
// Read implements io.Reader https://golang.org/pkg/io/#Reader
// Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p))
// and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch
// space during the call. If some data is available but not len(p) bytes, Read conventionally
// returns what is available instead of waiting for more.
// When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes,
// it returns the number of bytes read. It may return the (non-nil) error from the same call or
// return the error (and n == 0) from a subsequent call. An instance of this general case is
// that a Reader returning a non-zero number of bytes at the end of the input stream may return
// either err == EOF or err == nil. The next Read should return 0, EOF.
func (f *FileBuffer) Read(ctx context.Context, b []byte) (n int, err error) {
if f.isClosed {
return 0, os.ErrClosed
}
if len(b) == 0 {
return 0, nil
}
if f.index >= int64(f.buff.Len()) {
return 0, io.EOF
}
n, err = bytes.NewBuffer(f.buff.Bytes()[f.index:]).Read(b)
f.index += int64(n)
return n, err
}
// ReadAt implements io.ReaderAt https://golang.org/pkg/io/#ReaderAt
// ReadAt reads len(p) bytes into p starting at offset off in the underlying input source.
// It returns the number of bytes read (0 <= n <= len(p)) and any error encountered.
//
// When ReadAt returns n < len(p), it returns a non-nil error explaining why more bytes were not returned.
// In this respect, ReadAt is stricter than Read.
//
// Even if ReadAt returns n < len(p), it may use all of p as scratch space during the call.
// If some data is available but not len(p) bytes, ReadAt blocks until either all the data is available or an error occurs.
// In this respect ReadAt is different from Read.
//
// If the n = len(p) bytes returned by ReadAt are at the end of the input source,
// ReadAt may return either err == EOF or err == nil.
//
// If ReadAt is reading from an input source with a seek offset,
// ReadAt should not affect nor be affected by the underlying seek offset.
// Clients of ReadAt can execute parallel ReadAt calls on the same input source.
func (f *FileBuffer) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
if f.isClosed {
return 0, os.ErrClosed
}
if off < 0 {
return 0, errors.New("filebuffer.ReadAt: negative offset")
}
reqLen := len(p)
buffLen := int64(f.buff.Len())
if off >= buffLen {
return 0, io.EOF
}
n = copy(p, f.buff.Bytes()[off:])
if n < reqLen {
err = io.EOF
}
return n, err
}
// Write implements io.Writer https://golang.org/pkg/io/#Writer
// by appending the passed bytes to the buffer unless the buffer is closed or index negative.
func (f *FileBuffer) Write(ctx context.Context, p []byte) (n int, err error) {
if f.isClosed {
return 0, os.ErrClosed
}
if f.index < 0 {
return 0, io.EOF
}
// we might have rewinded, let's reset the buffer before appending to it
idx := int(f.index)
buffLen := f.buff.Len()
if idx != buffLen && idx <= buffLen {
f.buff = bytes.NewBuffer(f.Bytes()[:f.index])
}
n, err = f.buff.Write(p)
f.index += int64(n)
return n, err
}
// Seek implements io.Seeker https://golang.org/pkg/io/#Seeker
func (f *FileBuffer) Seek(offset int64, whence int) (idx int64, err error) {
if f.isClosed {
return 0, os.ErrClosed
}
var abs int64
switch whence {
case 0:
abs = offset
case 1:
abs = int64(f.index) + offset
case 2:
abs = int64(f.buff.Len()) + offset
default:
return 0, errors.New("filebuffer.Seek: invalid whence")
}
if abs < 0 {
return 0, errors.New("filebuffer.Seek: negative position")
}
f.index = abs
return abs, nil
}
// Close implements io.Closer https://golang.org/pkg/io/#Closer
// It closes the buffer, rendering it unusable for I/O. It returns an error, if any.
func (f *FileBuffer) Close(ctx context.Context) error {
f.isClosed = true
f.buff = nil
return nil
}

663
pkg/ctxio/io.go Normal file
View file

@ -0,0 +1,663 @@
package ctxio
import (
"context"
"errors"
"io"
"sync"
)
// Seek whence values.
const (
SeekStart = 0 // seek relative to the origin of the file
SeekCurrent = 1 // seek relative to the current offset
SeekEnd = 2 // seek relative to the end
)
// ErrShortWrite means that a write accepted fewer bytes than requested
// but failed to return an explicit error.
var ErrShortWrite = io.ErrShortWrite
// errInvalidWrite means that a write returned an impossible count.
var errInvalidWrite = errors.New("invalid write result")
// ErrShortBuffer means that a read required a longer buffer than was provided.
var ErrShortBuffer = io.ErrShortBuffer
// EOF is the error returned by Read when no more input is available.
// (Read must return EOF itself, not an error wrapping EOF,
// because callers will test for EOF using ==.)
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either [ErrUnexpectedEOF] or some other error
// giving more detail.
var EOF = io.EOF
// ErrUnexpectedEOF means that EOF was encountered in the
// middle of reading a fixed-size block or data structure.
var ErrUnexpectedEOF = io.ErrUnexpectedEOF
// ErrNoProgress is returned by some clients of a [Reader] when
// many calls to Read have failed to return any data or error,
// usually the sign of a broken [Reader] implementation.
var ErrNoProgress = io.ErrNoProgress
// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered. Even if Read
// returns n < len(p), it may use all of p as scratch space during the call.
// If some data is available but not len(p) bytes, Read conventionally
// returns what is available instead of waiting for more.
//
// When Read encounters an error or end-of-file condition after
// successfully reading n > 0 bytes, it returns the number of
// bytes read. It may return the (non-nil) error from the same call
// or return the error (and n == 0) from a subsequent call.
// An instance of this general case is that a Reader returning
// a non-zero number of bytes at the end of the input stream may
// return either err == EOF or err == nil. The next Read should
// return 0, EOF.
//
// Callers should always process the n > 0 bytes returned before
// considering the error err. Doing so correctly handles I/O errors
// that happen after reading some bytes and also both of the
// allowed EOF behaviors.
//
// If len(p) == 0, Read should always return n == 0. It may return a
// non-nil error if some error condition is known, such as EOF.
//
// Implementations of Read are discouraged from returning a
// zero byte count with a nil error, except when len(p) == 0.
// Callers should treat a return of 0 and nil as indicating that
// nothing happened; in particular it does not indicate EOF.
//
// Implementations must not retain p.
type Reader interface {
Read(ctx context.Context, p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
Write(ctx context.Context, p []byte) (n int, err error)
}
// Closer is the interface that wraps the basic Close method.
//
// The behavior of Close after the first call is undefined.
// Specific implementations may document their own behavior.
type Closer interface {
Close(ctx context.Context) error
}
// Seeker is the interface that wraps the basic Seek method.
//
// Seek sets the offset for the next Read or Write to offset,
// interpreted according to whence:
// [SeekStart] means relative to the start of the file,
// [SeekCurrent] means relative to the current offset, and
// [SeekEnd] means relative to the end
// (for example, offset = -2 specifies the penultimate byte of the file).
// Seek returns the new offset relative to the start of the
// file or an error, if any.
//
// Seeking to an offset before the start of the file is an error.
// Seeking to any positive offset may be allowed, but if the new offset exceeds
// the size of the underlying object the behavior of subsequent I/O operations
// is implementation-dependent.
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
Reader
Closer
}
// WriteCloser is the interface that groups the basic Write and Close methods.
type WriteCloser interface {
Writer
Closer
}
// ReadWriteCloser is the interface that groups the basic Read, Write and Close methods.
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// ReadSeeker is the interface that groups the basic Read and Seek methods.
type ReadSeeker interface {
Reader
Seeker
}
// ReadSeekCloser is the interface that groups the basic Read, Seek and Close
// methods.
type ReadSeekCloser interface {
Reader
Seeker
Closer
}
// WriteSeeker is the interface that groups the basic Write and Seek methods.
type WriteSeeker interface {
Writer
Seeker
}
// ReadWriteSeeker is the interface that groups the basic Read, Write and Seek methods.
type ReadWriteSeeker interface {
Reader
Writer
Seeker
}
// ReaderFrom is the interface that wraps the ReadFrom method.
//
// ReadFrom reads data from r until EOF or error.
// The return value n is the number of bytes read.
// Any error except EOF encountered during the read is also returned.
//
// The [Copy] function uses [ReaderFrom] if available.
type ReaderFrom interface {
ReadFrom(ctx context.Context, r Reader) (n int64, err error)
}
// WriterTo is the interface that wraps the WriteTo method.
//
// WriteTo writes data to w until there's no more data to write or
// when an error occurs. The return value n is the number of bytes
// written. Any error encountered during the write is also returned.
//
// The Copy function uses WriterTo if available.
type WriterTo interface {
WriteTo(ctx context.Context, w Writer) (n int64, err error)
}
// ReaderAt is the interface that wraps the basic ReadAt method.
//
// ReadAt reads len(p) bytes into p starting at offset off in the
// underlying input source. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered.
//
// When ReadAt returns n < len(p), it returns a non-nil error
// explaining why more bytes were not returned. In this respect,
// ReadAt is stricter than Read.
//
// Even if ReadAt returns n < len(p), it may use all of p as scratch
// space during the call. If some data is available but not len(p) bytes,
// ReadAt blocks until either all the data is available or an error occurs.
// In this respect ReadAt is different from Read.
//
// If the n = len(p) bytes returned by ReadAt are at the end of the
// input source, ReadAt may return either err == EOF or err == nil.
//
// If ReadAt is reading from an input source with a seek offset,
// ReadAt should not affect nor be affected by the underlying
// seek offset.
//
// Clients of ReadAt can execute parallel ReadAt calls on the
// same input source.
//
// Implementations must not retain p.
type ReaderAt interface {
ReadAt(ctx context.Context, p []byte, off int64) (n int, err error)
}
// WriterAt is the interface that wraps the basic WriteAt method.
//
// WriteAt writes len(p) bytes from p to the underlying data stream
// at offset off. It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// WriteAt must return a non-nil error if it returns n < len(p).
//
// If WriteAt is writing to a destination with a seek offset,
// WriteAt should not affect nor be affected by the underlying
// seek offset.
//
// Clients of WriteAt can execute parallel WriteAt calls on the same
// destination if the ranges do not overlap.
//
// Implementations must not retain p.
type WriterAt interface {
WriteAt(ctx context.Context, p []byte, off int64) (n int, err error)
}
// StringWriter is the interface that wraps the WriteString method.
type StringWriter interface {
WriteString(s string) (n int, err error)
}
// WriteString writes the contents of the string s to w, which accepts a slice of bytes.
// If w implements [StringWriter], [StringWriter.WriteString] is invoked directly.
// Otherwise, [Writer.Write] is called exactly once.
func WriteString(ctx context.Context, w Writer, s string) (n int, err error) {
if sw, ok := w.(StringWriter); ok {
return sw.WriteString(s)
}
return w.Write(ctx, []byte(s))
}
// ReadAtLeast reads from r into buf until it has read at least min bytes.
// It returns the number of bytes copied and an error if fewer bytes were read.
// The error is EOF only if no bytes were read.
// If an EOF happens after reading fewer than min bytes,
// ReadAtLeast returns [ErrUnexpectedEOF].
// If min is greater than the length of buf, ReadAtLeast returns [ErrShortBuffer].
// On return, n >= min if and only if err == nil.
// If r returns an error having read at least min bytes, the error is dropped.
func ReadAtLeast(ctx context.Context, r Reader, buf []byte, min int) (n int, err error) {
if len(buf) < min {
return 0, ErrShortBuffer
}
for n < min && err == nil {
var nn int
nn, err = r.Read(ctx, buf[n:])
n += nn
}
if n >= min {
err = nil
} else if n > 0 && err == EOF {
err = ErrUnexpectedEOF
}
return
}
// ReadFull reads exactly len(buf) bytes from r into buf.
// It returns the number of bytes copied and an error if fewer bytes were read.
// The error is EOF only if no bytes were read.
// If an EOF happens after reading some but not all the bytes,
// ReadFull returns [ErrUnexpectedEOF].
// On return, n == len(buf) if and only if err == nil.
// If r returns an error having read at least len(buf) bytes, the error is dropped.
func ReadFull(ctx context.Context, r Reader, buf []byte) (n int, err error) {
return ReadAtLeast(ctx, r, buf, len(buf))
}
// CopyN copies n bytes (or until an error) from src to dst.
// It returns the number of bytes copied and the earliest
// error encountered while copying.
// On return, written == n if and only if err == nil.
//
// If dst implements [ReaderFrom], the copy is implemented using it.
func CopyN(ctx context.Context, dst Writer, src Reader, n int64) (written int64, err error) {
written, err = Copy(ctx, dst, LimitReader(src, n))
if written == n {
return n, nil
}
if written < n && err == nil {
// src stopped early; must have been EOF.
err = EOF
}
return
}
// Copy copies from src to dst until either EOF is reached
// on src or an error occurs. It returns the number of bytes
// copied and the first error encountered while copying, if any.
//
// A successful Copy returns err == nil, not err == EOF.
// Because Copy is defined to read from src until EOF, it does
// not treat an EOF from Read as an error to be reported.
//
// If src implements [WriterTo],
// the copy is implemented by calling src.WriteTo(dst).
// Otherwise, if dst implements [ReaderFrom],
// the copy is implemented by calling dst.ReadFrom(src).
func Copy(ctx context.Context, dst Writer, src Reader) (written int64, err error) {
return copyBuffer(ctx, dst, src, nil)
}
// CopyBuffer is identical to Copy except that it stages through the
// provided buffer (if one is required) rather than allocating a
// temporary one. If buf is nil, one is allocated; otherwise if it has
// zero length, CopyBuffer panics.
//
// If either src implements [WriterTo] or dst implements [ReaderFrom],
// buf will not be used to perform the copy.
func CopyBuffer(ctx context.Context, dst Writer, src Reader, buf []byte) (written int64, err error) {
if buf != nil && len(buf) == 0 {
panic("empty buffer in CopyBuffer")
}
return copyBuffer(ctx, dst, src, buf)
}
// copyBuffer is the actual implementation of Copy and CopyBuffer.
// if buf is nil, one is allocated.
func copyBuffer(ctx context.Context, dst Writer, src Reader, buf []byte) (written int64, err error) {
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(ctx, dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rt, ok := dst.(ReaderFrom); ok {
return rt.ReadFrom(ctx, src)
}
if buf == nil {
size := 32 * 1024
if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
if l.N < 1 {
size = 1
} else {
size = int(l.N)
}
}
buf = make([]byte, size)
}
for {
nr, er := src.Read(ctx, buf)
if nr > 0 {
nw, ew := dst.Write(ctx, buf[0:nr])
if nw < 0 || nr < nw {
nw = 0
if ew == nil {
ew = errInvalidWrite
}
}
written += int64(nw)
if ew != nil {
err = ew
break
}
if nr != nw {
err = ErrShortWrite
break
}
}
if er != nil {
if er != EOF {
err = er
}
break
}
}
return written, err
}
// LimitReader returns a Reader that reads from r
// but stops with EOF after n bytes.
// The underlying implementation is a *LimitedReader.
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
// A LimitedReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns EOF when N <= 0 or when the underlying R returns EOF.
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
func (l *LimitedReader) Read(ctx context.Context, p []byte) (n int, err error) {
if l.N <= 0 {
return 0, EOF
}
if int64(len(p)) > l.N {
p = p[0:l.N]
}
n, err = l.R.Read(ctx, p)
l.N -= int64(n)
return
}
// NewSectionReader returns a [SectionReader] that reads from r
// starting at offset off and stops with EOF after n bytes.
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader {
var remaining int64
const maxint64 = 1<<63 - 1
if off <= maxint64-n {
remaining = n + off
} else {
// Overflow, with no way to return error.
// Assume we can read up to an offset of 1<<63 - 1.
remaining = maxint64
}
return &SectionReader{r, off, off, remaining, n}
}
// SectionReader implements Read, Seek, and ReadAt on a section
// of an underlying [ReaderAt].
type SectionReader struct {
r ReaderAt // constant after creation
base int64 // constant after creation
off int64
limit int64 // constant after creation
n int64 // constant after creation
}
func (s *SectionReader) Read(ctx context.Context, p []byte) (n int, err error) {
if s.off >= s.limit {
return 0, EOF
}
if max := s.limit - s.off; int64(len(p)) > max {
p = p[0:max]
}
n, err = s.r.ReadAt(ctx, p, s.off)
s.off += int64(n)
return
}
var errWhence = errors.New("Seek: invalid whence")
var errOffset = errors.New("Seek: invalid offset")
func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {
switch whence {
default:
return 0, errWhence
case SeekStart:
offset += s.base
case SeekCurrent:
offset += s.off
case SeekEnd:
offset += s.limit
}
if offset < s.base {
return 0, errOffset
}
s.off = offset
return offset - s.base, nil
}
func (s *SectionReader) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
if off < 0 || off >= s.Size() {
return 0, EOF
}
off += s.base
if max := s.limit - off; int64(len(p)) > max {
p = p[0:max]
n, err = s.r.ReadAt(ctx, p, off)
if err == nil {
err = EOF
}
return n, err
}
return s.r.ReadAt(ctx, p, off)
}
// Size returns the size of the section in bytes.
func (s *SectionReader) Size() int64 { return s.limit - s.base }
// Outer returns the underlying [ReaderAt] and offsets for the section.
//
// The returned values are the same that were passed to [NewSectionReader]
// when the [SectionReader] was created.
func (s *SectionReader) Outer() (r ReaderAt, off int64, n int64) {
return s.r, s.base, s.n
}
// An OffsetWriter maps writes at offset base to offset base+off in the underlying writer.
type OffsetWriter struct {
w WriterAt
base int64 // the original offset
off int64 // the current offset
}
// NewOffsetWriter returns an [OffsetWriter] that writes to w
// starting at offset off.
func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter {
return &OffsetWriter{w, off, off}
}
func (o *OffsetWriter) Write(ctx context.Context, p []byte) (n int, err error) {
n, err = o.w.WriteAt(ctx, p, o.off)
o.off += int64(n)
return
}
func (o *OffsetWriter) WriteAt(ctx context.Context, p []byte, off int64) (n int, err error) {
if off < 0 {
return 0, errOffset
}
off += o.base
return o.w.WriteAt(ctx, p, off)
}
func (o *OffsetWriter) Seek(offset int64, whence int) (int64, error) {
switch whence {
default:
return 0, errWhence
case SeekStart:
offset += o.base
case SeekCurrent:
offset += o.off
}
if offset < o.base {
return 0, errOffset
}
o.off = offset
return offset - o.base, nil
}
// TeeReader returns a [Reader] that writes to w what it reads from r.
// All reads from r performed through it are matched with
// corresponding writes to w. There is no internal buffering -
// the write must complete before the read completes.
// Any error encountered while writing is reported as a read error.
func TeeReader(r Reader, w Writer) Reader {
return &teeReader{r, w}
}
type teeReader struct {
r Reader
w Writer
}
func (t *teeReader) Read(ctx context.Context, p []byte) (n int, err error) {
n, err = t.r.Read(ctx, p)
if n > 0 {
if n, err := t.w.Write(ctx, p[:n]); err != nil {
return n, err
}
}
return
}
// Discard is a [Writer] on which all Write calls succeed
// without doing anything.
var Discard Writer = discard{}
type discard struct{}
// discard implements ReaderFrom as an optimization so Copy to
// io.Discard can avoid doing unnecessary work.
var _ ReaderFrom = discard{}
func (discard) Write(ctx context.Context, p []byte) (int, error) {
return len(p), nil
}
func (discard) WriteString(ctx context.Context, s string) (int, error) {
return len(s), nil
}
var blackHolePool = sync.Pool{
New: func() any {
b := make([]byte, 8192)
return &b
},
}
func (discard) ReadFrom(ctx context.Context, r Reader) (n int64, err error) {
bufp := blackHolePool.Get().(*[]byte)
readSize := 0
for {
readSize, err = r.Read(ctx, *bufp)
n += int64(readSize)
if err != nil {
blackHolePool.Put(bufp)
if err == EOF {
return n, nil
}
return
}
}
}
// NopCloser returns a [ReadCloser] with a no-op Close method wrapping
// the provided [Reader] r.
// If r implements [WriterTo], the returned [ReadCloser] will implement [WriterTo]
// by forwarding calls to r.
func NopCloser(r Reader) ReadCloser {
if _, ok := r.(WriterTo); ok {
return nopCloserWriterTo{r}
}
return nopCloser{r}
}
type nopCloser struct {
Reader
}
func (nopCloser) Close(ctx context.Context) error { return nil }
type nopCloserWriterTo struct {
Reader
}
func (nopCloserWriterTo) Close(ctx context.Context) error { return nil }
func (c nopCloserWriterTo) WriteTo(ctx context.Context, w Writer) (n int64, err error) {
return c.Reader.(WriterTo).WriteTo(ctx, w)
}
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(ctx context.Context, r Reader) ([]byte, error) {
b := make([]byte, 0, 512)
for {
n, err := r.Read(ctx, b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == EOF {
err = nil
}
return b, err
}
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
}
}

View file

@ -5,28 +5,22 @@ import (
"io"
)
type ReaderAtCloser interface {
type FileReader interface {
Reader
ReaderAt
Closer
}
type ReaderAt interface {
ReadAt(ctx context.Context, p []byte, off int64) (n int, err error)
}
type Reader interface {
Read(ctx context.Context, p []byte) (n int, err error)
}
type Closer interface {
Close(ctx context.Context) error
}
type contextReader struct {
ctx context.Context
r Reader
}
func (r *contextReader) Read(p []byte) (n int, err error) {
if r.ctx.Err() != nil {
return 0, r.ctx.Err()
}
return r.r.Read(r.ctx, p)
}
@ -40,9 +34,31 @@ type contextReaderAt struct {
}
func (c *contextReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
if c.ctx.Err() != nil {
return 0, c.ctx.Err()
}
return c.r.ReadAt(c.ctx, p, off)
}
func IoReader(ctx context.Context, r Reader) io.Reader {
return &contextReader{ctx: ctx, r: r}
}
func WrapIoReader(r io.Reader) Reader {
return &wrapReader{r: r}
}
type wrapReader struct {
r io.Reader
}
var _ Reader = (*wrapReader)(nil)
// Read implements Reader.
func (c *wrapReader) Read(ctx context.Context, p []byte) (n int, err error) {
if ctx.Err() != nil {
return 0, ctx.Err()
}
return c.r.Read(p)
}

View file

@ -59,10 +59,10 @@ type ioSeekerCloserWrapper struct {
pos int64
size int64
r ReaderAtCloser
r FileReader
}
func IoReadSeekCloserWrapper(ctx context.Context, r ReaderAtCloser, size int64) io.ReadSeekCloser {
func IoReadSeekCloserWrapper(ctx context.Context, r FileReader, size int64) io.ReadSeekCloser {
return &ioSeekerCloserWrapper{
ctx: ctx,
r: r,

20
pkg/ctxio/teereader.go Normal file
View file

@ -0,0 +1,20 @@
package ctxio
// func TeeReader(r Reader, w Writer) Reader {
// return &teeReader{r, w}
// }
// type teeReader struct {
// r Reader
// w Writer
// }
// func (t *teeReader) Read(ctx context.Context, p []byte) (n int, err error) {
// n, err = t.r.Read(ctx, p)
// if n > 0 {
// if n, err := t.w.Write(ctx, p[:n]); err != nil {
// return n, err
// }
// }
// return
// }

11
pkg/go-nfs/.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/" # Location of package manifests
schedule:
interval: "daily"

View file

@ -0,0 +1,51 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 18 * * 3'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

36
pkg/go-nfs/.github/workflows/go.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v3
with:
go-version: ^1.19
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Get dependencies
run: go get -v -t -d ./...
- name: Build
run: go build -v ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
- name: Test
run: go test -v .

View file

@ -0,0 +1,11 @@
# Contributing Guidelines
We appreciate your interest in improving go-nfs!
## Looking for ways to contribute?
There are several ways you can contribute:
- Start contributing immediately via the [opened](https://github.com/willscott/go-nfs/issues) issues on GitHub.
Defined issues provide an excellent starting point.
- Reporting issues, bugs, mistakes, or inconsistencies.
As many open source projects, we are short-staffed, we thus kindly ask you to be open to contribute a fix for discovered issues.

202
pkg/go-nfs/LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

96
pkg/go-nfs/README.md Normal file
View file

@ -0,0 +1,96 @@
Golang Network File Server
===
NFSv3 protocol implementation in pure Golang.
Current Status:
* Minimally tested
* Mounts, read-only and read-write support
Usage
===
The most interesting demo is currently in `example/osview`.
Start the server
`go run ./example/osview .`.
The local folder at `.` will be the initial view in the mount. mutations to metadata or contents
will be stored purely in memory and not written back to the OS. When run, this
demo will print the port it is listening on.
The mount can be accessed using a command similar to
`mount -o port=<n>,mountport=<n> -t nfs localhost:/mount <mountpoint>` (For Mac users)
or
`mount -o port=<n>,mountport=<n>,nfsvers=3,noacl,tcp -t nfs localhost:/mount <mountpoint>` (For Linux users)
API
===
The NFS server runs on a `net.Listener` to export a file system to NFS clients.
Usage is structured similarly to many other golang network servers.
```golang
package main
import (
"fmt"
"log"
"net"
"github.com/go-git/go-billy/v5/memfs"
nfs "github.com/willscott/go-nfs"
nfshelper "github.com/willscott/go-nfs/helpers"
)
func main() {
listener, err := net.Listen("tcp", ":0")
panicOnErr(err, "starting TCP listener")
fmt.Printf("Server running at %s\n", listener.Addr())
mem := memfs.New()
f, err := mem.Create("hello.txt")
panicOnErr(err, "creating file")
_, err = f.Write([]byte("hello world"))
panicOnErr(err, "writing data")
f.Close()
handler := nfshelper.NewNullAuthHandler(mem)
cacheHelper := nfshelper.NewCachingHandler(handler, 1)
panicOnErr(nfs.Serve(listener, cacheHelper), "serving nfs")
}
func panicOnErr(err error, desc ...interface{}) {
if err == nil {
return
}
log.Println(desc...)
log.Panicln(err)
}
```
Notes
---
* Ports are typically determined through portmap. The need for running portmap
(which is the only part that needs a privileged listening port) can be avoided
through specific mount options. e.g.
`mount -o port=n,mountport=n -t nfs host:/mount /localmount`
* This server currently uses [billy](https://github.com/go-git/go-billy/) to
provide a file system abstraction layer. There are some edges of the NFS protocol
which do not translate to this abstraction.
* NFS expects access to an `inode` or equivalent unique identifier to reference
files in a file system. These are considered opaque identifiers here, which
means they will not work as expected in cases of hard linking.
* The billy abstraction layer does not extend to exposing `uid` and `gid`
ownership of files. If ownership is important to your file system, you
will need to ensure that the `os.FileInfo` meets additional constraints.
In particular, the `Sys()` escape hatch is queried by this library, and
if your file system populates a [`syscall.Stat_t`](https://golang.org/pkg/syscall/#Stat_t)
concrete struct, the ownership specified in that object will be used.
* Relevant RFCS:
[5531 - RPC protocol](https://tools.ietf.org/html/rfc5531),
[1813 - NFSv3](https://tools.ietf.org/html/rfc1813),
[1094 - NFS](https://tools.ietf.org/html/rfc1094)

11
pkg/go-nfs/SECURITY.md Normal file
View file

@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
The latest release reflects the current best recommendation / supported version at this time.
## Reporting a Vulnerability
Please email Will (the git commit author) if you need to report issues privately.
I will endeavor to respond within a day, but if I am offline, responses may be delayed longer than that.
If you need a stronger SLA to have confidence in using this code, feel free to reach out.

View file

@ -0,0 +1,9 @@
package nfs
import (
billy "github.com/go-git/go-billy/v5"
)
func CapabilityCheck(fs Filesystem, cap billy.Capability) bool {
return true
}

335
pkg/go-nfs/conn.go Normal file
View file

@ -0,0 +1,335 @@
package nfs
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
xdr2 "github.com/rasky/go-xdr/xdr2"
"github.com/willscott/go-nfs-client/nfs/rpc"
"github.com/willscott/go-nfs-client/nfs/xdr"
"go.opentelemetry.io/otel"
)
var (
// ErrInputInvalid is returned when input cannot be parsed
ErrInputInvalid = errors.New("invalid input")
// ErrAlreadySent is returned when writing a header/status multiple times
ErrAlreadySent = errors.New("response already started")
)
// ResponseCode is a combination of accept_stat and reject_stat.
type ResponseCode uint32
// ResponseCode Codes
const (
ResponseCodeSuccess ResponseCode = iota
ResponseCodeProgUnavailable
ResponseCodeProcUnavailable
ResponseCodeGarbageArgs
ResponseCodeSystemErr
ResponseCodeRPCMismatch
ResponseCodeAuthError
)
type conn struct {
*Server
writeSerializer chan []byte
net.Conn
}
var tracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/pkg/go-nfs")
func (c *conn) serve() {
ctx := context.Background() // TODO implement correct timeout on serve side
c.writeSerializer = make(chan []byte, 1)
go c.serializeWrites(ctx)
bio := bufio.NewReader(c.Conn)
for {
w, err := c.readRequestHeader(ctx, bio)
if err != nil {
if err == io.EOF {
// Clean close.
c.Close()
return
}
return
}
Log.Tracef("request: %v", w.req)
err = c.handle(ctx, w)
respErr := w.finish(ctx)
if err != nil {
Log.Errorf("error handling req: %v", err)
// failure to handle at a level needing to close the connection.
c.Close()
return
}
if respErr != nil {
Log.Errorf("error sending response: %v", respErr)
c.Close()
return
}
}
}
func (c *conn) serializeWrites(ctx context.Context) {
// todo: maybe don't need the extra buffer
writer := bufio.NewWriter(c.Conn)
var fragmentBuf [4]byte
var fragmentInt uint32
for {
select {
case <-ctx.Done():
return
case msg, ok := <-c.writeSerializer:
if !ok {
return
}
// prepend the fragmentation header
fragmentInt = uint32(len(msg))
fragmentInt |= (1 << 31)
binary.BigEndian.PutUint32(fragmentBuf[:], fragmentInt)
n, err := writer.Write(fragmentBuf[:])
if n < 4 || err != nil {
return
}
n, err = writer.Write(msg)
if err != nil {
return
}
if n < len(msg) {
panic("todo: ensure writes complete fully.")
}
if err = writer.Flush(); err != nil {
return
}
}
}
}
// Handle a request. errors from this method indicate a failure to read or
// write on the network stream, and trigger a disconnection of the connection.
func (c *conn) handle(ctx context.Context, w *response) error {
ctx, span := tracer.Start(ctx, fmt.Sprintf("nfs.handle.%s", NFSProcedure(w.req.Header.Proc).String()))
defer span.End()
handler := c.Server.handlerFor(w.req.Header.Prog, w.req.Header.Proc)
if handler == nil {
Log.Errorf("No handler for %d.%d", w.req.Header.Prog, w.req.Header.Proc)
if err := w.drain(ctx); err != nil {
return err
}
return c.err(ctx, w, &ResponseCodeProcUnavailableError{})
}
appError := handler(ctx, w, c.Server.Handler)
if drainErr := w.drain(ctx); drainErr != nil {
return drainErr
}
if appError != nil && !w.responded {
Log.Errorf("call to %+v failed: %v", handler, appError)
if err := c.err(ctx, w, appError); err != nil {
return err
}
}
if !w.responded {
Log.Errorf("Handler did not indicate response status via writing or erroring")
if err := c.err(ctx, w, &ResponseCodeSystemError{}); err != nil {
return err
}
}
return nil
}
func (c *conn) err(ctx context.Context, w *response, err error) error {
select {
case <-ctx.Done():
return nil
default:
}
if w.err == nil {
w.err = err
}
if w.responded {
return nil
}
rpcErr := w.errorFmt(err)
if writeErr := w.writeHeader(rpcErr.Code()); writeErr != nil {
return writeErr
}
body, _ := rpcErr.MarshalBinary()
return w.Write(body)
}
type request struct {
xid uint32
rpc.Header
Body io.Reader
}
func (r *request) String() string {
if r.Header.Prog == nfsServiceID {
return fmt.Sprintf("RPC #%d (nfs.%s)", r.xid, NFSProcedure(r.Header.Proc))
} else if r.Header.Prog == mountServiceID {
return fmt.Sprintf("RPC #%d (mount.%s)", r.xid, MountProcedure(r.Header.Proc))
}
return fmt.Sprintf("RPC #%d (%d.%d)", r.xid, r.Header.Prog, r.Header.Proc)
}
type response struct {
*conn
writer *bytes.Buffer
responded bool
err error
errorFmt func(error) RPCError
req *request
}
func (w *response) writeXdrHeader() error {
err := xdr.Write(w.writer, &w.req.xid)
if err != nil {
return err
}
respType := uint32(1)
err = xdr.Write(w.writer, &respType)
if err != nil {
return err
}
return nil
}
func (w *response) writeHeader(code ResponseCode) error {
if w.responded {
return ErrAlreadySent
}
w.responded = true
if err := w.writeXdrHeader(); err != nil {
return err
}
status := rpc.MsgAccepted
if code == ResponseCodeAuthError || code == ResponseCodeRPCMismatch {
status = rpc.MsgDenied
}
err := xdr.Write(w.writer, &status)
if err != nil {
return err
}
if status == rpc.MsgAccepted {
// Write opaque_auth header.
err = xdr.Write(w.writer, &rpc.AuthNull)
if err != nil {
return err
}
}
return xdr.Write(w.writer, &code)
}
// Write a response to an xdr message
func (w *response) Write(dat []byte) error {
if !w.responded {
if err := w.writeHeader(ResponseCodeSuccess); err != nil {
return err
}
}
acc := 0
for acc < len(dat) {
n, err := w.writer.Write(dat[acc:])
if err != nil {
return err
}
acc += n
}
return nil
}
// drain reads the rest of the request frame if not consumed by the handler.
func (w *response) drain(ctx context.Context) error {
if reader, ok := w.req.Body.(*io.LimitedReader); ok {
if reader.N == 0 {
return nil
}
// todo: wrap body in a context reader.
_, err := io.CopyN(io.Discard, w.req.Body, reader.N)
if err == nil || err == io.EOF {
return nil
}
return err
}
return io.ErrUnexpectedEOF
}
func (w *response) finish(ctx context.Context) error {
select {
case w.conn.writeSerializer <- w.writer.Bytes():
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (c *conn) readRequestHeader(ctx context.Context, reader *bufio.Reader) (w *response, err error) {
fragment, err := xdr.ReadUint32(reader)
if err != nil {
if xdrErr, ok := err.(*xdr2.UnmarshalError); ok {
if xdrErr.Err == io.EOF {
return nil, io.EOF
}
}
return nil, err
}
if fragment&(1<<31) == 0 {
Log.Warnf("Warning: haven't implemented fragment reconstruction.\n")
return nil, ErrInputInvalid
}
reqLen := fragment - uint32(1<<31)
if reqLen < 40 {
return nil, ErrInputInvalid
}
r := io.LimitedReader{R: reader, N: int64(reqLen)}
xid, err := xdr.ReadUint32(&r)
if err != nil {
return nil, err
}
reqType, err := xdr.ReadUint32(&r)
if err != nil {
return nil, err
}
if reqType != 0 { // 0 = request, 1 = response
return nil, ErrInputInvalid
}
req := request{
xid,
rpc.Header{},
&r,
}
if err = xdr.Read(&r, &req.Header); err != nil {
return nil, err
}
w = &response{
conn: c,
req: &req,
errorFmt: basicErrorFormatter,
// TODO: use a pool for these.
writer: bytes.NewBuffer([]byte{}),
}
return w, nil
}

230
pkg/go-nfs/errors.go Normal file
View file

@ -0,0 +1,230 @@
package nfs
import (
"encoding"
"encoding/binary"
"errors"
"fmt"
)
// RPCError provides the error interface for errors thrown by
// procedures to be transmitted over the XDR RPC channel
type RPCError interface {
// An RPCError is an `error` with this method
Error() string
// Code is the RPC Response code to send
Code() ResponseCode
// BinaryMarshaler is the on-wire representation of this error
encoding.BinaryMarshaler
}
// AuthStat is an enumeration of why authentication ahs failed
type AuthStat uint32
// AuthStat Codes
const (
AuthStatOK AuthStat = iota
AuthStatBadCred
AuthStatRejectedCred
AuthStatBadVerifier
AuthStatRejectedVerfier
AuthStatTooWeak
AuthStatInvalidResponse
AuthStatFailed
AuthStatKerbGeneric
AuthStatTimeExpire
AuthStatTktFile
AuthStatDecode
AuthStatNetAddr
AuthStatRPCGSSCredProblem
AuthStatRPCGSSCTXProblem
)
// AuthError is an RPCError
type AuthError struct {
AuthStat
}
// Code for AuthErrors is ResponseCodeAuthError
func (a *AuthError) Code() ResponseCode {
return ResponseCodeAuthError
}
// Error is a textual representaiton of the auth error. From the RFC
func (a *AuthError) Error() string {
switch a.AuthStat {
case AuthStatOK:
return "Auth Status: OK"
case AuthStatBadCred:
return "Auth Status: bad credential"
case AuthStatRejectedCred:
return "Auth Status: client must begin new session"
case AuthStatBadVerifier:
return "Auth Status: bad verifier"
case AuthStatRejectedVerfier:
return "Auth Status: verifier expired or replayed"
case AuthStatTooWeak:
return "Auth Status: rejected for security reasons"
case AuthStatInvalidResponse:
return "Auth Status: bogus response verifier"
case AuthStatFailed:
return "Auth Status: reason unknown"
case AuthStatKerbGeneric:
return "Auth Status: kerberos generic error"
case AuthStatTimeExpire:
return "Auth Status: time of credential expired"
case AuthStatTktFile:
return "Auth Status: problem with ticket file"
case AuthStatDecode:
return "Auth Status: can't decode authenticator"
case AuthStatNetAddr:
return "Auth Status: wrong net address in ticket"
case AuthStatRPCGSSCredProblem:
return "Auth Status: no credentials for user"
case AuthStatRPCGSSCTXProblem:
return "Auth Status: problem with context"
}
return "Auth Status: Unknown"
}
// MarshalBinary sends the specific auth status
func (a *AuthError) MarshalBinary() (data []byte, err error) {
var resp [4]byte
binary.LittleEndian.PutUint32(resp[:], uint32(a.AuthStat))
return resp[:], nil
}
// RPCMismatchError is an RPCError
type RPCMismatchError struct {
Low uint32
High uint32
}
// Code for RPCMismatchError is ResponseCodeRPCMismatch
func (r *RPCMismatchError) Code() ResponseCode {
return ResponseCodeRPCMismatch
}
func (r *RPCMismatchError) Error() string {
return fmt.Sprintf("RPC Mismatch: Expected version between %d and %d.", r.Low, r.High)
}
// MarshalBinary sends the specific rpc mismatch range
func (r *RPCMismatchError) MarshalBinary() (data []byte, err error) {
var resp [8]byte
binary.LittleEndian.PutUint32(resp[0:4], uint32(r.Low))
binary.LittleEndian.PutUint32(resp[4:8], uint32(r.High))
return resp[:], nil
}
// ResponseCodeProcUnavailableError is an RPCError
type ResponseCodeProcUnavailableError struct {
}
// Code for ResponseCodeProcUnavailableError
func (r *ResponseCodeProcUnavailableError) Code() ResponseCode {
return ResponseCodeProcUnavailable
}
func (r *ResponseCodeProcUnavailableError) Error() string {
return "The requested procedure is unexported"
}
// MarshalBinary - this error has no associated body
func (r *ResponseCodeProcUnavailableError) MarshalBinary() (data []byte, err error) {
return []byte{}, nil
}
// ResponseCodeSystemError is an RPCError
type ResponseCodeSystemError struct {
}
// Code for ResponseCodeSystemError
func (r *ResponseCodeSystemError) Code() ResponseCode {
return ResponseCodeSystemErr
}
func (r *ResponseCodeSystemError) Error() string {
return "memory allocation failure"
}
// MarshalBinary - this error has no associated body
func (r *ResponseCodeSystemError) MarshalBinary() (data []byte, err error) {
return []byte{}, nil
}
// basicErrorFormatter is the default error handler for response errors.
// if the error is already formatted, it is directly written. Otherwise,
// ResponseCodeSystemError is sent to the client.
func basicErrorFormatter(err error) RPCError {
var rpcErr RPCError
if errors.As(err, &rpcErr) {
return rpcErr
}
return &ResponseCodeSystemError{}
}
// NFSStatusError represents an error at the NFS level.
type NFSStatusError struct {
NFSStatus
WrappedErr error
}
// Error is The wrapped error
func (s *NFSStatusError) Error() string {
message := s.NFSStatus.String()
if s.WrappedErr != nil {
message = fmt.Sprintf("%s: %v", message, s.WrappedErr)
}
return message
}
// Code for NFS issues are successful RPC responses
func (s *NFSStatusError) Code() ResponseCode {
return ResponseCodeSuccess
}
// MarshalBinary - The binary form of the code.
func (s *NFSStatusError) MarshalBinary() (data []byte, err error) {
var resp [4]byte
binary.BigEndian.PutUint32(resp[0:4], uint32(s.NFSStatus))
return resp[:], nil
}
// Unwrap unpacks wrapped errors
func (s *NFSStatusError) Unwrap() error {
return s.WrappedErr
}
// StatusErrorWithBody is an NFS error with a payload.
type StatusErrorWithBody struct {
NFSStatusError
Body []byte
}
// MarshalBinary provides the wire format of the error response
func (s *StatusErrorWithBody) MarshalBinary() (data []byte, err error) {
head, err := s.NFSStatusError.MarshalBinary()
return append(head, s.Body...), err
}
// errFormatterWithBody appends a provided body to errors
func errFormatterWithBody(body []byte) func(err error) RPCError {
return func(err error) RPCError {
if nerr, ok := err.(*NFSStatusError); ok {
return &StatusErrorWithBody{*nerr, body[:]}
}
var rErr RPCError
if errors.As(err, &rErr) {
return rErr
}
return &ResponseCodeSystemError{}
}
}
var (
opAttrErrorBody = [4]byte{}
opAttrErrorFormatter = errFormatterWithBody(opAttrErrorBody[:])
wccDataErrorBody = [8]byte{}
wccDataErrorFormatter = errFormatterWithBody(wccDataErrorBody[:])
)

View file

@ -0,0 +1,52 @@
package main
import (
"context"
"fmt"
"net"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
nfshelper "git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
)
// ROFS is an intercepter for the filesystem indicating it should
// be read only. The undelrying billy.Memfs indicates it supports
// writing, but does not in implement billy.Change to support
// modification of permissions / modTimes, and as such cannot be
// used as RW system.
type ROFS struct {
nfs.Filesystem
}
// Capabilities exports the filesystem as readonly
func (ROFS) Capabilities() billy.Capability {
return billy.ReadCapability | billy.SeekCapability
}
func main() {
ctx := context.Background()
listener, err := net.Listen("tcp", ":0")
if err != nil {
fmt.Printf("Failed to listen: %v\n", err)
return
}
fmt.Printf("Server running at %s\n", listener.Addr())
mem := helpers.WrapBillyFS(memfs.New())
f, err := mem.Create(ctx, "hello.txt")
if err != nil {
fmt.Printf("Failed to create file: %v\n", err)
return
}
_, _ = f.Write(ctx, []byte("hello world"))
_ = f.Close(ctx)
handler := nfshelper.NewNullAuthHandler(ROFS{mem})
cacheHelper := nfshelper.NewCachingHandler(handler, 1024)
fmt.Printf("%v", nfs.Serve(listener, cacheHelper))
}

View file

@ -0,0 +1,38 @@
package main
import (
"os"
"time"
"github.com/go-git/go-billy/v5"
)
// NewChangeOSFS wraps billy osfs to add the change interface
func NewChangeOSFS(fs billy.Filesystem) billy.Filesystem {
return COS{fs}
}
// COS or OSFS + Change wraps a billy.FS to not fail the `Change` interface.
type COS struct {
billy.Filesystem
}
// Chmod changes mode
func (fs COS) Chmod(name string, mode os.FileMode) error {
return os.Chmod(fs.Join(fs.Root(), name), mode)
}
// Lchown changes ownership
func (fs COS) Lchown(name string, uid, gid int) error {
return os.Lchown(fs.Join(fs.Root(), name), uid, gid)
}
// Chown changes ownership
func (fs COS) Chown(name string, uid, gid int) error {
return os.Chown(fs.Join(fs.Root(), name), uid, gid)
}
// Chtimes changes access time
func (fs COS) Chtimes(name string, atime time.Time, mtime time.Time) error {
return os.Chtimes(fs.Join(fs.Root(), name), atime, mtime)
}

View file

@ -0,0 +1,28 @@
//go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris
package main
import (
"golang.org/x/sys/unix"
)
func (fs COS) Mknod(path string, mode uint32, major uint32, minor uint32) error {
dev := unix.Mkdev(major, minor)
return unix.Mknod(fs.Join(fs.Root(), path), mode, int(dev))
}
func (fs COS) Mkfifo(path string, mode uint32) error {
return unix.Mkfifo(fs.Join(fs.Root(), path), mode)
}
func (fs COS) Link(path string, link string) error {
return unix.Link(fs.Join(fs.Root(), path), link)
}
func (fs COS) Socket(path string) error {
fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
return err
}
return unix.Bind(fd, &unix.SockaddrUnix{Name: fs.Join(fs.Root(), path)})
}

View file

@ -0,0 +1,36 @@
package main
import (
"fmt"
"net"
"os"
nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
nfshelper "git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
osfs "github.com/go-git/go-billy/v5/osfs"
)
func main() {
port := ""
if len(os.Args) < 2 {
fmt.Printf("Usage: osnfs </path/to/folder> [port]\n")
return
} else if len(os.Args) == 3 {
port = os.Args[2]
}
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
fmt.Printf("Failed to listen: %v\n", err)
return
}
fmt.Printf("osnfs server running at %s\n", listener.Addr())
bfs := osfs.New(os.Args[1])
bfsPlusChange := helpers.WrapBillyFS(NewChangeOSFS(bfs))
handler := nfshelper.NewNullAuthHandler(bfsPlusChange)
cacheHelper := nfshelper.NewCachingHandler(handler, 1024)
fmt.Printf("%v", nfs.Serve(listener, cacheHelper))
}

View file

@ -0,0 +1,37 @@
package main
import (
"fmt"
"net"
"os"
"github.com/willscott/memphis"
nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
nfshelper "git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
)
func main() {
port := ""
if len(os.Args) < 2 {
fmt.Printf("Usage: osview </path/to/folder> [port]\n")
return
} else if len(os.Args) == 3 {
port = os.Args[2]
}
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
fmt.Printf("Failed to listen: %v\n", err)
return
}
fmt.Printf("Server running at %s\n", listener.Addr())
fs := memphis.FromOS(os.Args[1])
bfs := helpers.WrapBillyFS(fs.AsBillyFS(0, 0))
handler := nfshelper.NewNullAuthHandler(bfs)
cacheHelper := nfshelper.NewCachingHandler(handler, 1024)
fmt.Printf("%v", nfs.Serve(listener, cacheHelper))
}

377
pkg/go-nfs/file.go Normal file
View file

@ -0,0 +1,377 @@
package nfs
import (
"context"
"errors"
"hash/fnv"
"io"
"math"
"os"
"time"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/file"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
// FileAttribute holds metadata about a filesystem object
type FileAttribute struct {
Type FileType
FileMode uint32
Nlink uint32
UID uint32
GID uint32
Filesize uint64
Used uint64
SpecData [2]uint32
FSID uint64
Fileid uint64
Atime, Mtime, Ctime FileTime
}
// FileType represents a NFS File Type
type FileType uint32
// Enumeration of NFS FileTypes
const (
FileTypeRegular FileType = iota + 1
FileTypeDirectory
FileTypeBlock
FileTypeCharacter
FileTypeLink
FileTypeSocket
FileTypeFIFO
)
func (f FileType) String() string {
switch f {
case FileTypeRegular:
return "Regular"
case FileTypeDirectory:
return "Directory"
case FileTypeBlock:
return "Block Device"
case FileTypeCharacter:
return "Character Device"
case FileTypeLink:
return "Symbolic Link"
case FileTypeSocket:
return "Socket"
case FileTypeFIFO:
return "FIFO"
default:
return "Unknown"
}
}
// Mode provides the OS interpreted mode of the file attributes
func (f *FileAttribute) Mode() os.FileMode {
return os.FileMode(f.FileMode)
}
// FileCacheAttribute is the subset of FileAttribute used by
// wcc_attr
type FileCacheAttribute struct {
Filesize uint64
Mtime, Ctime FileTime
}
// AsCache provides the wcc view of the file attributes
func (f FileAttribute) AsCache() *FileCacheAttribute {
wcc := FileCacheAttribute{
Filesize: f.Filesize,
Mtime: f.Mtime,
Ctime: f.Ctime,
}
return &wcc
}
// ToFileAttribute creates an NFS fattr3 struct from an OS.FileInfo
func ToFileAttribute(info os.FileInfo, filePath string) *FileAttribute {
f := FileAttribute{}
m := info.Mode()
f.FileMode = uint32(m)
if info.IsDir() {
f.Type = FileTypeDirectory
} else if m&os.ModeSymlink != 0 {
f.Type = FileTypeLink
} else if m&os.ModeCharDevice != 0 {
f.Type = FileTypeCharacter
} else if m&os.ModeDevice != 0 {
f.Type = FileTypeBlock
} else if m&os.ModeSocket != 0 {
f.Type = FileTypeSocket
} else if m&os.ModeNamedPipe != 0 {
f.Type = FileTypeFIFO
} else {
f.Type = FileTypeRegular
}
// The number of hard links to the file.
f.Nlink = 1
if a := file.GetInfo(info); a != nil {
f.Nlink = a.Nlink
f.UID = a.UID
f.GID = a.GID
f.SpecData = [2]uint32{a.Major, a.Minor}
f.Fileid = a.Fileid
} else {
hasher := fnv.New64()
_, _ = hasher.Write([]byte(filePath))
f.Fileid = hasher.Sum64()
}
f.Filesize = uint64(info.Size())
f.Used = uint64(info.Size())
f.Atime = ToNFSTime(info.ModTime())
f.Mtime = f.Atime
f.Ctime = f.Atime
return &f
}
// tryStat attempts to create a FileAttribute from a path.
func tryStat(ctx context.Context, fs Filesystem, path []string) *FileAttribute {
fullPath := fs.Join(path...)
attrs, err := fs.Lstat(ctx, fullPath)
if err != nil || attrs == nil {
Log.Errorf("err loading attrs for %s: %v", fs.Join(path...), err)
return nil
}
return ToFileAttribute(attrs, fullPath)
}
// WriteWcc writes the `wcc_data` representation of an object.
func WriteWcc(writer io.Writer, pre *FileCacheAttribute, post *FileAttribute) error {
if pre == nil {
if err := xdr.Write(writer, uint32(0)); err != nil {
return err
}
} else {
if err := xdr.Write(writer, uint32(1)); err != nil {
return err
}
if err := xdr.Write(writer, *pre); err != nil {
return err
}
}
if post == nil {
if err := xdr.Write(writer, uint32(0)); err != nil {
return err
}
} else {
if err := xdr.Write(writer, uint32(1)); err != nil {
return err
}
if err := xdr.Write(writer, *post); err != nil {
return err
}
}
return nil
}
// WritePostOpAttrs writes the `post_op_attr` representation of a files attributes
func WritePostOpAttrs(writer io.Writer, post *FileAttribute) error {
if post == nil {
if err := xdr.Write(writer, uint32(0)); err != nil {
return err
}
} else {
if err := xdr.Write(writer, uint32(1)); err != nil {
return err
}
if err := xdr.Write(writer, *post); err != nil {
return err
}
}
return nil
}
// SetFileAttributes represents a command to update some metadata
// about a file.
type SetFileAttributes struct {
SetMode *uint32
SetUID *uint32
SetGID *uint32
SetSize *uint64
SetAtime *time.Time
SetMtime *time.Time
}
// Apply uses a `Change` implementation to set defined attributes on a
// provided file.
func (s *SetFileAttributes) Apply(ctx context.Context, changer Change, fs Filesystem, file string) error {
curOS, err := fs.Lstat(ctx, file)
if errors.Is(err, os.ErrNotExist) {
return &NFSStatusError{NFSStatusNoEnt, os.ErrNotExist}
} else if errors.Is(err, os.ErrPermission) {
return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
} else if err != nil {
return nil
}
curr := ToFileAttribute(curOS, file)
if s.SetMode != nil {
mode := os.FileMode(*s.SetMode) & os.ModePerm
if mode != curr.Mode().Perm() {
if changer == nil {
return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
}
if err := changer.Chmod(ctx, file, mode); err != nil {
if errors.Is(err, os.ErrPermission) {
return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
}
return err
}
}
}
if s.SetUID != nil || s.SetGID != nil {
euid := curr.UID
if s.SetUID != nil {
euid = *s.SetUID
}
egid := curr.GID
if s.SetGID != nil {
egid = *s.SetGID
}
if euid != curr.UID || egid != curr.GID {
if changer == nil {
return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
}
if err := changer.Lchown(ctx, file, int(euid), int(egid)); err != nil {
if errors.Is(err, os.ErrPermission) {
return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
}
return err
}
}
}
if s.SetSize != nil {
if curr.Mode()&os.ModeSymlink != 0 {
return &NFSStatusError{NFSStatusNotSupp, os.ErrInvalid}
}
fp, err := fs.OpenFile(ctx, file, os.O_WRONLY|os.O_EXCL, 0)
if errors.Is(err, os.ErrPermission) {
return &NFSStatusError{NFSStatusAccess, err}
} else if err != nil {
return err
}
if *s.SetSize > math.MaxInt64 {
return &NFSStatusError{NFSStatusInval, os.ErrInvalid}
}
if err := fp.Truncate(ctx, int64(*s.SetSize)); err != nil {
return err
}
if err := fp.Close(ctx); err != nil {
return err
}
}
if s.SetAtime != nil || s.SetMtime != nil {
atime := curr.Atime.Native()
if s.SetAtime != nil {
atime = s.SetAtime
}
mtime := curr.Mtime.Native()
if s.SetMtime != nil {
mtime = s.SetMtime
}
if atime != curr.Atime.Native() || mtime != curr.Mtime.Native() {
if changer == nil {
return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
}
if err := changer.Chtimes(ctx, file, *atime, *mtime); err != nil {
if errors.Is(err, os.ErrPermission) {
return &NFSStatusError{NFSStatusAccess, err}
}
return err
}
}
}
return nil
}
// Mode returns a mode if specified or the provided default mode.
func (s *SetFileAttributes) Mode(def os.FileMode) os.FileMode {
if s.SetMode != nil {
return os.FileMode(*s.SetMode) & os.ModePerm
}
return def
}
// ReadSetFileAttributes reads an sattr3 xdr stream into a go struct.
func ReadSetFileAttributes(r io.Reader) (*SetFileAttributes, error) {
attrs := SetFileAttributes{}
hasMode, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
if hasMode != 0 {
mode, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
attrs.SetMode = &mode
}
hasUID, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
if hasUID != 0 {
uid, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
attrs.SetUID = &uid
}
hasGID, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
if hasGID != 0 {
gid, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
attrs.SetGID = &gid
}
hasSize, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
if hasSize != 0 {
var size uint64
attrs.SetSize = &size
if err := xdr.Read(r, &size); err != nil {
return nil, err
}
}
aTime, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
if aTime == 1 {
now := time.Now()
attrs.SetAtime = &now
} else if aTime == 2 {
t := FileTime{}
if err := xdr.Read(r, &t); err != nil {
return nil, err
}
attrs.SetAtime = t.Native()
}
mTime, err := xdr.ReadUint32(r)
if err != nil {
return nil, err
}
if mTime == 1 {
now := time.Now()
attrs.SetMtime = &now
} else if mTime == 2 {
t := FileTime{}
if err := xdr.Read(r, &t); err != nil {
return nil, err
}
attrs.SetMtime = t.Native()
}
return &attrs, nil
}

17
pkg/go-nfs/file/file.go Normal file
View file

@ -0,0 +1,17 @@
package file
import "os"
type FileInfo struct {
Nlink uint32
UID uint32
GID uint32
Major uint32
Minor uint32
Fileid uint64
}
// GetInfo extracts some non-standardized items from the result of a Stat call.
func GetInfo(fi os.FileInfo) *FileInfo {
return getInfo(fi)
}

View file

@ -0,0 +1,24 @@
//go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris
package file
import (
"os"
"syscall"
"golang.org/x/sys/unix"
)
func getInfo(info os.FileInfo) *FileInfo {
fi := &FileInfo{}
if s, ok := info.Sys().(*syscall.Stat_t); ok {
fi.Nlink = uint32(s.Nlink)
fi.UID = s.Uid
fi.GID = s.Gid
fi.Major = unix.Major(uint64(s.Rdev))
fi.Minor = unix.Minor(uint64(s.Rdev))
fi.Fileid = s.Ino
return fi
}
return nil
}

View file

@ -0,0 +1,12 @@
//go:build windows
package file
import "os"
func getInfo(info os.FileInfo) *FileInfo {
// https://godoc.org/golang.org/x/sys/windows#GetFileInformationByHandle
// can be potentially used to populate Nlink
return nil
}

101
pkg/go-nfs/filesystem.go Normal file
View file

@ -0,0 +1,101 @@
package nfs
import (
"context"
"io"
"os"
"time"
"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
)
// FSStat returns metadata about a file system
type FSStat struct {
TotalSize uint64
FreeSize uint64
AvailableSize uint64
TotalFiles uint64
FreeFiles uint64
AvailableFiles uint64
// CacheHint is called "invarsec" in the nfs standard
CacheHint time.Duration
}
type Filesystem interface {
// Create creates the named file with mode 0666 (before umask), truncating
// it if it already exists. If successful, methods on the returned File can
// be used for I/O; the associated file descriptor has mode O_RDWR.
Create(ctx context.Context, filename string) (File, error)
// Open opens the named file for reading. If successful, methods on the
// returned file can be used for reading; the associated file descriptor has
// mode O_RDONLY.
Open(ctx context.Context, filename string) (File, error)
// OpenFile is the generalized open call; most users will use Open or Create
// instead. It opens the named file with specified flag (O_RDONLY etc.) and
// perm, (0666 etc.) if applicable. If successful, methods on the returned
// File can be used for I/O.
OpenFile(ctx context.Context, filename string, flag int, perm os.FileMode) (File, error)
// Stat returns a FileInfo describing the named file.
Stat(ctx context.Context, filename string) (os.FileInfo, error)
// Rename renames (moves) oldpath to newpath. If newpath already exists and
// is not a directory, Rename replaces it. OS-specific restrictions may
// apply when oldpath and newpath are in different directories.
Rename(ctx context.Context, oldpath, newpath string) error
// Remove removes the named file or directory.
Remove(ctx context.Context, filename string) error
// Join joins any number of path elements into a single path, adding a
// Separator if necessary. Join calls filepath.Clean on the result; in
// particular, all empty strings are ignored. On Windows, the result is a
// UNC path if and only if the first path element is a UNC path.
Join(elem ...string) string
// ReadDir reads the directory named by d(irname and returns a list of
// directory entries sorted by filename.
ReadDir(ctx context.Context, path string) ([]os.FileInfo, error)
// MkdirAll creates a directory named path, along with any necessary
// parents, and returns nil, or else returns an error. The permission bits
// perm are used for all directories that MkdirAll creates. If path is/
// already a directory, MkdirAll does nothing and returns nil.
MkdirAll(ctx context.Context, filename string, perm os.FileMode) error
// Lstat returns a FileInfo describing the named file. If the file is a
// symbolic link, the returned FileInfo describes the symbolic link. Lstat
// makes no attempt to follow the link.
Lstat(ctx context.Context, filename string) (os.FileInfo, error)
// Symlink creates a symbolic-link from link to target. target may be an
// absolute or relative path, and need not refer to an existing node.
// Parent directories of link are created as necessary.
Symlink(ctx context.Context, target, link string) error
// Readlink returns the target path of link.
Readlink(ctx context.Context, link string) (string, error)
}
type File interface {
// Name returns the name of the file as presented to Open.
Name() string
ctxio.Writer
ctxio.Reader
ctxio.ReaderAt
io.Seeker
ctxio.Closer
// Truncate the file.
Truncate(ctx context.Context, size int64) error
}
// Change abstract the FileInfo change related operations in a storage-agnostic
// interface as an extension to the Basic interface
type Change interface {
// Chmod changes the mode of the named file to mode. If the file is a
// symbolic link, it changes the mode of the link's target.
Chmod(ctx context.Context, name string, mode os.FileMode) error
// Lchown changes the numeric uid and gid of the named file. If the file is
// a symbolic link, it changes the uid and gid of the link itself.
Lchown(ctx context.Context, name string, uid, gid int) error
// Chtimes changes the access and modification times of the named file,
// similar to the Unix utime() or utimes() functions.
//
// The underlying filesystem may truncate or round the values to a less
// precise time unit.
Chtimes(ctx context.Context, name string, atime time.Time, mtime time.Time) error
}

52
pkg/go-nfs/handler.go Normal file
View file

@ -0,0 +1,52 @@
package nfs
import (
"context"
"io/fs"
"net"
"git.kmsign.ru/royalcat/tstor/pkg/ctxbilly"
)
// Handler represents the interface of the file system / vfs being exposed over NFS
type Handler interface {
// Required methods
Mount(context.Context, net.Conn, MountRequest) (MountStatus, Filesystem, []AuthFlavor)
// Change can return 'nil' if filesystem is read-only
// If the returned value can be cast to `UnixChange`, mknod and link RPCs will be available.
Change(Filesystem) Change
// Optional methods - generic helpers or trivial implementations can be sufficient depending on use case.
// Fill in information about a file system's free space.
FSStat(context.Context, Filesystem, *FSStat) error
// represent file objects as opaque references
// Can be safely implemented via helpers/cachinghandler.
ToHandle(fs Filesystem, path []string) []byte
FromHandle(fh []byte) (Filesystem, []string, error)
InvalidateHandle(Filesystem, []byte) error
// How many handles can be safely maintained by the handler.
HandleLimit() int
}
// UnixChange extends the billy `Change` interface with support for special files.
type UnixChange interface {
ctxbilly.Change
Mknod(ctx context.Context, path string, mode uint32, major uint32, minor uint32) error
Mkfifo(ctx context.Context, path string, mode uint32) error
Socket(ctx context.Context, path string) error
Link(ctx context.Context, path string, link string) error
}
// CachingHandler represents the optional caching work that a user may wish to over-ride with
// their own implementations, but which can be otherwise provided through defaults.
type CachingHandler interface {
VerifierFor(path string, contents []fs.FileInfo) uint64
// fs.FileInfo needs to be sorted by Name(), nil in case of a cache-miss
DataForVerifier(path string, verifier uint64) []fs.FileInfo
}

View file

@ -0,0 +1,157 @@
package helpers
import (
"context"
"io/fs"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
"github.com/go-git/go-billy/v5"
)
func WrapBillyFS(bf billy.Filesystem) nfs.Filesystem {
return &wrapFS{
Filesystem: bf,
}
}
type wrapFS struct {
billy.Filesystem
}
var _ nfs.Filesystem = (*wrapFS)(nil)
// Create implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Create of MemFS.Filesystem.
func (m *wrapFS) Create(ctx context.Context, filename string) (nfs.File, error) {
bf, err := m.Filesystem.Create(filename)
if err != nil {
return nil, err
}
return &wrapFile{bf}, nil
}
// Lstat implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Lstat of MemFS.Filesystem.
func (m *wrapFS) Lstat(ctx context.Context, filename string) (fs.FileInfo, error) {
return m.Filesystem.Lstat(filename)
}
// MkdirAll implements Filesystem.
// Subtle: this method shadows the method (Filesystem).MkdirAll of MemFS.Filesystem.
func (m *wrapFS) MkdirAll(ctx context.Context, filename string, perm fs.FileMode) error {
return m.Filesystem.MkdirAll(filename, perm)
}
// Open implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Open of MemFS.Filesystem.
func (m *wrapFS) Open(ctx context.Context, filename string) (nfs.File, error) {
bf, err := m.Filesystem.Open(filename)
if err != nil {
return nil, err
}
return WrapFile(bf), nil
}
// OpenFile implements Filesystem.
// Subtle: this method shadows the method (Filesystem).OpenFile of MemFS.Filesystem.
func (m *wrapFS) OpenFile(ctx context.Context, filename string, flag int, perm fs.FileMode) (nfs.File, error) {
bf, err := m.Filesystem.OpenFile(filename, flag, perm)
if err != nil {
return nil, err
}
return WrapFile(bf), nil
}
// ReadDir implements Filesystem.
// Subtle: this method shadows the method (Filesystem).ReadDir of MemFS.Filesystem.
func (m *wrapFS) ReadDir(ctx context.Context, path string) ([]fs.FileInfo, error) {
return m.Filesystem.ReadDir(path)
}
// Readlink implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Readlink of MemFS.Filesystem.
func (m *wrapFS) Readlink(ctx context.Context, link string) (string, error) {
return m.Filesystem.Readlink(link)
}
// Remove implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Remove of MemFS.Filesystem.
func (m *wrapFS) Remove(ctx context.Context, filename string) error {
return m.Filesystem.Remove(filename)
}
// Rename implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Rename of MemFS.Filesystem.
func (m *wrapFS) Rename(ctx context.Context, oldpath string, newpath string) error {
return m.Filesystem.Rename(oldpath, newpath)
}
// Stat implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Stat of MemFS.Filesystem.
func (m *wrapFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
return m.Filesystem.Stat(filename)
}
// Symlink implements Filesystem.
// Subtle: this method shadows the method (Filesystem).Symlink of MemFS.Filesystem.
func (m *wrapFS) Symlink(ctx context.Context, target string, link string) error {
return m.Filesystem.Symlink(target, link)
}
func WrapFile(bf billy.File) nfs.File {
return &wrapFile{File: bf}
}
type wrapFile struct {
billy.File
}
var _ nfs.File = (*wrapFile)(nil)
// Close implements File.
// Subtle: this method shadows the method (File).Close of MemFile.File.
func (m *wrapFile) Close(ctx context.Context) error {
return m.File.Close()
}
// Lock implements File.
// Subtle: this method shadows the method (File).Lock of MemFile.File.
func (m *wrapFile) Lock() error {
return m.File.Lock()
}
// Name implements File.
// Subtle: this method shadows the method (File).Name of MemFile.File.
func (m *wrapFile) Name() string {
return m.File.Name()
}
// Truncate implements File.
// Subtle: this method shadows the method (File).Truncate of memFile.File.
func (m *wrapFile) Truncate(ctx context.Context, size int64) error {
return m.File.Truncate(size)
}
// Read implements File.
// Subtle: this method shadows the method (File).Read of MemFile.File.
func (m *wrapFile) Read(ctx context.Context, p []byte) (n int, err error) {
return m.File.Read(p)
}
// ReadAt implements File.
// Subtle: this method shadows the method (File).ReadAt of MemFile.File.
func (m *wrapFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
return m.File.ReadAt(p, off)
}
// Unlock implements File.
// Subtle: this method shadows the method (File).Unlock of MemFile.File.
func (m *wrapFile) Unlock() error {
return m.File.Unlock()
}
// Write implements File.
// Subtle: this method shadows the method (File).Write of MemFile.File.
func (m *wrapFile) Write(ctx context.Context, p []byte) (n int, err error) {
return m.File.Write(p)
}

View file

@ -0,0 +1,198 @@
package helpers
import (
"crypto/sha256"
"encoding/binary"
"io/fs"
"reflect"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
"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 nfs.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 nfs.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) (nfs.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)
}
}
if ok {
newP := make([]string, len(f.p))
copy(newP, f.p)
return f.f, newP, nil
}
}
return nil, []string{}, &nfs.NFSStatusError{NFSStatus: nfs.NFSStatusStale}
}
func (c *CachingHandler) searchReverseCache(f nfs.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 nfs.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
}

View file

@ -0,0 +1,414 @@
// Package memfs is a variant of "github.com/go-git/go-billy/v5/memfs" with
// stable mtimes for items.
package memfs
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/helper/chroot"
"github.com/go-git/go-billy/v5/util"
)
const separator = filepath.Separator
// Memory a very convenient filesystem based on memory files
type Memory struct {
s *storage
}
// New returns a new Memory filesystem.
func New() billy.Filesystem {
fs := &Memory{s: newStorage()}
return chroot.New(fs, string(separator))
}
func (fs *Memory) Create(filename string) (billy.File, error) {
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
}
func (fs *Memory) Open(filename string) (billy.File, error) {
return fs.OpenFile(filename, os.O_RDONLY, 0)
}
func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
f, has := fs.s.Get(filename)
if !has {
if !isCreate(flag) {
return nil, os.ErrNotExist
}
var err error
f, err = fs.s.New(filename, perm, flag)
if err != nil {
return nil, err
}
} else {
if isExclusive(flag) {
return nil, os.ErrExist
}
if target, isLink := fs.resolveLink(filename, f); isLink {
return fs.OpenFile(target, flag, perm)
}
}
if f.mode.IsDir() {
return nil, fmt.Errorf("cannot open directory: %s", filename)
}
return f.Duplicate(filename, perm, flag), nil
}
func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
if !isSymlink(f.mode) {
return fullpath, false
}
target = string(f.content.bytes)
if !isAbs(target) {
target = fs.Join(filepath.Dir(fullpath), target)
}
return target, true
}
// On Windows OS, IsAbs validates if a path is valid based on if stars with a
// unit (eg.: `C:\`) to assert that is absolute, but in this mem implementation
// any path starting by `separator` is also considered absolute.
func isAbs(path string) bool {
return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator))
}
func (fs *Memory) Stat(filename string) (os.FileInfo, error) {
f, has := fs.s.Get(filename)
if !has {
return nil, os.ErrNotExist
}
fi, _ := f.Stat()
var err error
if target, isLink := fs.resolveLink(filename, f); isLink {
fi, err = fs.Stat(target)
if err != nil {
return nil, err
}
}
// the name of the file should always the name of the stated file, so we
// overwrite the Stat returned from the storage with it, since the
// filename may belong to a link.
fi.(*fileInfo).name = filepath.Base(filename)
return fi, nil
}
func (fs *Memory) Lstat(filename string) (os.FileInfo, error) {
f, has := fs.s.Get(filename)
if !has {
return nil, os.ErrNotExist
}
return f.Stat()
}
type ByName []os.FileInfo
func (a ByName) Len() int { return len(a) }
func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) {
if f, has := fs.s.Get(path); has {
if target, isLink := fs.resolveLink(path, f); isLink {
return fs.ReadDir(target)
}
} else {
return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
}
var entries []os.FileInfo
for _, f := range fs.s.Children(path) {
fi, _ := f.Stat()
entries = append(entries, fi)
}
sort.Sort(ByName(entries))
return entries, nil
}
func (fs *Memory) MkdirAll(path string, perm os.FileMode) error {
_, err := fs.s.New(path, perm|os.ModeDir, 0)
return err
}
func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) {
return util.TempFile(fs, dir, prefix)
}
func (fs *Memory) Rename(from, to string) error {
return fs.s.Rename(from, to)
}
func (fs *Memory) Remove(filename string) error {
return fs.s.Remove(filename)
}
func (fs *Memory) Join(elem ...string) string {
return filepath.Join(elem...)
}
func (fs *Memory) Symlink(target, link string) error {
_, err := fs.Stat(link)
if err == nil {
return os.ErrExist
}
if !os.IsNotExist(err) {
return err
}
return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink)
}
func (fs *Memory) Readlink(link string) (string, error) {
f, has := fs.s.Get(link)
if !has {
return "", os.ErrNotExist
}
if !isSymlink(f.mode) {
return "", &os.PathError{
Op: "readlink",
Path: link,
Err: fmt.Errorf("not a symlink"),
}
}
return string(f.content.bytes), nil
}
// Capabilities implements the Capable interface.
func (fs *Memory) Capabilities() billy.Capability {
return billy.WriteCapability |
billy.ReadCapability |
billy.ReadAndWriteCapability |
billy.SeekCapability |
billy.TruncateCapability
}
type file struct {
name string
content *content
position int64
flag int
mode os.FileMode
mtime time.Time
isClosed bool
}
func (f *file) Name() string {
return f.name
}
func (f *file) Read(b []byte) (int, error) {
n, err := f.ReadAt(b, f.position)
f.position += int64(n)
if err == io.EOF && n != 0 {
err = nil
}
return n, err
}
func (f *file) ReadAt(b []byte, off int64) (int, error) {
if f.isClosed {
return 0, os.ErrClosed
}
if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
return 0, errors.New("read not supported")
}
n, err := f.content.ReadAt(b, off)
return n, err
}
func (f *file) Seek(offset int64, whence int) (int64, error) {
if f.isClosed {
return 0, os.ErrClosed
}
switch whence {
case io.SeekCurrent:
f.position += offset
case io.SeekStart:
f.position = offset
case io.SeekEnd:
f.position = int64(f.content.Len()) + offset
}
return f.position, nil
}
func (f *file) Write(p []byte) (int, error) {
return f.WriteAt(p, f.position)
}
func (f *file) WriteAt(p []byte, off int64) (int, error) {
if f.isClosed {
return 0, os.ErrClosed
}
if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
return 0, errors.New("write not supported")
}
n, err := f.content.WriteAt(p, off)
f.position = off + int64(n)
f.mtime = time.Now()
return n, err
}
func (f *file) Close() error {
if f.isClosed {
return os.ErrClosed
}
f.isClosed = true
return nil
}
func (f *file) Truncate(size int64) error {
if size < int64(len(f.content.bytes)) {
f.content.bytes = f.content.bytes[:size]
} else if more := int(size) - len(f.content.bytes); more > 0 {
f.content.bytes = append(f.content.bytes, make([]byte, more)...)
}
f.mtime = time.Now()
return nil
}
func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File {
new := &file{
name: filename,
content: f.content,
mode: mode,
flag: flag,
mtime: time.Now(),
}
if isTruncate(flag) {
new.content.Truncate()
}
if isAppend(flag) {
new.position = int64(new.content.Len())
}
return new
}
func (f *file) Stat() (os.FileInfo, error) {
return &fileInfo{
name: f.Name(),
mode: f.mode,
size: f.content.Len(),
mtime: f.mtime,
}, nil
}
// Lock is a no-op in memfs.
func (f *file) Lock() error {
return nil
}
// Unlock is a no-op in memfs.
func (f *file) Unlock() error {
return nil
}
type fileInfo struct {
name string
size int
mode os.FileMode
mtime time.Time
}
func (fi *fileInfo) Name() string {
return fi.name
}
func (fi *fileInfo) Size() int64 {
return int64(fi.size)
}
func (fi *fileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi *fileInfo) ModTime() time.Time {
return fi.mtime
}
func (fi *fileInfo) IsDir() bool {
return fi.mode.IsDir()
}
func (*fileInfo) Sys() interface{} {
return nil
}
func (c *content) Truncate() {
c.bytes = make([]byte, 0)
}
func (c *content) Len() int {
return len(c.bytes)
}
func isCreate(flag int) bool {
return flag&os.O_CREATE != 0
}
func isExclusive(flag int) bool {
return flag&os.O_EXCL != 0
}
func isAppend(flag int) bool {
return flag&os.O_APPEND != 0
}
func isTruncate(flag int) bool {
return flag&os.O_TRUNC != 0
}
func isReadAndWrite(flag int) bool {
return flag&os.O_RDWR != 0
}
func isReadOnly(flag int) bool {
return flag == os.O_RDONLY
}
func isWriteOnly(flag int) bool {
return flag&os.O_WRONLY != 0
}
func isSymlink(m os.FileMode) bool {
return m&os.ModeSymlink != 0
}

View file

@ -0,0 +1,243 @@
package memfs
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type storage struct {
files map[string]*file
children map[string]map[string]*file
}
func newStorage() *storage {
return &storage{
files: make(map[string]*file, 0),
children: make(map[string]map[string]*file, 0),
}
}
func (s *storage) Has(path string) bool {
path = clean(path)
_, ok := s.files[path]
return ok
}
func (s *storage) New(path string, mode os.FileMode, flag int) (*file, error) {
path = clean(path)
if s.Has(path) {
if !s.MustGet(path).mode.IsDir() {
return nil, fmt.Errorf("file already exists %q", path)
}
return nil, nil
}
name := filepath.Base(path)
f := &file{
name: name,
content: &content{name: name},
mode: mode,
flag: flag,
mtime: time.Now(),
}
s.files[path] = f
if err := s.createParent(path, mode, f); err != nil {
return nil, err
}
return f, nil
}
func (s *storage) createParent(path string, mode os.FileMode, f *file) error {
base := filepath.Dir(path)
base = clean(base)
if f.Name() == string(separator) {
return nil
}
if _, err := s.New(base, mode.Perm()|os.ModeDir, 0); err != nil {
return err
}
if _, ok := s.children[base]; !ok {
s.children[base] = make(map[string]*file, 0)
}
s.children[base][f.Name()] = f
return nil
}
func (s *storage) Children(path string) []*file {
path = clean(path)
l := make([]*file, 0)
for _, f := range s.children[path] {
l = append(l, f)
}
return l
}
func (s *storage) MustGet(path string) *file {
f, ok := s.Get(path)
if !ok {
panic(fmt.Errorf("couldn't find %q", path))
}
return f
}
func (s *storage) Get(path string) (*file, bool) {
path = clean(path)
if !s.Has(path) {
return nil, false
}
file, ok := s.files[path]
return file, ok
}
func (s *storage) Rename(from, to string) error {
from = clean(from)
to = clean(to)
if !s.Has(from) {
return os.ErrNotExist
}
move := [][2]string{{from, to}}
for pathFrom := range s.files {
if pathFrom == from || !strings.HasPrefix(pathFrom, from) {
continue
}
rel, _ := filepath.Rel(from, pathFrom)
pathTo := filepath.Join(to, rel)
move = append(move, [2]string{pathFrom, pathTo})
}
for _, ops := range move {
from := ops[0]
to := ops[1]
if err := s.move(from, to); err != nil {
return err
}
}
return nil
}
func (s *storage) move(from, to string) error {
s.files[to] = s.files[from]
s.files[to].name = filepath.Base(to)
s.children[to] = s.children[from]
defer func() {
delete(s.children, from)
delete(s.files, from)
delete(s.children[filepath.Dir(from)], filepath.Base(from))
}()
return s.createParent(to, 0644, s.files[to])
}
func (s *storage) Remove(path string) error {
path = clean(path)
f, has := s.Get(path)
if !has {
return os.ErrNotExist
}
if f.mode.IsDir() && len(s.children[path]) != 0 {
return fmt.Errorf("dir: %s contains files", path)
}
base, file := filepath.Split(path)
base = filepath.Clean(base)
delete(s.children[base], file)
delete(s.files, path)
return nil
}
func clean(path string) string {
return filepath.Clean(filepath.FromSlash(path))
}
type content struct {
name string
bytes []byte
m sync.RWMutex
}
func (c *content) WriteAt(p []byte, off int64) (int, error) {
if off < 0 {
return 0, &os.PathError{
Op: "writeat",
Path: c.name,
Err: errors.New("negative offset"),
}
}
c.m.Lock()
prev := len(c.bytes)
diff := int(off) - prev
if diff > 0 {
c.bytes = append(c.bytes, make([]byte, diff)...)
}
c.bytes = append(c.bytes[:off], p...)
if len(c.bytes) < prev {
c.bytes = c.bytes[:prev]
}
c.m.Unlock()
return len(p), nil
}
func (c *content) ReadAt(b []byte, off int64) (n int, err error) {
if off < 0 {
return 0, &os.PathError{
Op: "readat",
Path: c.name,
Err: errors.New("negative offset"),
}
}
c.m.RLock()
size := int64(len(c.bytes))
if off >= size {
c.m.RUnlock()
return 0, io.EOF
}
l := int64(len(b))
if off+l > size {
l = size - off
}
btr := c.bytes[off : off+l]
n = copy(b, btr)
if len(btr) < len(b) {
err = io.EOF
}
c.m.RUnlock()
return
}

View file

@ -0,0 +1,59 @@
package helpers
import (
"context"
"net"
"git.kmsign.ru/royalcat/tstor/pkg/ctxbilly"
nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
)
// NewNullAuthHandler creates a handler for the provided filesystem
func NewNullAuthHandler(fs nfs.Filesystem) nfs.Handler {
return &NullAuthHandler{fs}
}
// NullAuthHandler returns a NFS backing that exposes a given file system in response to all mount requests.
type NullAuthHandler struct {
fs nfs.Filesystem
}
// Mount backs Mount RPC Requests, allowing for access control policies.
func (h *NullAuthHandler) Mount(ctx context.Context, conn net.Conn, req nfs.MountRequest) (status nfs.MountStatus, hndl nfs.Filesystem, auths []nfs.AuthFlavor) {
status = nfs.MountStatusOk
hndl = h.fs
auths = []nfs.AuthFlavor{nfs.AuthFlavorNull}
return
}
// Change provides an interface for updating file attributes.
func (h *NullAuthHandler) Change(fs nfs.Filesystem) nfs.Change {
if c, ok := h.fs.(ctxbilly.Change); ok {
return c
}
return nil
}
// FSStat provides information about a filesystem.
func (h *NullAuthHandler) FSStat(ctx context.Context, f nfs.Filesystem, s *nfs.FSStat) error {
return nil
}
// ToHandle handled by CachingHandler
func (h *NullAuthHandler) ToHandle(f nfs.Filesystem, s []string) []byte {
return []byte{}
}
// FromHandle handled by CachingHandler
func (h *NullAuthHandler) FromHandle([]byte) (nfs.Filesystem, []string, error) {
return nil, []string{}, nil
}
func (c *NullAuthHandler) InvalidateHandle(nfs.Filesystem, []byte) error {
return nil
}
// HandleLImit handled by cachingHandler
func (h *NullAuthHandler) HandleLimit() int {
return -1
}

216
pkg/go-nfs/log.go Normal file
View file

@ -0,0 +1,216 @@
package nfs
import (
"fmt"
"log"
"os"
)
var (
Log Logger = &DefaultLogger{}
)
type LogLevel int
const (
PanicLevel LogLevel = iota
FatalLevel
ErrorLevel
WarnLevel
InfoLevel
DebugLevel
TraceLevel
panicLevelStr string = "[PANIC] "
fatalLevelStr string = "[FATAL] "
errorLevelStr string = "[ERROR] "
warnLevelStr string = "[WARN] "
infoLevelStr string = "[INFO] "
debugLevelStr string = "[DEBUG] "
traceLevelStr string = "[TRACE] "
)
type Logger interface {
SetLevel(level LogLevel)
GetLevel() LogLevel
ParseLevel(level string) (LogLevel, error)
Panic(args ...interface{})
Fatal(args ...interface{})
Error(args ...interface{})
Warn(args ...interface{})
Info(args ...interface{})
Debug(args ...interface{})
Trace(args ...interface{})
Print(args ...interface{})
Panicf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Warnf(format string, args ...interface{})
Infof(format string, args ...interface{})
Debugf(format string, args ...interface{})
Tracef(format string, args ...interface{})
Printf(format string, args ...interface{})
}
type DefaultLogger struct {
Level LogLevel
}
func SetLogger(logger Logger) {
Log = logger
}
func init() {
if os.Getenv("LOG_LEVEL") != "" {
if level, err := Log.ParseLevel(os.Getenv("LOG_LEVEL")); err == nil {
Log.SetLevel(level)
}
} else {
// set default log level to info
Log.SetLevel(InfoLevel)
}
}
func (l *DefaultLogger) GetLevel() LogLevel {
return l.Level
}
func (l *DefaultLogger) SetLevel(level LogLevel) {
l.Level = level
}
func (l *DefaultLogger) ParseLevel(level string) (LogLevel, error) {
switch level {
case "panic":
return PanicLevel, nil
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
case "trace":
return TraceLevel, nil
}
var ll LogLevel
return ll, fmt.Errorf("invalid log level %q", level)
}
func (l *DefaultLogger) Panic(args ...interface{}) {
if l.Level < PanicLevel {
return
}
args = append([]interface{}{panicLevelStr}, args...)
log.Print(args...)
}
func (l *DefaultLogger) Panicf(format string, args ...interface{}) {
if l.Level < PanicLevel {
return
}
log.Printf(panicLevelStr+format, args...)
}
func (l *DefaultLogger) Fatal(args ...interface{}) {
if l.Level < FatalLevel {
return
}
args = append([]interface{}{fatalLevelStr}, args...)
log.Print(args...)
}
func (l *DefaultLogger) Fatalf(format string, args ...interface{}) {
if l.Level < FatalLevel {
return
}
log.Printf(fatalLevelStr+format, args...)
}
func (l *DefaultLogger) Error(args ...interface{}) {
if l.Level < ErrorLevel {
return
}
args = append([]interface{}{errorLevelStr}, args...)
log.Print(args...)
}
func (l *DefaultLogger) Errorf(format string, args ...interface{}) {
if l.Level < ErrorLevel {
return
}
log.Printf(errorLevelStr+format, args...)
}
func (l *DefaultLogger) Warn(args ...interface{}) {
if l.Level < WarnLevel {
return
}
args = append([]interface{}{warnLevelStr}, args...)
log.Print(args...)
}
func (l *DefaultLogger) Warnf(format string, args ...interface{}) {
if l.Level < WarnLevel {
return
}
log.Printf(warnLevelStr+format, args...)
}
func (l *DefaultLogger) Info(args ...interface{}) {
if l.Level < InfoLevel {
return
}
args = append([]interface{}{infoLevelStr}, args...)
log.Print(args...)
}
func (l *DefaultLogger) Infof(format string, args ...interface{}) {
if l.Level < InfoLevel {
return
}
log.Printf(infoLevelStr+format, args...)
}
func (l *DefaultLogger) Debug(args ...interface{}) {
if l.Level < DebugLevel {
return
}
args = append([]interface{}{debugLevelStr}, args...)
log.Print(args...)
}
func (l *DefaultLogger) Debugf(format string, args ...interface{}) {
if l.Level < DebugLevel {
return
}
log.Printf(debugLevelStr+format, args...)
}
func (l *DefaultLogger) Trace(args ...interface{}) {
if l.Level < TraceLevel {
return
}
args = append([]interface{}{traceLevelStr}, args...)
log.Print(args...)
}
func (l *DefaultLogger) Tracef(format string, args ...interface{}) {
if l.Level < TraceLevel {
return
}
log.Printf(traceLevelStr+format, args...)
}
func (l *DefaultLogger) Print(args ...interface{}) {
log.Print(args...)
}
func (l *DefaultLogger) Printf(format string, args ...interface{}) {
log.Printf(format, args...)
}

58
pkg/go-nfs/mount.go Normal file
View file

@ -0,0 +1,58 @@
package nfs
import (
"bytes"
"context"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
const (
mountServiceID = 100005
)
func init() {
_ = RegisterMessageHandler(mountServiceID, uint32(MountProcNull), onMountNull)
_ = RegisterMessageHandler(mountServiceID, uint32(MountProcMount), onMount)
_ = RegisterMessageHandler(mountServiceID, uint32(MountProcUmnt), onUMount)
}
func onMountNull(ctx context.Context, w *response, userHandle Handler) error {
return w.writeHeader(ResponseCodeSuccess)
}
func onMount(ctx context.Context, w *response, userHandle Handler) error {
// TODO: auth check.
dirpath, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return err
}
mountReq := MountRequest{Header: w.req.Header, Dirpath: dirpath}
status, handle, flavors := userHandle.Mount(ctx, w.conn, mountReq)
if err := w.writeHeader(ResponseCodeSuccess); err != nil {
return err
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(status)); err != nil {
return err
}
rootHndl := userHandle.ToHandle(handle, []string{})
if status == MountStatusOk {
_ = xdr.Write(writer, rootHndl)
_ = xdr.Write(writer, flavors)
}
return w.Write(writer.Bytes())
}
func onUMount(ctx context.Context, w *response, userHandle Handler) error {
_, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return err
}
return w.writeHeader(ResponseCodeSuccess)
}

View file

@ -0,0 +1,90 @@
package nfs
import (
"github.com/willscott/go-nfs-client/nfs/rpc"
)
// FHSize is the maximum size of a FileHandle
const FHSize = 64
// MNTNameLen is the maximum size of a mount name
const MNTNameLen = 255
// MntPathLen is the maximum size of a mount path
const MntPathLen = 1024
// FileHandle maps to a fhandle3
type FileHandle []byte
// MountStatus defines the response to the Mount Procedure
type MountStatus uint32
// MountStatus Codes
const (
MountStatusOk MountStatus = 0
MountStatusErrPerm MountStatus = 1
MountStatusErrNoEnt MountStatus = 2
MountStatusErrIO MountStatus = 5
MountStatusErrAcces MountStatus = 13
MountStatusErrNotDir MountStatus = 20
MountStatusErrInval MountStatus = 22
MountStatusErrNameTooLong MountStatus = 63
MountStatusErrNotSupp MountStatus = 10004
MountStatusErrServerFault MountStatus = 10006
)
// MountProcedure is the valid RPC calls for the mount service.
type MountProcedure uint32
// MountProcedure Codes
const (
MountProcNull MountProcedure = iota
MountProcMount
MountProcDump
MountProcUmnt
MountProcUmntAll
MountProcExport
)
func (m MountProcedure) String() string {
switch m {
case MountProcNull:
return "Null"
case MountProcMount:
return "Mount"
case MountProcDump:
return "Dump"
case MountProcUmnt:
return "Umnt"
case MountProcUmntAll:
return "UmntAll"
case MountProcExport:
return "Export"
default:
return "Unknown"
}
}
// AuthFlavor is a form of authentication, per rfc1057 section 7.2
type AuthFlavor uint32
// AuthFlavor Codes
const (
AuthFlavorNull AuthFlavor = 0
AuthFlavorUnix AuthFlavor = 1
AuthFlavorShort AuthFlavor = 2
AuthFlavorDES AuthFlavor = 3
)
// MountRequest contains the format of a client request to open a mount.
type MountRequest struct {
rpc.Header
Dirpath []byte
}
// MountResponse is the server's response with status `MountStatusOk`
type MountResponse struct {
rpc.Header
FileHandle
AuthFlavors []int
}

38
pkg/go-nfs/nfs.go Normal file
View file

@ -0,0 +1,38 @@
package nfs
import (
"context"
)
const (
nfsServiceID = 100003
)
func init() {
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureNull), onNull) // 0
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureGetAttr), onGetAttr) // 1
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureSetAttr), onSetAttr) // 2
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureLookup), onLookup) // 3
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureAccess), onAccess) // 4
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadlink), onReadLink) // 5
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRead), onRead) // 6
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureWrite), onWrite) // 7
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureCreate), onCreate) // 8
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureMkDir), onMkdir) // 9
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureSymlink), onSymlink) // 10
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureMkNod), onMknod) // 11
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRemove), onRemove) // 12
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRmDir), onRmDir) // 13
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRename), onRename) // 14
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureLink), onLink) // 15
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadDir), onReadDir) // 16
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadDirPlus), onReadDirPlus) // 17
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureFSStat), onFSStat) // 18
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureFSInfo), onFSInfo) // 19
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedurePathConf), onPathConf) // 20
_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureCommit), onCommit) // 21
}
func onNull(ctx context.Context, w *response, userHandle Handler) error {
return w.Write([]byte{})
}

View file

@ -0,0 +1,45 @@
package nfs
import (
"bytes"
"context"
billy "github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func onAccess(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = opAttrErrorFormatter
roothandle, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(roothandle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
mask, err := xdr.ReadUint32(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
mask = mask & (1 | 2 | 0x20)
}
if err := xdr.Write(writer, mask); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,51 @@
package nfs
import (
"bytes"
"context"
"os"
billy "github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
// onCommit - note this is a no-op, as we always push writes to the backing store.
func onCommit(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
handle, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
// The conn will drain the unread offset and count arguments.
fs, path, err := userHandle.FromHandle(handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
return &NFSStatusError{NFSStatusServerFault, os.ErrPermission}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return err
}
// no pre-op cache data.
if err := xdr.Write(writer, uint32(0)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// write the 8 bytes of write verification.
if err := xdr.Write(writer, w.Server.ID); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

125
pkg/go-nfs/nfs_oncreate.go Normal file
View file

@ -0,0 +1,125 @@
package nfs
import (
"bytes"
"context"
"os"
billy "github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
const (
createModeUnchecked = 0
createModeGuarded = 1
createModeExclusive = 2
)
func onCreate(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
obj := DirOpArg{}
err := xdr.Read(w.req.Body, &obj)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
how, err := xdr.ReadUint32(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
var attrs *SetFileAttributes
if how == createModeUnchecked || how == createModeGuarded {
sattr, err := ReadSetFileAttributes(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
attrs = sattr
} else if how == createModeExclusive {
// read createverf3
var verf [8]byte
if err := xdr.Read(w.req.Body, &verf); err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
Log.Errorf("failing create to indicate lack of support for 'exclusive' mode.")
// TODO: support 'exclusive' mode.
return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
} else {
// invalid
return &NFSStatusError{NFSStatusNotSupp, os.ErrInvalid}
}
fs, path, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
}
if len(string(obj.Filename)) > PathNameMax {
return &NFSStatusError{NFSStatusNameTooLong, nil}
}
newFile := append(path, string(obj.Filename))
newFilePath := fs.Join(newFile...)
if s, err := fs.Stat(ctx, newFilePath); err == nil {
if s.IsDir() {
return &NFSStatusError{NFSStatusExist, nil}
}
if how == createModeGuarded {
return &NFSStatusError{NFSStatusExist, os.ErrPermission}
}
} else {
if s, err := fs.Stat(ctx, fs.Join(path...)); err != nil {
return &NFSStatusError{NFSStatusAccess, err}
} else if !s.IsDir() {
return &NFSStatusError{NFSStatusNotDir, nil}
}
}
file, err := fs.Create(ctx, newFilePath)
if err != nil {
Log.Errorf("Error Creating: %v", err)
return &NFSStatusError{NFSStatusAccess, err}
}
if err := file.Close(ctx); err != nil {
Log.Errorf("Error Creating: %v", err)
return &NFSStatusError{NFSStatusAccess, err}
}
fp := userHandle.ToHandle(fs, newFile)
changer := userHandle.Change(fs)
if err := attrs.Apply(ctx, changer, fs, newFilePath); err != nil {
Log.Errorf("Error applying attributes: %v\n", err)
return &NFSStatusError{NFSStatusIO, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// "handle follows"
if err := xdr.Write(writer, uint32(1)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, fp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, []string{file.Name()})); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// dir_wcc (we don't include pre_op_attr)
if err := xdr.Write(writer, uint32(0)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,89 @@
package nfs
import (
"bytes"
"context"
"github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
const (
// FSInfoPropertyLink does the FS support hard links?
FSInfoPropertyLink = 0x0001
// FSInfoPropertySymlink does the FS support soft links?
FSInfoPropertySymlink = 0x0002
// FSInfoPropertyHomogeneous does the FS need PATHCONF calls for each file
FSInfoPropertyHomogeneous = 0x0008
// FSInfoPropertyCanSetTime can the FS support setting access/mod times?
FSInfoPropertyCanSetTime = 0x0010
)
func onFSInfo(ctx context.Context, w *response, userHandle Handler) error {
roothandle, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(roothandle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
type fsinfores struct {
Rtmax uint32
Rtpref uint32
Rtmult uint32
Wtmax uint32
Wtpref uint32
Wtmult uint32
Dtpref uint32
Maxfilesize uint64
TimeDelta uint64
Properties uint32
}
res := fsinfores{
Rtmax: 1 << 30,
Rtpref: 1 << 30,
Rtmult: 4096,
Wtmax: 1 << 30,
Wtpref: 1 << 30,
Wtmult: 4096,
Dtpref: 8192,
Maxfilesize: 1 << 62, // wild guess. this seems big.
TimeDelta: 1, // nanosecond precision.
Properties: 0,
}
// TODO: these aren't great indications of support, really.
// if _, ok := fs.(billy.Symlink); ok {
// res.Properties |= FSInfoPropertyLink
// res.Properties |= FSInfoPropertySymlink
// }
// TODO: if the nfs share spans multiple virtual mounts, may need
// to support granular PATHINFO responses.
res.Properties |= FSInfoPropertyHomogeneous
// TODO: not a perfect indicator
if CapabilityCheck(fs, billy.WriteCapability) {
res.Properties |= FSInfoPropertyCanSetTime
}
// TODO: this whole struct should be specifiable by the userhandler.
if err := xdr.Write(writer, res); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,59 @@
package nfs
import (
"bytes"
"context"
billy "github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func onFSStat(ctx context.Context, w *response, userHandle Handler) error {
roothandle, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(roothandle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
defaults := FSStat{
TotalSize: 1 << 62,
FreeSize: 1 << 62,
AvailableSize: 1 << 62,
TotalFiles: 1 << 62,
FreeFiles: 1 << 62,
AvailableFiles: 1 << 62,
CacheHint: 0,
}
if !CapabilityCheck(fs, billy.WriteCapability) {
defaults.AvailableFiles = 0
defaults.AvailableSize = 0
}
err = userHandle.FSStat(ctx, fs, &defaults)
if err != nil {
if _, ok := err.(*NFSStatusError); ok {
return err
}
return &NFSStatusError{NFSStatusServerFault, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, defaults); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,48 @@
package nfs
import (
"bytes"
"context"
"errors"
"os"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func onGetAttr(ctx context.Context, w *response, userHandle Handler) error {
handle, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
fullPath := fs.Join(path...)
info, err := fs.Lstat(ctx, fullPath)
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusIO, err}
}
attr := ToFileAttribute(info, fullPath)
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, attr); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

94
pkg/go-nfs/nfs_onlink.go Normal file
View file

@ -0,0 +1,94 @@
package nfs
import (
"bytes"
"context"
"os"
"github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
// Backing billy.FS doesn't support hard links
func onLink(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
obj := DirOpArg{}
err := xdr.Read(w.req.Body, &obj)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
attrs, err := ReadSetFileAttributes(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
target, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
}
if len(string(obj.Filename)) > PathNameMax {
return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
}
newFilePath := fs.Join(append(path, string(obj.Filename))...)
if _, err := fs.Stat(ctx, newFilePath); err == nil {
return &NFSStatusError{NFSStatusExist, os.ErrExist}
}
if s, err := fs.Stat(ctx, fs.Join(path...)); err != nil {
return &NFSStatusError{NFSStatusAccess, err}
} else if !s.IsDir() {
return &NFSStatusError{NFSStatusNotDir, nil}
}
fp := userHandle.ToHandle(fs, append(path, string(obj.Filename)))
changer := userHandle.Change(fs)
if changer == nil {
return &NFSStatusError{NFSStatusAccess, err}
}
cos, ok := changer.(UnixChange)
if !ok {
return &NFSStatusError{NFSStatusAccess, err}
}
err = cos.Link(ctx, string(target), newFilePath)
if err != nil {
return &NFSStatusError{NFSStatusAccess, err}
}
if err := attrs.Apply(ctx, changer, fs, newFilePath); err != nil {
return &NFSStatusError{NFSStatusIO, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// "handle follows"
if err := xdr.Write(writer, uint32(1)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, fp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, append(path, string(obj.Filename)))); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WriteWcc(writer, nil, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,86 @@
package nfs
import (
"bytes"
"context"
"os"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func lookupSuccessResponse(ctx context.Context, handle []byte, entPath, dirPath []string, fs Filesystem) ([]byte, error) {
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return nil, err
}
if err := xdr.Write(writer, handle); err != nil {
return nil, err
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, entPath)); err != nil {
return nil, err
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, dirPath)); err != nil {
return nil, err
}
return writer.Bytes(), nil
}
func onLookup(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = opAttrErrorFormatter
obj := DirOpArg{}
err := xdr.Read(w.req.Body, &obj)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, p, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
dirInfo, err := fs.Lstat(ctx, fs.Join(p...))
if err != nil || !dirInfo.IsDir() {
return &NFSStatusError{NFSStatusNotDir, err}
}
// Special cases for "." and ".."
if bytes.Equal(obj.Filename, []byte(".")) {
resp, err := lookupSuccessResponse(ctx, obj.Handle, p, p, fs)
if err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(resp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}
if bytes.Equal(obj.Filename, []byte("..")) {
if len(p) == 0 {
return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
}
pPath := p[0 : len(p)-1]
pHandle := userHandle.ToHandle(fs, pPath)
resp, err := lookupSuccessResponse(ctx, pHandle, pPath, p, fs)
if err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(resp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}
reqPath := append(p, string(obj.Filename))
if _, err = fs.Lstat(ctx, fs.Join(reqPath...)); err != nil {
return &NFSStatusError{NFSStatusNoEnt, os.ErrNotExist}
}
newHandle := userHandle.ToHandle(fs, reqPath)
resp, err := lookupSuccessResponse(ctx, newHandle, reqPath, p, fs)
if err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(resp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

94
pkg/go-nfs/nfs_onmkdir.go Normal file
View file

@ -0,0 +1,94 @@
package nfs
import (
"bytes"
"context"
"os"
"github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
const (
mkdirDefaultMode = 755
)
func onMkdir(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
obj := DirOpArg{}
err := xdr.Read(w.req.Body, &obj)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
attrs, err := ReadSetFileAttributes(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
}
if len(string(obj.Filename)) > PathNameMax {
return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
}
if string(obj.Filename) == "." || string(obj.Filename) == ".." {
return &NFSStatusError{NFSStatusExist, os.ErrExist}
}
newFolder := append(path, string(obj.Filename))
newFolderPath := fs.Join(newFolder...)
if s, err := fs.Stat(ctx, newFolderPath); err == nil {
if s.IsDir() {
return &NFSStatusError{NFSStatusExist, nil}
}
} else {
if s, err := fs.Stat(ctx, fs.Join(path...)); err != nil {
return &NFSStatusError{NFSStatusAccess, err}
} else if !s.IsDir() {
return &NFSStatusError{NFSStatusNotDir, nil}
}
}
if err := fs.MkdirAll(ctx, newFolderPath, attrs.Mode(mkdirDefaultMode)); err != nil {
return &NFSStatusError{NFSStatusAccess, err}
}
fp := userHandle.ToHandle(fs, newFolder)
changer := userHandle.Change(fs)
if changer != nil {
if err := attrs.Apply(ctx, changer, fs, newFolderPath); err != nil {
return &NFSStatusError{NFSStatusIO, err}
}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// "handle follows"
if err := xdr.Write(writer, uint32(1)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, fp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, newFolder)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WriteWcc(writer, nil, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

158
pkg/go-nfs/nfs_onmknod.go Normal file
View file

@ -0,0 +1,158 @@
package nfs
import (
"bytes"
"context"
"os"
billy "github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
type nfs_ftype int32
const (
FTYPE_NF3REG nfs_ftype = 1
FTYPE_NF3DIR nfs_ftype = 2
FTYPE_NF3BLK nfs_ftype = 3
FTYPE_NF3CHR nfs_ftype = 4
FTYPE_NF3LNK nfs_ftype = 5
FTYPE_NF3SOCK nfs_ftype = 6
FTYPE_NF3FIFO nfs_ftype = 7
)
// Backing billy.FS doesn't support creation of
// char, block, socket, or fifo pipe nodes
func onMknod(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
obj := DirOpArg{}
err := xdr.Read(w.req.Body, &obj)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
ftype, err := xdr.ReadUint32(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
// see if the filesystem supports mknod
fs, path, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
}
c := userHandle.Change(fs)
if c == nil {
return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
}
cu, ok := c.(UnixChange)
if !ok {
return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
}
if len(string(obj.Filename)) > PathNameMax {
return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
}
newFilePath := fs.Join(append(path, string(obj.Filename))...)
if _, err := fs.Stat(ctx, newFilePath); err == nil {
return &NFSStatusError{NFSStatusExist, os.ErrExist}
}
parent, err := fs.Stat(ctx, fs.Join(path...))
if err != nil {
return &NFSStatusError{NFSStatusAccess, err}
} else if !parent.IsDir() {
return &NFSStatusError{NFSStatusNotDir, nil}
}
fp := userHandle.ToHandle(fs, append(path, string(obj.Filename)))
switch nfs_ftype(ftype) {
case FTYPE_NF3CHR:
case FTYPE_NF3BLK:
// read devicedata3 = {sattr3, specdata3}
attrs, err := ReadSetFileAttributes(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
specData1, err := xdr.ReadUint32(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
specData2, err := xdr.ReadUint32(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
err = cu.Mknod(ctx, newFilePath, uint32(attrs.Mode(parent.Mode())), specData1, specData2)
if err != nil {
return &NFSStatusError{NFSStatusAccess, err}
}
if err = attrs.Apply(ctx, cu, fs, newFilePath); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
case FTYPE_NF3SOCK:
// read sattr3
attrs, err := ReadSetFileAttributes(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
if err := cu.Socket(ctx, newFilePath); err != nil {
return &NFSStatusError{NFSStatusAccess, err}
}
if err = attrs.Apply(ctx, cu, fs, newFilePath); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
case FTYPE_NF3FIFO:
// read sattr3
attrs, err := ReadSetFileAttributes(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
err = cu.Mkfifo(ctx, newFilePath, uint32(attrs.Mode(parent.Mode())))
if err != nil {
return &NFSStatusError{NFSStatusAccess, err}
}
if err = attrs.Apply(ctx, cu, fs, newFilePath); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
default:
return &NFSStatusError{NFSStatusBadType, os.ErrInvalid}
// end of input.
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// "handle follows"
if err := xdr.Write(writer, uint32(1)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// fh3
if err := xdr.Write(writer, fp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// attr
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, append(path, string(obj.Filename)))); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// wcc
if err := WriteWcc(writer, nil, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,55 @@
package nfs
import (
"bytes"
"context"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
// PathNameMax is the maximum length for a file name
const PathNameMax = 255
func onPathConf(ctx context.Context, w *response, userHandle Handler) error {
roothandle, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(roothandle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
type PathConf struct {
LinkMax uint32
NameMax uint32
NoTrunc uint32
ChownRestricted uint32
CaseInsensitive uint32
CasePreserving uint32
}
defaults := PathConf{
LinkMax: 1,
NameMax: PathNameMax,
NoTrunc: 1,
ChownRestricted: 0,
CaseInsensitive: 0,
CasePreserving: 1,
}
if err := xdr.Write(writer, defaults); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

97
pkg/go-nfs/nfs_onread.go Normal file
View file

@ -0,0 +1,97 @@
package nfs
import (
"bytes"
"context"
"errors"
"io"
"os"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
type nfsReadArgs struct {
Handle []byte
Offset uint64
Count uint32
}
type nfsReadResponse struct {
Count uint32
EOF uint32
Data []byte
}
// MaxRead is the advertised largest buffer the server is willing to read
const MaxRead = 1 << 24
// CheckRead is a size where - if a request to read is larger than this,
// the server will stat the file to learn it's actual size before allocating
// a buffer to read into.
const CheckRead = 1 << 15
func onRead(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = opAttrErrorFormatter
var obj nfsReadArgs
err := xdr.Read(w.req.Body, &obj)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
fh, err := fs.Open(ctx, fs.Join(path...))
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusAccess, err}
}
resp := nfsReadResponse{}
if obj.Count > CheckRead {
info, err := fs.Stat(ctx, fs.Join(path...))
if err != nil {
return &NFSStatusError{NFSStatusAccess, err}
}
if info.Size()-int64(obj.Offset) < int64(obj.Count) {
obj.Count = uint32(uint64(info.Size()) - obj.Offset)
}
}
if obj.Count > MaxRead {
obj.Count = MaxRead
}
resp.Data = make([]byte, obj.Count)
// todo: multiple reads if size isn't full
cnt, err := fh.ReadAt(ctx, resp.Data, int64(obj.Offset))
if err != nil && !errors.Is(err, io.EOF) {
return &NFSStatusError{NFSStatusIO, err}
}
resp.Count = uint32(cnt)
resp.Data = resp.Data[:resp.Count]
if errors.Is(err, io.EOF) {
resp.EOF = 1
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, resp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

195
pkg/go-nfs/nfs_onreaddir.go Normal file
View file

@ -0,0 +1,195 @@
package nfs
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"io"
"io/fs"
"os"
"path"
"sort"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
type readDirArgs struct {
Handle []byte
Cookie uint64
CookieVerif uint64
Count uint32
}
type readDirEntity struct {
FileID uint64
Name []byte
Cookie uint64
Next bool
}
func onReadDir(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = opAttrErrorFormatter
obj := readDirArgs{}
err := xdr.Read(w.req.Body, &obj)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
if obj.Count < 1024 {
return &NFSStatusError{NFSStatusTooSmall, io.ErrShortBuffer}
}
fs, p, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
contents, verifier, err := getDirListingWithVerifier(ctx, userHandle, obj.Handle, obj.CookieVerif)
if err != nil {
return err
}
if obj.Cookie > 0 && obj.CookieVerif > 0 && verifier != obj.CookieVerif {
return &NFSStatusError{NFSStatusBadCookie, nil}
}
entities := make([]readDirEntity, 0)
maxBytes := uint32(100) // conservative overhead measure
started := obj.Cookie == 0
if started {
// add '.' and '..' to entities
dotdotFileID := uint64(0)
if len(p) > 0 {
dda := tryStat(ctx, fs, p[0:len(p)-1])
if dda != nil {
dotdotFileID = dda.Fileid
}
}
dotFileID := uint64(0)
da := tryStat(ctx, fs, p)
if da != nil {
dotFileID = da.Fileid
}
entities = append(entities,
readDirEntity{Name: []byte("."), Cookie: 0, Next: true, FileID: dotFileID},
readDirEntity{Name: []byte(".."), Cookie: 1, Next: true, FileID: dotdotFileID},
)
}
eof := true
maxEntities := userHandle.HandleLimit() / 2
for i, c := range contents {
// cookie equates to index within contents + 2 (for '.' and '..')
cookie := uint64(i + 2)
if started {
maxBytes += 512 // TODO: better estimation.
if maxBytes > obj.Count || len(entities) > maxEntities {
eof = false
break
}
attrs := ToFileAttribute(c, path.Join(append(p, c.Name())...))
entities = append(entities, readDirEntity{
FileID: attrs.Fileid,
Name: []byte(c.Name()),
Cookie: cookie,
Next: true,
})
} else if cookie == obj.Cookie {
started = true
}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, p)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, verifier); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, len(entities) > 0); err != nil { // next
return &NFSStatusError{NFSStatusServerFault, err}
}
if len(entities) > 0 {
entities[len(entities)-1].Next = false
// no next for last entity
for _, e := range entities {
if err := xdr.Write(writer, e); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
}
}
if err := xdr.Write(writer, eof); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// TODO: track writer size at this point to validate maxcount estimation and stop early if needed.
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}
func getDirListingWithVerifier(ctx context.Context, userHandle Handler, fsHandle []byte, verifier uint64) ([]fs.FileInfo, uint64, error) {
// figure out what directory it is.
fs, p, err := userHandle.FromHandle(fsHandle)
if err != nil {
return nil, 0, &NFSStatusError{NFSStatusStale, err}
}
path := fs.Join(p...)
// see if the verifier has this dir cached:
if vh, ok := userHandle.(CachingHandler); verifier != 0 && ok {
entries := vh.DataForVerifier(path, verifier)
if entries != nil {
return entries, verifier, nil
}
}
// load the entries.
contents, err := fs.ReadDir(ctx, path)
if err != nil {
if os.IsPermission(err) {
return nil, 0, &NFSStatusError{NFSStatusAccess, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return nil, 0, &NFSStatusError{NFSStatusJukebox, err}
}
return nil, 0, &NFSStatusError{NFSStatusIO, err}
}
sort.Slice(contents, func(i, j int) bool {
return contents[i].Name() < contents[j].Name()
})
if vh, ok := userHandle.(CachingHandler); ok {
// let the user handler make a verifier if it can.
v := vh.VerifierFor(path, contents)
return contents, v, nil
}
id := hashPathAndContents(path, contents)
return contents, id, nil
}
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([]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)
}

View file

@ -0,0 +1,153 @@
package nfs
import (
"bytes"
"context"
"path"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
type readDirPlusArgs struct {
Handle []byte
Cookie uint64
CookieVerif uint64
DirCount uint32
MaxCount uint32
}
type readDirPlusEntity struct {
FileID uint64
Name []byte
Cookie uint64
Attributes *FileAttribute `xdr:"optional"`
Handle *[]byte `xdr:"optional"`
Next bool
}
func joinPath(parent []string, elements ...string) []string {
joinedPath := make([]string, 0, len(parent)+len(elements))
joinedPath = append(joinedPath, parent...)
joinedPath = append(joinedPath, elements...)
return joinedPath
}
func onReadDirPlus(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = opAttrErrorFormatter
obj := readDirPlusArgs{}
if err := xdr.Read(w.req.Body, &obj); err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
// in case of test, nfs-client send:
// DirCount = 512
// MaxCount = 4096
if obj.DirCount < 512 || obj.MaxCount < 4096 {
return &NFSStatusError{NFSStatusTooSmall, nil}
}
fs, p, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
contents, verifier, err := getDirListingWithVerifier(ctx, userHandle, obj.Handle, obj.CookieVerif)
if err != nil {
return err
}
if obj.Cookie > 0 && obj.CookieVerif > 0 && verifier != obj.CookieVerif {
return &NFSStatusError{NFSStatusBadCookie, nil}
}
entities := make([]readDirPlusEntity, 0)
dirBytes := uint32(0)
maxBytes := uint32(100) // conservative overhead measure
started := obj.Cookie == 0
if started {
// add '.' and '..' to entities
dotdotFileID := uint64(0)
if len(p) > 0 {
dda := tryStat(ctx, fs, p[0:len(p)-1])
if dda != nil {
dotdotFileID = dda.Fileid
}
}
dotFileID := uint64(0)
da := tryStat(ctx, fs, p)
if da != nil {
dotFileID = da.Fileid
}
entities = append(entities,
readDirPlusEntity{Name: []byte("."), Cookie: 0, Next: true, FileID: dotFileID, Attributes: da},
readDirPlusEntity{Name: []byte(".."), Cookie: 1, Next: true, FileID: dotdotFileID},
)
}
eof := true
maxEntities := userHandle.HandleLimit() / 2
fb := 0
fss := 0
for i, c := range contents {
// cookie equates to index within contents + 2 (for '.' and '..')
cookie := uint64(i + 2)
fb++
if started {
fss++
dirBytes += uint32(len(c.Name()) + 20)
maxBytes += 512 // TODO: better estimation.
if dirBytes > obj.DirCount || maxBytes > obj.MaxCount || len(entities) > maxEntities {
eof = false
break
}
filePath := joinPath(p, c.Name())
handle := userHandle.ToHandle(fs, filePath)
attrs := ToFileAttribute(c, path.Join(filePath...))
entities = append(entities, readDirPlusEntity{
FileID: attrs.Fileid,
Name: []byte(c.Name()),
Cookie: cookie,
Attributes: attrs,
Handle: &handle,
Next: true,
})
} else if cookie == obj.Cookie {
started = true
}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, p)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, verifier); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, len(entities) > 0); err != nil { // next
return &NFSStatusError{NFSStatusServerFault, err}
}
if len(entities) > 0 {
entities[len(entities)-1].Next = false
// no next for last entity
for _, e := range entities {
if err := xdr.Write(writer, e); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
}
}
if err := xdr.Write(writer, eof); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// TODO: track writer size at this point to validate maxcount estimation and stop early if needed.
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,55 @@
package nfs
import (
"bytes"
"context"
"errors"
"os"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func onReadLink(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = opAttrErrorFormatter
handle, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
out, err := fs.Readlink(ctx, fs.Join(path...))
if err != nil {
if info, err := fs.Stat(ctx, fs.Join(path...)); err == nil {
if info.Mode()&os.ModeSymlink == 0 {
return &NFSStatusError{NFSStatusInval, err}
}
}
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusAccess, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, out); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,85 @@
package nfs
import (
"bytes"
"context"
"errors"
"os"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func onRemove(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
obj := DirOpArg{}
if err := xdr.Read(w.req.Body, &obj); err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
// TODO
// if !CapabilityCheck(fs, billy.WriteCapability) {
// return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
// }
if len(string(obj.Filename)) > PathNameMax {
return &NFSStatusError{NFSStatusNameTooLong, nil}
}
fullPath := fs.Join(path...)
dirInfo, err := fs.Stat(ctx, fullPath)
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if os.IsPermission(err) {
return &NFSStatusError{NFSStatusAccess, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusIO, err}
}
if !dirInfo.IsDir() {
return &NFSStatusError{NFSStatusNotDir, nil}
}
preCacheData := ToFileAttribute(dirInfo, fullPath).AsCache()
toDelete := fs.Join(append(path, string(obj.Filename))...)
toDeleteHandle := userHandle.ToHandle(fs, append(path, string(obj.Filename)))
err = fs.Remove(ctx, toDelete)
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if os.IsPermission(err) {
return &NFSStatusError{NFSStatusAccess, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusIO, err}
}
if err := userHandle.InvalidateHandle(fs, toDeleteHandle); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WriteWcc(writer, preCacheData, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

120
pkg/go-nfs/nfs_onrename.go Normal file
View file

@ -0,0 +1,120 @@
package nfs
import (
"bytes"
"context"
"errors"
"os"
"reflect"
"github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
var doubleWccErrorBody = [16]byte{}
func onRename(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = errFormatterWithBody(doubleWccErrorBody[:])
from := DirOpArg{}
err := xdr.Read(w.req.Body, &from)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, fromPath, err := userHandle.FromHandle(from.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
to := DirOpArg{}
if err = xdr.Read(w.req.Body, &to); err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs2, toPath, err := userHandle.FromHandle(to.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
// check the two fs are the same
if !reflect.DeepEqual(fs, fs2) {
return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
}
if len(string(from.Filename)) > PathNameMax || len(string(to.Filename)) > PathNameMax {
return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
}
fromDirPath := fs.Join(fromPath...)
fromDirInfo, err := fs.Stat(ctx, fromDirPath)
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusIO, err}
}
if !fromDirInfo.IsDir() {
return &NFSStatusError{NFSStatusNotDir, nil}
}
preCacheData := ToFileAttribute(fromDirInfo, fromDirPath).AsCache()
toDirPath := fs.Join(toPath...)
toDirInfo, err := fs.Stat(ctx, toDirPath)
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusIO, err}
}
if !toDirInfo.IsDir() {
return &NFSStatusError{NFSStatusNotDir, nil}
}
preDestData := ToFileAttribute(toDirInfo, toDirPath).AsCache()
oldHandle := userHandle.ToHandle(fs, append(fromPath, string(from.Filename)))
fromLoc := fs.Join(append(fromPath, string(from.Filename))...)
toLoc := fs.Join(append(toPath, string(to.Filename))...)
err = fs.Rename(ctx, fromLoc, toLoc)
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if os.IsPermission(err) {
return &NFSStatusError{NFSStatusAccess, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusIO, err}
}
if err := userHandle.InvalidateHandle(fs, oldHandle); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WriteWcc(writer, preCacheData, tryStat(ctx, fs, fromPath)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WriteWcc(writer, preDestData, tryStat(ctx, fs, toPath)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,9 @@
package nfs
import (
"context"
)
func onRmDir(ctx context.Context, w *response, userHandle Handler) error {
return onRemove(ctx, w, userHandle)
}

View file

@ -0,0 +1,80 @@
package nfs
import (
"bytes"
"context"
"errors"
"os"
"github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func onSetAttr(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
handle, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
attrs, err := ReadSetFileAttributes(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fullPath := fs.Join(path...)
info, err := fs.Lstat(ctx, fullPath)
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusAccess, err}
}
// see if there's a "guard"
if guard, err := xdr.ReadUint32(w.req.Body); err != nil {
return &NFSStatusError{NFSStatusInval, err}
} else if guard != 0 {
// read the ctime.
t := FileTime{}
if err := xdr.Read(w.req.Body, &t); err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
attr := ToFileAttribute(info, fullPath)
if t != attr.Ctime {
return &NFSStatusError{NFSStatusNotSync, nil}
}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
}
changer := userHandle.Change(fs)
if err := attrs.Apply(ctx, changer, fs, fs.Join(path...)); err != nil {
// Already an nfsstatuserror
return err
}
preAttr := ToFileAttribute(info, fullPath).AsCache()
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WriteWcc(writer, preAttr, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

View file

@ -0,0 +1,88 @@
package nfs
import (
"bytes"
"context"
"os"
"github.com/go-git/go-billy/v5"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func onSymlink(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
obj := DirOpArg{}
err := xdr.Read(w.req.Body, &obj)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
attrs, err := ReadSetFileAttributes(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
target, err := xdr.ReadOpaque(w.req.Body)
if err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(obj.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
if !CapabilityCheck(fs, billy.WriteCapability) {
return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
}
if len(string(obj.Filename)) > PathNameMax {
return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
}
newFilePath := fs.Join(append(path, string(obj.Filename))...)
if _, err := fs.Stat(ctx, newFilePath); err == nil {
return &NFSStatusError{NFSStatusExist, os.ErrExist}
}
if s, err := fs.Stat(ctx, fs.Join(path...)); err != nil {
return &NFSStatusError{NFSStatusAccess, err}
} else if !s.IsDir() {
return &NFSStatusError{NFSStatusNotDir, nil}
}
err = fs.Symlink(ctx, string(target), newFilePath)
if err != nil {
return &NFSStatusError{NFSStatusAccess, err}
}
fp := userHandle.ToHandle(fs, append(path, string(obj.Filename)))
changer := userHandle.Change(fs)
if changer != nil {
if err := attrs.Apply(ctx, changer, fs, newFilePath); err != nil {
return &NFSStatusError{NFSStatusIO, err}
}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
// "handle follows"
if err := xdr.Write(writer, uint32(1)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, fp); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WritePostOpAttrs(writer, tryStat(ctx, fs, append(path, string(obj.Filename)))); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WriteWcc(writer, nil, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

116
pkg/go-nfs/nfs_onwrite.go Normal file
View file

@ -0,0 +1,116 @@
package nfs
import (
"bytes"
"context"
"errors"
"io"
"math"
"os"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
// writeStability is the level of durability requested with the write
type writeStability uint32
const (
unstable writeStability = 0
dataSync writeStability = 1
fileSync writeStability = 2
)
type writeArgs struct {
Handle []byte
Offset uint64
Count uint32
How uint32
Data []byte
}
func onWrite(ctx context.Context, w *response, userHandle Handler) error {
w.errorFmt = wccDataErrorFormatter
var req writeArgs
if err := xdr.Read(w.req.Body, &req); err != nil {
return &NFSStatusError{NFSStatusInval, err}
}
fs, path, err := userHandle.FromHandle(req.Handle)
if err != nil {
return &NFSStatusError{NFSStatusStale, err}
}
// TODO
// if !CapabilityCheck(fs, billy.WriteCapability) {
// return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
// }
if len(req.Data) > math.MaxInt32 || req.Count > math.MaxInt32 {
return &NFSStatusError{NFSStatusFBig, os.ErrInvalid}
}
if req.How != uint32(unstable) && req.How != uint32(dataSync) && req.How != uint32(fileSync) {
return &NFSStatusError{NFSStatusInval, os.ErrInvalid}
}
// stat first for pre-op wcc.
fullPath := fs.Join(path...)
info, err := fs.Stat(ctx, fullPath)
if err != nil {
if os.IsNotExist(err) {
return &NFSStatusError{NFSStatusNoEnt, err}
}
if errors.Is(err, context.DeadlineExceeded) {
return &NFSStatusError{NFSStatusJukebox, err}
}
return &NFSStatusError{NFSStatusAccess, err}
}
if !info.Mode().IsRegular() {
return &NFSStatusError{NFSStatusInval, os.ErrInvalid}
}
preOpCache := ToFileAttribute(info, fullPath).AsCache()
// now the actual op.
file, err := fs.OpenFile(ctx, fs.Join(path...), os.O_RDWR, info.Mode().Perm())
if err != nil {
return &NFSStatusError{NFSStatusAccess, err}
}
if req.Offset > 0 {
if _, err := file.Seek(int64(req.Offset), io.SeekStart); err != nil {
return &NFSStatusError{NFSStatusIO, err}
}
}
end := req.Count
if len(req.Data) < int(end) {
end = uint32(len(req.Data))
}
writtenCount, err := file.Write(ctx, req.Data[:end])
if err != nil {
Log.Errorf("Error writing: %v", err)
return &NFSStatusError{NFSStatusIO, err}
}
if err := file.Close(ctx); err != nil {
Log.Errorf("error closing: %v", err)
return &NFSStatusError{NFSStatusIO, err}
}
writer := bytes.NewBuffer([]byte{})
if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := WriteWcc(writer, preOpCache, tryStat(ctx, fs, path)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, uint32(writtenCount)); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, fileSync); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := xdr.Write(writer, w.Server.ID); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
if err := w.Write(writer.Bytes()); err != nil {
return &NFSStatusError{NFSStatusServerFault, err}
}
return nil
}

293
pkg/go-nfs/nfs_test.go Normal file
View file

@ -0,0 +1,293 @@
package nfs_test
import (
"bytes"
"context"
"fmt"
"net"
"reflect"
"sort"
"testing"
nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers/memfs"
nfsc "github.com/willscott/go-nfs-client/nfs"
rpc "github.com/willscott/go-nfs-client/nfs/rpc"
"github.com/willscott/go-nfs-client/nfs/util"
"github.com/willscott/go-nfs-client/nfs/xdr"
)
func TestNFS(t *testing.T) {
ctx := context.Background()
if testing.Verbose() {
util.DefaultLogger.SetDebug(true)
}
// make an empty in-memory server.
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
mem := helpers.WrapBillyFS(memfs.New())
// File needs to exist in the root for memfs to acknowledge the root exists.
_, _ = mem.Create(ctx, "/test")
handler := helpers.NewNullAuthHandler(mem)
cacheHelper := helpers.NewCachingHandler(handler, 1024)
go func() {
_ = nfs.Serve(listener, cacheHelper)
}()
c, err := rpc.DialTCP(listener.Addr().Network(), listener.Addr().(*net.TCPAddr).String(), false)
if err != nil {
t.Fatal(err)
}
defer c.Close()
var mounter nfsc.Mount
mounter.Client = c
target, err := mounter.Mount("/", rpc.AuthNull)
if err != nil {
t.Fatal(err)
}
defer func() {
_ = mounter.Unmount()
}()
_, err = target.FSInfo()
if err != nil {
t.Fatal(err)
}
// Validate sample file creation
_, err = target.Create("/helloworld.txt", 0666)
if err != nil {
t.Fatal(err)
}
if info, err := mem.Stat(ctx, "/helloworld.txt"); err != nil {
t.Fatal(err)
} else {
if info.Size() != 0 || info.Mode().Perm() != 0666 {
t.Fatal("incorrect creation.")
}
}
// Validate writing to a file.
f, err := target.OpenFile("/helloworld.txt", 0666)
if err != nil {
t.Fatal(err)
}
b := []byte("hello world")
_, err = f.Write(b)
if err != nil {
t.Fatal(err)
}
mf, _ := mem.Open(ctx, "/helloworld.txt")
buf := make([]byte, len(b))
if _, err = mf.Read(ctx, buf[:]); err != nil {
t.Fatal(err)
}
if !bytes.Equal(buf, b) {
t.Fatal("written does not match expected")
}
// for test nfs.ReadDirPlus in case of many files
dirF1, err := mem.ReadDir(ctx, "/")
if err != nil {
t.Fatal(err)
}
shouldBeNames := []string{}
for _, f := range dirF1 {
shouldBeNames = append(shouldBeNames, f.Name())
}
for i := 0; i < 2000; i++ {
fName := fmt.Sprintf("f-%04d.txt", i)
shouldBeNames = append(shouldBeNames, fName)
f, err := mem.Create(ctx, fName)
if err != nil {
t.Fatal(err)
}
f.Close(ctx)
}
manyEntitiesPlus, err := target.ReadDirPlus("/")
if err != nil {
t.Fatal(err)
}
actualBeNamesPlus := []string{}
for _, e := range manyEntitiesPlus {
actualBeNamesPlus = append(actualBeNamesPlus, e.Name())
}
as := sort.StringSlice(shouldBeNames)
bs := sort.StringSlice(actualBeNamesPlus)
as.Sort()
bs.Sort()
if !reflect.DeepEqual(as, bs) {
t.Fatal("nfs.ReadDirPlus error")
}
// for test nfs.ReadDir in case of many files
manyEntities, err := readDir(target, "/")
if err != nil {
t.Fatal(err)
}
actualBeNames := []string{}
for _, e := range manyEntities {
actualBeNames = append(actualBeNames, e.FileName)
}
as2 := sort.StringSlice(shouldBeNames)
bs2 := sort.StringSlice(actualBeNames)
as2.Sort()
bs2.Sort()
if !reflect.DeepEqual(as2, bs2) {
fmt.Printf("should be %v\n", as2)
fmt.Printf("actual be %v\n", bs2)
t.Fatal("nfs.ReadDir error")
}
// confirm rename works as expected
oldFA, _, err := target.Lookup("/f-0010.txt", false)
if err != nil {
t.Fatal(err)
}
if err := target.Rename("/f-0010.txt", "/g-0010.txt"); err != nil {
t.Fatal(err)
}
new, _, err := target.Lookup("/g-0010.txt", false)
if err != nil {
t.Fatal(err)
}
if new.Sys() != oldFA.Sys() {
t.Fatal("rename failed to update")
}
_, _, err = target.Lookup("/f-0010.txt", false)
if err == nil {
t.Fatal("old handle should be invalid")
}
// for test nfs.ReadDirPlus in case of empty directory
_, err = target.Mkdir("/empty", 0755)
if err != nil {
t.Fatal(err)
}
emptyEntitiesPlus, err := target.ReadDirPlus("/empty")
if err != nil {
t.Fatal(err)
}
if len(emptyEntitiesPlus) != 0 {
t.Fatal("nfs.ReadDirPlus error reading empty dir")
}
// for test nfs.ReadDir in case of empty directory
emptyEntities, err := readDir(target, "/empty")
if err != nil {
t.Fatal(err)
}
if len(emptyEntities) != 0 {
t.Fatal("nfs.ReadDir error reading empty dir")
}
}
type readDirEntry struct {
FileId uint64
FileName string
Cookie uint64
}
// readDir implementation "appropriated" from go-nfs-client implementation of READDIRPLUS
func readDir(target *nfsc.Target, dir string) ([]*readDirEntry, error) {
_, fh, err := target.Lookup(dir)
if err != nil {
return nil, err
}
type readDirArgs struct {
rpc.Header
Handle []byte
Cookie uint64
CookieVerif uint64
Count uint32
}
type readDirList struct {
IsSet bool `xdr:"union"`
Entry readDirEntry `xdr:"unioncase=1"`
}
type readDirListOK struct {
DirAttrs nfsc.PostOpAttr
CookieVerf uint64
}
cookie := uint64(0)
cookieVerf := uint64(0)
eof := false
var entries []*readDirEntry
for !eof {
res, err := target.Call(&readDirArgs{
Header: rpc.Header{
Rpcvers: 2,
Vers: nfsc.Nfs3Vers,
Prog: nfsc.Nfs3Prog,
Proc: uint32(nfs.NFSProcedureReadDir),
Cred: rpc.AuthNull,
Verf: rpc.AuthNull,
},
Handle: fh,
Cookie: cookie,
CookieVerif: cookieVerf,
Count: 4096,
})
if err != nil {
return nil, err
}
status, err := xdr.ReadUint32(res)
if err != nil {
return nil, err
}
if err = nfsc.NFS3Error(status); err != nil {
return nil, err
}
dirListOK := new(readDirListOK)
if err = xdr.Read(res, dirListOK); err != nil {
return nil, err
}
for {
var item readDirList
if err = xdr.Read(res, &item); err != nil {
return nil, err
}
if !item.IsSet {
break
}
cookie = item.Entry.Cookie
if item.Entry.FileName == "." || item.Entry.FileName == ".." {
continue
}
entries = append(entries, &item.Entry)
}
if err = xdr.Read(res, &eof); err != nil {
return nil, err
}
cookieVerf = dirListOK.CookieVerf
}
return entries, nil
}

188
pkg/go-nfs/nfsinterface.go Normal file
View file

@ -0,0 +1,188 @@
package nfs
// NFSProcedure is the valid RPC calls for the nfs service.
type NFSProcedure uint32
// NfsProcedure Codes
const (
NFSProcedureNull NFSProcedure = iota
NFSProcedureGetAttr
NFSProcedureSetAttr
NFSProcedureLookup
NFSProcedureAccess
NFSProcedureReadlink
NFSProcedureRead
NFSProcedureWrite
NFSProcedureCreate
NFSProcedureMkDir
NFSProcedureSymlink
NFSProcedureMkNod
NFSProcedureRemove
NFSProcedureRmDir
NFSProcedureRename
NFSProcedureLink
NFSProcedureReadDir
NFSProcedureReadDirPlus
NFSProcedureFSStat
NFSProcedureFSInfo
NFSProcedurePathConf
NFSProcedureCommit
)
func (n NFSProcedure) String() string {
switch n {
case NFSProcedureNull:
return "Null"
case NFSProcedureGetAttr:
return "GetAttr"
case NFSProcedureSetAttr:
return "SetAttr"
case NFSProcedureLookup:
return "Lookup"
case NFSProcedureAccess:
return "Access"
case NFSProcedureReadlink:
return "ReadLink"
case NFSProcedureRead:
return "Read"
case NFSProcedureWrite:
return "Write"
case NFSProcedureCreate:
return "Create"
case NFSProcedureMkDir:
return "Mkdir"
case NFSProcedureSymlink:
return "Symlink"
case NFSProcedureMkNod:
return "Mknod"
case NFSProcedureRemove:
return "Remove"
case NFSProcedureRmDir:
return "Rmdir"
case NFSProcedureRename:
return "Rename"
case NFSProcedureLink:
return "Link"
case NFSProcedureReadDir:
return "ReadDir"
case NFSProcedureReadDirPlus:
return "ReadDirPlus"
case NFSProcedureFSStat:
return "FSStat"
case NFSProcedureFSInfo:
return "FSInfo"
case NFSProcedurePathConf:
return "PathConf"
case NFSProcedureCommit:
return "Commit"
default:
return "Unknown"
}
}
// NFSStatus (nfsstat3) is a result code for nfs rpc calls
type NFSStatus uint32
// NFSStatus codes
const (
NFSStatusOk NFSStatus = 0
NFSStatusPerm NFSStatus = 1
NFSStatusNoEnt NFSStatus = 2
NFSStatusIO NFSStatus = 5
NFSStatusNXIO NFSStatus = 6
NFSStatusAccess NFSStatus = 13
NFSStatusExist NFSStatus = 17
NFSStatusXDev NFSStatus = 18
NFSStatusNoDev NFSStatus = 19
NFSStatusNotDir NFSStatus = 20
NFSStatusIsDir NFSStatus = 21
NFSStatusInval NFSStatus = 22
NFSStatusFBig NFSStatus = 27
NFSStatusNoSPC NFSStatus = 28
NFSStatusROFS NFSStatus = 30
NFSStatusMlink NFSStatus = 31
NFSStatusNameTooLong NFSStatus = 63
NFSStatusNotEmpty NFSStatus = 66
NFSStatusDQuot NFSStatus = 69
NFSStatusStale NFSStatus = 70
NFSStatusRemote NFSStatus = 71
NFSStatusBadHandle NFSStatus = 10001
NFSStatusNotSync NFSStatus = 10002
NFSStatusBadCookie NFSStatus = 10003
NFSStatusNotSupp NFSStatus = 10004
NFSStatusTooSmall NFSStatus = 10005
NFSStatusServerFault NFSStatus = 10006
NFSStatusBadType NFSStatus = 10007
NFSStatusJukebox NFSStatus = 10008
)
func (s NFSStatus) String() string {
switch s {
case NFSStatusOk:
return "Call Completed Successfull"
case NFSStatusPerm:
return "Not Owner"
case NFSStatusNoEnt:
return "No such file or directory"
case NFSStatusIO:
return "I/O error"
case NFSStatusNXIO:
return "I/O error: No such device"
case NFSStatusAccess:
return "Permission denied"
case NFSStatusExist:
return "File exists"
case NFSStatusXDev:
return "Attempt to do a cross device hard link"
case NFSStatusNoDev:
return "No such device"
case NFSStatusNotDir:
return "Not a directory"
case NFSStatusIsDir:
return "Is a directory"
case NFSStatusInval:
return "Invalid argument"
case NFSStatusFBig:
return "File too large"
case NFSStatusNoSPC:
return "No space left on device"
case NFSStatusROFS:
return "Read only file system"
case NFSStatusMlink:
return "Too many hard links"
case NFSStatusNameTooLong:
return "Name too long"
case NFSStatusNotEmpty:
return "Not empty"
case NFSStatusDQuot:
return "Resource quota exceeded"
case NFSStatusStale:
return "Invalid file handle"
case NFSStatusRemote:
return "Too many levels of remote in path"
case NFSStatusBadHandle:
return "Illegal NFS file handle"
case NFSStatusNotSync:
return "Synchronization mismatch"
case NFSStatusBadCookie:
return "Cookie is Stale"
case NFSStatusNotSupp:
return "Operation not supported"
case NFSStatusTooSmall:
return "Buffer or request too small"
case NFSStatusServerFault:
return "Unmapped error (EIO)"
case NFSStatusBadType:
return "Type not supported"
case NFSStatusJukebox:
return "Initiated, but too slow. Try again with new txn"
default:
return "unknown"
}
}
// DirOpArg is a common serialization used for referencing an object in a directory
type DirOpArg struct {
Handle []byte
Filename []byte
}

102
pkg/go-nfs/server.go Normal file
View file

@ -0,0 +1,102 @@
package nfs
import (
"bytes"
"context"
"crypto/rand"
"errors"
"net"
"time"
)
// Server is a handle to the listening NFS server.
type Server struct {
Handler
ID [8]byte
}
// RegisterMessageHandler registers a handler for a specific
// XDR procedure.
func RegisterMessageHandler(protocol uint32, proc uint32, handler HandleFunc) error {
if registeredHandlers == nil {
registeredHandlers = make(map[registeredHandlerID]HandleFunc)
}
for k := range registeredHandlers {
if k.protocol == protocol && k.proc == proc {
return errors.New("already registered")
}
}
id := registeredHandlerID{protocol, proc}
registeredHandlers[id] = handler
return nil
}
// HandleFunc represents a handler for a specific protocol message.
type HandleFunc func(ctx context.Context, w *response, userHandler Handler) error
// TODO: store directly as a uint64 for more efficient lookups
type registeredHandlerID struct {
protocol uint32
proc uint32
}
var registeredHandlers map[registeredHandlerID]HandleFunc
// Serve listens on the provided listener port for incoming client requests.
func (s *Server) Serve(l net.Listener) error {
defer l.Close()
if bytes.Equal(s.ID[:], []byte{0, 0, 0, 0, 0, 0, 0, 0}) {
if _, err := rand.Reader.Read(s.ID[:]); err != nil {
return err
}
}
var tempDelay time.Duration
for {
conn, err := l.Accept()
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
time.Sleep(tempDelay)
continue
}
return err
}
tempDelay = 0
c := s.newConn(conn)
go c.serve()
}
}
func (s *Server) newConn(nc net.Conn) *conn {
c := &conn{
Server: s,
Conn: nc,
}
return c
}
// TODO: keep an immutable map for each server instance to have less
// chance of races.
func (s *Server) handlerFor(prog uint32, proc uint32) HandleFunc {
for k, v := range registeredHandlers {
if k.protocol == prog && k.proc == proc {
return v
}
}
return nil
}
// Serve is a singleton listener paralleling http.Serve
func Serve(l net.Listener, handler Handler) error {
srv := &Server{Handler: handler}
return srv.Serve(l)
}

32
pkg/go-nfs/time.go Normal file
View file

@ -0,0 +1,32 @@
package nfs
import (
"time"
)
// FileTime is the NFS wire time format
// This is equivalent to go-nfs-client/nfs.NFS3Time
type FileTime struct {
Seconds uint32
Nseconds uint32
}
// ToNFSTime generates the nfs 64bit time format from a golang time.
func ToNFSTime(t time.Time) FileTime {
return FileTime{
Seconds: uint32(t.Unix()),
Nseconds: uint32(t.UnixNano() % int64(time.Second)),
}
}
// Native generates a golang time from an nfs time spec
func (t FileTime) Native() *time.Time {
ts := time.Unix(int64(t.Seconds), int64(t.Nseconds))
return &ts
}
// EqualTimespec returns if this time is equal to a local time spec
func (t FileTime) EqualTimespec(sec int64, nsec int64) bool {
// TODO: bounds check on sec/nsec overflow
return t.Nseconds == uint32(nsec) && t.Seconds == uint32(sec)
}

90
pkg/kvtrace/kvmetrics.go Normal file
View file

@ -0,0 +1,90 @@
package kvtrace
import (
"context"
"github.com/royalcat/kv"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var tracer = otel.Tracer("github.com/royalcat/kv/tracer")
type traceSrtore[K, V any] struct {
kv kv.Store[K, V]
attrs []attribute.KeyValue
}
func WrapTracing[K, V any](kv kv.Store[K, V], attrs ...attribute.KeyValue) kv.Store[K, V] {
return &traceSrtore[K, V]{
kv: kv,
attrs: attrs,
}
}
// Close implements kv.Store.
func (m *traceSrtore[K, V]) Close(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "Close", trace.WithAttributes(m.attrs...))
defer span.End()
return m.kv.Close(ctx)
}
// Delete implements kv.Store.
func (m *traceSrtore[K, V]) Delete(ctx context.Context, k K) error {
ctx, span := tracer.Start(ctx, "Delete", trace.WithAttributes(m.attrs...))
defer span.End()
return m.kv.Delete(ctx, k)
}
// Get implements kv.Store.
func (m *traceSrtore[K, V]) Get(ctx context.Context, k K) (v V, found bool, err error) {
ctx, span := tracer.Start(ctx, "Get", trace.WithAttributes(m.attrs...))
defer span.End()
return m.kv.Get(ctx, k)
}
// Range implements kv.Store.
func (m *traceSrtore[K, V]) Range(ctx context.Context, iter kv.Iter[K, V]) error {
ctx, span := tracer.Start(ctx, "Range", trace.WithAttributes(m.attrs...))
defer span.End()
count := 0
iterCount := func(k K, v V) bool {
count++
return iter(k, v)
}
err := m.kv.Range(ctx, iterCount)
span.SetAttributes(attribute.Int("count", count))
return err
}
// RangeWithPrefix implements kv.Store.
func (m *traceSrtore[K, V]) RangeWithPrefix(ctx context.Context, k K, iter kv.Iter[K, V]) error {
ctx, span := tracer.Start(ctx, "RangeWithPrefix", trace.WithAttributes(m.attrs...))
defer span.End()
count := 0
iterCount := func(k K, v V) bool {
count++
return iter(k, v)
}
err := m.kv.Range(ctx, iterCount)
span.SetAttributes(attribute.Int("count", count))
return err
}
// Set implements kv.Store.
func (m *traceSrtore[K, V]) Set(ctx context.Context, k K, v V) error {
ctx, span := tracer.Start(ctx, "Set", trace.WithAttributes(m.attrs...))
defer span.End()
return m.kv.Set(ctx, k, v)
}
var _ kv.Store[any, any] = (*traceSrtore[any, any])(nil)