small refactor*
This commit is contained in:
parent
b6b541e050
commit
24a4d30275
232 changed files with 2164 additions and 1906 deletions
server/pkg
cowutils
ctxbilly
go-nfs
.github
CONTRIBUTING.mdLICENSEREADME.mdSECURITY.mdcapability_check.goconn.goerrors.goexample
file.gofile
filesystem.gohandler.gohelpers
log.gomount.gomountinterface.gonfs.gonfs_onaccess.gonfs_oncommit.gonfs_oncreate.gonfs_onfsinfo.gonfs_onfsstat.gonfs_ongetattr.gonfs_onlink.gonfs_onlookup.gonfs_onmkdir.gonfs_onmknod.gonfs_onpathconf.gonfs_onread.gonfs_onreaddir.gonfs_onreaddirplus.gonfs_onreadlink.gonfs_onremove.gonfs_onrename.gonfs_onrmdir.gonfs_onsetattr.gonfs_onsymlink.gonfs_onwrite.gonfs_test.gonfsinterface.goserver.gotime.goioutils
kvsingle
kvtrace
maxcache
rlog
slicesutils
uring
uuid
ytdlp
14
server/pkg/cowutils/cowutils.go
Normal file
14
server/pkg/cowutils/cowutils.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package cowutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// ErrNotSupported is returned by Always() if the operation is not
|
||||
// supported on the current operating system. Auto() will never return this
|
||||
// error.
|
||||
var (
|
||||
ErrNotSupported = errors.New("cow is not supported on this OS")
|
||||
ErrFailed = errors.New("cow is not supported on this OS or file")
|
||||
ErrTooSmall = errors.New("file is too smaller then filesystem block size")
|
||||
)
|
88
server/pkg/cowutils/dedupe.go
Normal file
88
server/pkg/cowutils/dedupe.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package cowutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func DedupeFiles(ctx context.Context, paths []string) (deduped uint64, err error) {
|
||||
srcF, err := os.Open(paths[0])
|
||||
if err != nil {
|
||||
return deduped, err
|
||||
}
|
||||
defer srcF.Close()
|
||||
srcStat, err := srcF.Stat()
|
||||
if err != nil {
|
||||
return deduped, err
|
||||
}
|
||||
|
||||
srcFd := int(srcF.Fd())
|
||||
srcSize := srcStat.Size()
|
||||
|
||||
fsStat := unix.Statfs_t{}
|
||||
err = unix.Fstatfs(srcFd, &fsStat)
|
||||
if err != nil {
|
||||
return deduped, err
|
||||
}
|
||||
|
||||
if int64(fsStat.Bsize) > srcSize { // for btrfs it means file residing in metadata and can't be deduplicated
|
||||
return deduped, nil
|
||||
}
|
||||
|
||||
blockSize := uint64((srcSize % int64(fsStat.Bsize)) * int64(fsStat.Bsize))
|
||||
|
||||
fdr := unix.FileDedupeRange{
|
||||
Src_offset: 0,
|
||||
Src_length: blockSize,
|
||||
Info: []unix.FileDedupeRangeInfo{},
|
||||
}
|
||||
|
||||
for _, dst := range paths[1:] {
|
||||
if ctx.Err() != nil {
|
||||
return deduped, ctx.Err()
|
||||
}
|
||||
|
||||
destF, err := os.OpenFile(dst, os.O_RDWR, os.ModePerm)
|
||||
if err != nil {
|
||||
return deduped, err
|
||||
}
|
||||
|
||||
// defer in cycle is intended, file must be closed only at the end of the function,
|
||||
// and, most importantly, this keeps GC from closing descriptor while dudupe in progress
|
||||
defer destF.Close()
|
||||
|
||||
fdr.Info = append(fdr.Info, unix.FileDedupeRangeInfo{
|
||||
Dest_fd: int64(destF.Fd()),
|
||||
Dest_offset: 0,
|
||||
})
|
||||
}
|
||||
|
||||
if len(fdr.Info) == 0 {
|
||||
return deduped, nil
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return deduped, ctx.Err()
|
||||
}
|
||||
|
||||
fdr.Src_offset = 0
|
||||
for i := range fdr.Info {
|
||||
fdr.Info[i].Dest_offset = 0
|
||||
}
|
||||
|
||||
err = unix.IoctlFileDedupeRange(srcFd, &fdr)
|
||||
if err != nil {
|
||||
return deduped, err
|
||||
}
|
||||
|
||||
for i := range fdr.Info {
|
||||
deduped += fdr.Info[i].Bytes_deduped
|
||||
|
||||
fdr.Info[i].Status = 0
|
||||
fdr.Info[i].Bytes_deduped = 0
|
||||
}
|
||||
|
||||
return deduped, nil
|
||||
}
|
54
server/pkg/cowutils/reflink.go
Normal file
54
server/pkg/cowutils/reflink.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package cowutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Reflink performs the reflink operation on the passed files, replacing
|
||||
// dst's contents with src. If fallback is true and reflink fails,
|
||||
// copy_file_range will be used first, and if that fails too io.Copy will
|
||||
// be used to copy the data.
|
||||
func Reflink(ctx context.Context, dst, src *os.File, fallback bool) error {
|
||||
err := reflink(dst, src)
|
||||
if (err != nil) && fallback {
|
||||
// reflink failed, but we can fallback, but first we need to know the file's size
|
||||
var st fs.FileInfo
|
||||
st, err = src.Stat()
|
||||
if err != nil {
|
||||
// couldn't stat source, this can't be helped
|
||||
return fmt.Errorf("failed to stat source: %w", err)
|
||||
}
|
||||
_, err = copyFileRange(dst, src, 0, 0, st.Size())
|
||||
if err != nil {
|
||||
// copyFileRange failed too, switch to simple io copy
|
||||
reader := io.NewSectionReader(src, 0, st.Size())
|
||||
writer := §ionWriter{w: dst}
|
||||
_ = dst.Truncate(0) // assuming any error in trucate will result in copy error
|
||||
_, err = io.Copy(writer, reader)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ReflinkRange performs a range reflink operation on the passed files, replacing
|
||||
// part of dst's contents with data from src. If fallback is true and reflink
|
||||
// fails, copy_file_range will be used first, and if that fails too io.CopyN
|
||||
// will be used to copy the data.
|
||||
func ReflinkRange(ctx context.Context, dst, src *os.File, dstOffset, srcOffset, n int64, fallback bool) error {
|
||||
err := reflinkRange(dst, src, dstOffset, srcOffset, n)
|
||||
if (err != nil) && fallback {
|
||||
_, err = copyFileRange(dst, src, dstOffset, srcOffset, n)
|
||||
}
|
||||
|
||||
if (err != nil) && fallback {
|
||||
// seek both src & dst
|
||||
reader := io.NewSectionReader(src, srcOffset, n)
|
||||
writer := §ionWriter{w: dst, base: dstOffset}
|
||||
_, err = io.CopyN(writer, reader, n)
|
||||
}
|
||||
return err
|
||||
}
|
53
server/pkg/cowutils/reflink_unix.go
Normal file
53
server/pkg/cowutils/reflink_unix.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
//!build +unix
|
||||
|
||||
package cowutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// reflink performs the actual reflink action without worrying about fallback
|
||||
func reflink(dst, src *os.File) error {
|
||||
srcFd := int(src.Fd())
|
||||
dstFd := int(dst.Fd())
|
||||
|
||||
err := unix.IoctlFileClone(dstFd, srcFd)
|
||||
|
||||
if err != nil && errors.Is(err, unix.ENOTSUP) {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func reflinkRange(dst, src *os.File, dstOffset, srcOffset, n int64) error {
|
||||
srcFd := int(src.Fd())
|
||||
dstFd := int(dst.Fd())
|
||||
|
||||
req := &unix.FileCloneRange{
|
||||
Src_fd: int64(srcFd),
|
||||
Src_offset: uint64(srcOffset),
|
||||
Src_length: uint64(n),
|
||||
Dest_offset: uint64(dstOffset),
|
||||
}
|
||||
|
||||
err := unix.IoctlFileCloneRange(dstFd, req)
|
||||
if err != nil && errors.Is(err, unix.ENOTSUP) {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func copyFileRange(dst, src *os.File, dstOffset, srcOffset, n int64) (int64, error) {
|
||||
srcFd := int(src.Fd())
|
||||
dstFd := int(dst.Fd())
|
||||
|
||||
resN, err := unix.CopyFileRange(srcFd, &srcOffset, dstFd, &dstOffset, int(n), 0)
|
||||
|
||||
return int64(resN), err
|
||||
|
||||
}
|
39
server/pkg/cowutils/writer.go
Normal file
39
server/pkg/cowutils/writer.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package cowutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// sectionWriter is a helper used when we need to fallback into copying data manually
|
||||
type sectionWriter struct {
|
||||
w io.WriterAt // target file
|
||||
base int64 // base position in file
|
||||
off int64 // current relative offset
|
||||
}
|
||||
|
||||
// Write writes & updates offset
|
||||
func (s *sectionWriter) Write(p []byte) (int, error) {
|
||||
n, err := s.w.WriteAt(p, s.base+s.off)
|
||||
s.off += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (s *sectionWriter) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
// nothing needed
|
||||
case io.SeekCurrent:
|
||||
offset += s.off
|
||||
case io.SeekEnd:
|
||||
// we don't support io.SeekEnd
|
||||
fallthrough
|
||||
default:
|
||||
return s.off, errors.New("Seek: invalid whence")
|
||||
}
|
||||
if offset < 0 {
|
||||
return s.off, errors.New("Seek: invalid offset")
|
||||
}
|
||||
s.off = offset
|
||||
return offset, nil
|
||||
}
|
27
server/pkg/ctxbilly/change.go
Normal file
27
server/pkg/ctxbilly/change.go
Normal 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
|
||||
}
|
97
server/pkg/ctxbilly/fs.go
Normal file
97
server/pkg/ctxbilly/fs.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package ctxbilly
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/royalcat/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)
|
||||
// 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)
|
||||
|
||||
// // 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 TempFileFS interface {
|
||||
// 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)
|
||||
}
|
||||
|
||||
type File interface {
|
||||
// Name returns the name of the file as presented to Open.
|
||||
Name() string
|
||||
ctxio.Writer
|
||||
ctxio.WriterAt
|
||||
ctxio.Reader
|
||||
ctxio.ReaderAt
|
||||
io.Seeker
|
||||
ctxio.Closer
|
||||
}
|
||||
|
||||
type LockFile interface {
|
||||
// Lock locks the file like e.g. flock. It protects against access from
|
||||
// other processes.
|
||||
Lock() error
|
||||
// Unlock unlocks the file.
|
||||
Unlock() error
|
||||
}
|
||||
|
||||
type TruncateFile interface {
|
||||
// Truncate the file.
|
||||
Truncate(ctx context.Context, size int64) error
|
||||
}
|
175
server/pkg/ctxbilly/mem.go
Normal file
175
server/pkg/ctxbilly/mem.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
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)
|
||||
}
|
||||
|
||||
// WriteAt implements File.
|
||||
func (m *wrapFile) WriteAt(ctx context.Context, p []byte, off int64) (n int, err error) {
|
||||
_, err = m.File.Seek(off, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return m.File.Write(p)
|
||||
}
|
355
server/pkg/ctxbilly/uring.go
Normal file
355
server/pkg/ctxbilly/uring.go
Normal file
|
@ -0,0 +1,355 @@
|
|||
package ctxbilly
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
// "errors"
|
||||
// "fmt"
|
||||
// "os"
|
||||
// "path/filepath"
|
||||
// "strings"
|
||||
|
||||
// securejoin "github.com/cyphar/filepath-securejoin"
|
||||
// "github.com/iceber/iouring-go"
|
||||
// )
|
||||
|
||||
// func NewURingFS() (*UringFS, error) {
|
||||
// ur, err := iouring.New(64, iouring.WithAsync())
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// return &UringFS{
|
||||
// ur: ur,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// var _ Filesystem = (*UringFS)(nil)
|
||||
|
||||
// const (
|
||||
// defaultDirectoryMode = 0o755
|
||||
// defaultCreateMode = 0o666
|
||||
// )
|
||||
|
||||
// // UringFS is a fs implementation based on the OS filesystem which is bound to
|
||||
// // a base dir.
|
||||
// // Prefer this fs implementation over ChrootOS.
|
||||
// //
|
||||
// // Behaviours of note:
|
||||
// // 1. Read and write operations can only be directed to files which descends
|
||||
// // from the base dir.
|
||||
// // 2. Symlinks don't have their targets modified, and therefore can point
|
||||
// // to locations outside the base dir or to non-existent paths.
|
||||
// // 3. Readlink and Lstat ensures that the link file is located within the base
|
||||
// // dir, evaluating any symlinks that file or base dir may contain.
|
||||
// type UringFS struct {
|
||||
// ur *iouring.IOURing
|
||||
// baseDir string
|
||||
// }
|
||||
|
||||
// func newBoundOS(d string) *UringFS {
|
||||
// return &UringFS{baseDir: d}
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) Create(ctx context.Context, filename string) (File, error) {
|
||||
// return fs.OpenFile(ctx, filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) OpenFile(ctx context.Context, filename string, flag int, perm os.FileMode) (File, error) {
|
||||
// fn, err := fs.abs(filename)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// f, err := os.OpenFile(fn, flag, perm)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// return newFile(fs.ur, f)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) ReadDir(ctx context.Context, path string) ([]os.FileInfo, error) {
|
||||
// dir, err := fs.abs(path)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// entries, err := os.ReadDir(dir)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// infos := make([]os.FileInfo, 0, len(entries))
|
||||
// for _, v := range entries {
|
||||
// info, err := v.Info()
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// infos = append(infos, info)
|
||||
// }
|
||||
|
||||
// return infos, nil
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) Rename(ctx context.Context, from, to string) error {
|
||||
// f, err := fs.abs(from)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// t, err := fs.abs(to)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // MkdirAll for target name.
|
||||
// if err := fs.createDir(t); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// return os.Rename(f, t)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) MkdirAll(ctx context.Context, path string, perm os.FileMode) error {
|
||||
// dir, err := fs.abs(path)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return os.MkdirAll(dir, perm)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) Stat(ctx context.Context, filename string) (os.FileInfo, error) {
|
||||
// filename, err := fs.abs(filename)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return os.Stat(filename)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) Remove(ctx context.Context, filename string) error {
|
||||
// fn, err := fs.abs(filename)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return os.Remove(fn)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) Join(elem ...string) string {
|
||||
// return filepath.Join(elem...)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) RemoveAll(path string) error {
|
||||
// dir, err := fs.abs(path)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return os.RemoveAll(dir)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) Symlink(ctx context.Context, target, link string) error {
|
||||
// ln, err := fs.abs(link)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // MkdirAll for containing dir.
|
||||
// if err := fs.createDir(ln); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return os.Symlink(target, ln)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) Lstat(ctx context.Context, filename string) (os.FileInfo, error) {
|
||||
// filename = filepath.Clean(filename)
|
||||
// if !filepath.IsAbs(filename) {
|
||||
// filename = filepath.Join(fs.baseDir, filename)
|
||||
// }
|
||||
// if ok, err := fs.insideBaseDirEval(filename); !ok {
|
||||
// return nil, err
|
||||
// }
|
||||
// return os.Lstat(filename)
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) Readlink(ctx context.Context, link string) (string, error) {
|
||||
// if !filepath.IsAbs(link) {
|
||||
// link = filepath.Clean(filepath.Join(fs.baseDir, link))
|
||||
// }
|
||||
// if ok, err := fs.insideBaseDirEval(link); !ok {
|
||||
// return "", err
|
||||
// }
|
||||
// return os.Readlink(link)
|
||||
// }
|
||||
|
||||
// // Chroot returns a new OS filesystem, with the base dir set to the
|
||||
// // result of joining the provided path with the underlying base dir.
|
||||
// // func (fs *UringFS) Chroot(path string) (Filesystem, error) {
|
||||
// // joined, err := securejoin.SecureJoin(fs.baseDir, path)
|
||||
// // if err != nil {
|
||||
// // return nil, err
|
||||
// // }
|
||||
// // return newBoundOS(joined), nil
|
||||
// // }
|
||||
|
||||
// // Root returns the current base dir of the billy.Filesystem.
|
||||
// // This is required in order for this implementation to be a drop-in
|
||||
// // replacement for other upstream implementations (e.g. memory and osfs).
|
||||
// func (fs *UringFS) Root() string {
|
||||
// return fs.baseDir
|
||||
// }
|
||||
|
||||
// func (fs *UringFS) createDir(fullpath string) error {
|
||||
// dir := filepath.Dir(fullpath)
|
||||
// if dir != "." {
|
||||
// if err := os.MkdirAll(dir, defaultDirectoryMode); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // abs transforms filename to an absolute path, taking into account the base dir.
|
||||
// // Relative paths won't be allowed to ascend the base dir, so `../file` will become
|
||||
// // `/working-dir/file`.
|
||||
// //
|
||||
// // Note that if filename is a symlink, the returned address will be the target of the
|
||||
// // symlink.
|
||||
// func (fs *UringFS) abs(filename string) (string, error) {
|
||||
// if filename == fs.baseDir {
|
||||
// filename = string(filepath.Separator)
|
||||
// }
|
||||
|
||||
// path, err := securejoin.SecureJoin(fs.baseDir, filename)
|
||||
// if err != nil {
|
||||
// return "", nil
|
||||
// }
|
||||
|
||||
// return path, nil
|
||||
// }
|
||||
|
||||
// // insideBaseDirEval checks whether filename is contained within
|
||||
// // a dir that is within the fs.baseDir, by first evaluating any symlinks
|
||||
// // that either filename or fs.baseDir may contain.
|
||||
// func (fs *UringFS) insideBaseDirEval(filename string) (bool, error) {
|
||||
// dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
||||
// if dir == "" || os.IsNotExist(err) {
|
||||
// dir = filepath.Dir(filename)
|
||||
// }
|
||||
// wd, err := filepath.EvalSymlinks(fs.baseDir)
|
||||
// if wd == "" || os.IsNotExist(err) {
|
||||
// wd = fs.baseDir
|
||||
// }
|
||||
// if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) {
|
||||
// return false, fmt.Errorf("path outside base dir")
|
||||
// }
|
||||
// return true, nil
|
||||
// }
|
||||
|
||||
// func newFile(fsur *iouring.IOURing, f *os.File) (*URingFile, error) {
|
||||
// ur, err := iouring.New(64, iouring.WithAttachWQ(fsur))
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// return &URingFile{
|
||||
// ur: ur,
|
||||
// f: f,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// type URingFile struct {
|
||||
// ur *iouring.IOURing
|
||||
// f *os.File
|
||||
// }
|
||||
|
||||
// // Close implements File.
|
||||
// func (o *URingFile) Close(ctx context.Context) error {
|
||||
// return errors.Join(o.ur.UnregisterFile(o.f), o.Close(ctx))
|
||||
// }
|
||||
|
||||
// // Name implements File.
|
||||
// func (o *URingFile) Name() string {
|
||||
// return o.f.Name()
|
||||
// }
|
||||
|
||||
// // Read implements File.
|
||||
// func (o *URingFile) Read(ctx context.Context, p []byte) (n int, err error) {
|
||||
// req, err := o.ur.Read(o.f, p, nil)
|
||||
// if err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
// defer req.Cancel()
|
||||
|
||||
// select {
|
||||
// case <-req.Done():
|
||||
// return req.GetRes()
|
||||
// case <-ctx.Done():
|
||||
// req.Cancel()
|
||||
// <-req.Done()
|
||||
// return req.GetRes()
|
||||
// }
|
||||
// }
|
||||
|
||||
// // ReadAt implements File.
|
||||
// func (o *URingFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
|
||||
// req, err := o.ur.Pread(o.f, p, uint64(off), nil)
|
||||
// if err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
// defer req.Cancel()
|
||||
|
||||
// select {
|
||||
// case <-req.Done():
|
||||
// return req.GetRes()
|
||||
// case <-ctx.Done():
|
||||
// req.Cancel()
|
||||
// <-req.Done()
|
||||
// return req.GetRes()
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Write implements File.
|
||||
// func (o *URingFile) Write(ctx context.Context, p []byte) (n int, err error) {
|
||||
// req, err := o.ur.Write(o.f, p, nil)
|
||||
// if err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
// defer req.Cancel()
|
||||
|
||||
// select {
|
||||
// case <-req.Done():
|
||||
// return req.GetRes()
|
||||
// case <-ctx.Done():
|
||||
// req.Cancel()
|
||||
// <-req.Done()
|
||||
// return req.GetRes()
|
||||
// }
|
||||
// }
|
||||
|
||||
// // WriteAt implements File.
|
||||
// func (o *URingFile) WriteAt(ctx context.Context, p []byte, off int64) (n int, err error) {
|
||||
// req, err := o.ur.Pwrite(o.f, p, uint64(off), nil)
|
||||
// if err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
// defer req.Cancel()
|
||||
|
||||
// select {
|
||||
// case <-req.Done():
|
||||
// return req.GetRes()
|
||||
// case <-ctx.Done():
|
||||
// req.Cancel()
|
||||
// <-req.Done()
|
||||
// return req.GetRes()
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Seek implements File.
|
||||
// func (o *URingFile) Seek(offset int64, whence int) (int64, error) {
|
||||
// return o.f.Seek(offset, whence)
|
||||
// }
|
||||
|
||||
// // Truncate implements File.
|
||||
// func (o *URingFile) Truncate(ctx context.Context, size int64) error {
|
||||
// return o.f.Truncate(size)
|
||||
// }
|
||||
|
||||
// var _ File = (*URingFile)(nil)
|
11
server/pkg/go-nfs/.github/dependabot.yml
vendored
Normal file
11
server/pkg/go-nfs/.github/dependabot.yml
vendored
Normal 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"
|
51
server/pkg/go-nfs/.github/workflows/codeql-analysis.yml
vendored
Normal file
51
server/pkg/go-nfs/.github/workflows/codeql-analysis.yml
vendored
Normal 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
server/pkg/go-nfs/.github/workflows/go.yml
vendored
Normal file
36
server/pkg/go-nfs/.github/workflows/go.yml
vendored
Normal 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 .
|
11
server/pkg/go-nfs/CONTRIBUTING.md
Normal file
11
server/pkg/go-nfs/CONTRIBUTING.md
Normal 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
server/pkg/go-nfs/LICENSE
Normal file
202
server/pkg/go-nfs/LICENSE
Normal 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
server/pkg/go-nfs/README.md
Normal file
96
server/pkg/go-nfs/README.md
Normal 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
server/pkg/go-nfs/SECURITY.md
Normal file
11
server/pkg/go-nfs/SECURITY.md
Normal 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.
|
9
server/pkg/go-nfs/capability_check.go
Normal file
9
server/pkg/go-nfs/capability_check.go
Normal 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
|
||||
}
|
344
server/pkg/go-nfs/conn.go
Normal file
344
server/pkg/go-nfs/conn.go
Normal file
|
@ -0,0 +1,344 @@
|
|||
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"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
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/server/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) (err error) {
|
||||
ctx, span := tracer.Start(ctx, fmt.Sprintf("nfs.handle.%s", NFSProcedure(w.req.Header.Proc).String()))
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
}()
|
||||
|
||||
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
server/pkg/go-nfs/errors.go
Normal file
230
server/pkg/go-nfs/errors.go
Normal 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[:])
|
||||
)
|
52
server/pkg/go-nfs/example/helloworld/main.go
Normal file
52
server/pkg/go-nfs/example/helloworld/main.go
Normal 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/server/pkg/go-nfs"
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/go-nfs/helpers"
|
||||
nfshelper "git.kmsign.ru/royalcat/tstor/server/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))
|
||||
}
|
38
server/pkg/go-nfs/example/osnfs/changeos.go
Normal file
38
server/pkg/go-nfs/example/osnfs/changeos.go
Normal 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)
|
||||
}
|
28
server/pkg/go-nfs/example/osnfs/changeos_unix.go
Normal file
28
server/pkg/go-nfs/example/osnfs/changeos_unix.go
Normal 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)})
|
||||
}
|
36
server/pkg/go-nfs/example/osnfs/main.go
Normal file
36
server/pkg/go-nfs/example/osnfs/main.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
nfs "git.kmsign.ru/royalcat/tstor/server/pkg/go-nfs"
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/go-nfs/helpers"
|
||||
nfshelper "git.kmsign.ru/royalcat/tstor/server/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))
|
||||
}
|
37
server/pkg/go-nfs/example/osview/main.go
Normal file
37
server/pkg/go-nfs/example/osview/main.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/willscott/memphis"
|
||||
|
||||
nfs "git.kmsign.ru/royalcat/tstor/server/pkg/go-nfs"
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/go-nfs/helpers"
|
||||
nfshelper "git.kmsign.ru/royalcat/tstor/server/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))
|
||||
}
|
379
server/pkg/go-nfs/file.go
Normal file
379
server/pkg/go-nfs/file.go
Normal file
|
@ -0,0 +1,379 @@
|
|||
package nfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/server/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
|
||||
}
|
||||
defer fp.Close(ctx)
|
||||
|
||||
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
server/pkg/go-nfs/file/file.go
Normal file
17
server/pkg/go-nfs/file/file.go
Normal 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)
|
||||
}
|
24
server/pkg/go-nfs/file/file_unix.go
Normal file
24
server/pkg/go-nfs/file/file_unix.go
Normal 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
|
||||
}
|
12
server/pkg/go-nfs/file/file_windows.go
Normal file
12
server/pkg/go-nfs/file/file_windows.go
Normal 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
|
||||
}
|
97
server/pkg/go-nfs/filesystem.go
Normal file
97
server/pkg/go-nfs/filesystem.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package nfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/royalcat/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)
|
||||
// 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
server/pkg/go-nfs/handler.go
Normal file
52
server/pkg/go-nfs/handler.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package nfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"net"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/server/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(ctx context.Context, fs Filesystem, path []string) []byte
|
||||
FromHandle(ctx context.Context, fh []byte) (Filesystem, []string, error)
|
||||
InvalidateHandle(context.Context, 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
|
||||
}
|
157
server/pkg/go-nfs/helpers/billlyfs.go
Normal file
157
server/pkg/go-nfs/helpers/billlyfs.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/server/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)
|
||||
}
|
199
server/pkg/go-nfs/helpers/cachinghandler.go
Normal file
199
server/pkg/go-nfs/helpers/cachinghandler.go
Normal file
|
@ -0,0 +1,199 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"io/fs"
|
||||
"reflect"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/server/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(ctx context.Context, 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(ctx context.Context, 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(ctx context.Context, 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
|
||||
}
|
414
server/pkg/go-nfs/helpers/memfs/memfs.go
Normal file
414
server/pkg/go-nfs/helpers/memfs/memfs.go
Normal 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
|
||||
}
|
243
server/pkg/go-nfs/helpers/memfs/storage.go
Normal file
243
server/pkg/go-nfs/helpers/memfs/storage.go
Normal 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
|
||||
}
|
59
server/pkg/go-nfs/helpers/nullauthhandler.go
Normal file
59
server/pkg/go-nfs/helpers/nullauthhandler.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/ctxbilly"
|
||||
nfs "git.kmsign.ru/royalcat/tstor/server/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(context.Context, nfs.Filesystem, *nfs.FSStat) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToHandle handled by CachingHandler
|
||||
func (h *NullAuthHandler) ToHandle(context.Context, nfs.Filesystem, []string) []byte {
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// FromHandle handled by CachingHandler
|
||||
func (h *NullAuthHandler) FromHandle(context.Context, []byte) (nfs.Filesystem, []string, error) {
|
||||
return nil, []string{}, nil
|
||||
}
|
||||
|
||||
func (c *NullAuthHandler) InvalidateHandle(context.Context, nfs.Filesystem, []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleLImit handled by cachingHandler
|
||||
func (h *NullAuthHandler) HandleLimit() int {
|
||||
return -1
|
||||
}
|
216
server/pkg/go-nfs/log.go
Normal file
216
server/pkg/go-nfs/log.go
Normal 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
server/pkg/go-nfs/mount.go
Normal file
58
server/pkg/go-nfs/mount.go
Normal 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(ctx, 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)
|
||||
}
|
90
server/pkg/go-nfs/mountinterface.go
Normal file
90
server/pkg/go-nfs/mountinterface.go
Normal 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
server/pkg/go-nfs/nfs.go
Normal file
38
server/pkg/go-nfs/nfs.go
Normal 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{})
|
||||
}
|
45
server/pkg/go-nfs/nfs_onaccess.go
Normal file
45
server/pkg/go-nfs/nfs_onaccess.go
Normal 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(ctx, 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
|
||||
}
|
51
server/pkg/go-nfs/nfs_oncommit.go
Normal file
51
server/pkg/go-nfs/nfs_oncommit.go
Normal 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(ctx, 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
server/pkg/go-nfs/nfs_oncreate.go
Normal file
125
server/pkg/go-nfs/nfs_oncreate.go
Normal 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(ctx, 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(ctx, 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
|
||||
}
|
89
server/pkg/go-nfs/nfs_onfsinfo.go
Normal file
89
server/pkg/go-nfs/nfs_onfsinfo.go
Normal 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(ctx, 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
|
||||
}
|
59
server/pkg/go-nfs/nfs_onfsstat.go
Normal file
59
server/pkg/go-nfs/nfs_onfsstat.go
Normal 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(ctx, 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
|
||||
}
|
48
server/pkg/go-nfs/nfs_ongetattr.go
Normal file
48
server/pkg/go-nfs/nfs_ongetattr.go
Normal 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(ctx, 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{timeoutStatus, 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
server/pkg/go-nfs/nfs_onlink.go
Normal file
94
server/pkg/go-nfs/nfs_onlink.go
Normal 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(ctx, 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(ctx, 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
|
||||
}
|
86
server/pkg/go-nfs/nfs_onlookup.go
Normal file
86
server/pkg/go-nfs/nfs_onlookup.go
Normal 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(ctx, 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(ctx, 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(ctx, 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
server/pkg/go-nfs/nfs_onmkdir.go
Normal file
94
server/pkg/go-nfs/nfs_onmkdir.go
Normal 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(ctx, 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(ctx, 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
server/pkg/go-nfs/nfs_onmknod.go
Normal file
158
server/pkg/go-nfs/nfs_onmknod.go
Normal 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(ctx, 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(ctx, 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
|
||||
}
|
55
server/pkg/go-nfs/nfs_onpathconf.go
Normal file
55
server/pkg/go-nfs/nfs_onpathconf.go
Normal 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(ctx, 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
|
||||
}
|
98
server/pkg/go-nfs/nfs_onread.go
Normal file
98
server/pkg/go-nfs/nfs_onread.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
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(ctx, obj.Handle)
|
||||
if err != nil {
|
||||
return &NFSStatusError{NFSStatusStale, err}
|
||||
}
|
||||
|
||||
fh, err := fs.OpenFile(ctx, fs.Join(path...), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &NFSStatusError{NFSStatusNoEnt, err}
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return &NFSStatusError{timeoutStatus, err}
|
||||
}
|
||||
return &NFSStatusError{NFSStatusAccess, err}
|
||||
}
|
||||
defer fh.Close(ctx)
|
||||
|
||||
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
server/pkg/go-nfs/nfs_onreaddir.go
Normal file
195
server/pkg/go-nfs/nfs_onreaddir.go
Normal 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(ctx, 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(ctx, 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{timeoutStatus, 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)
|
||||
}
|
153
server/pkg/go-nfs/nfs_onreaddirplus.go
Normal file
153
server/pkg/go-nfs/nfs_onreaddirplus.go
Normal 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(ctx, 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(ctx, 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
|
||||
}
|
55
server/pkg/go-nfs/nfs_onreadlink.go
Normal file
55
server/pkg/go-nfs/nfs_onreadlink.go
Normal 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(ctx, 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{timeoutStatus, 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
|
||||
}
|
85
server/pkg/go-nfs/nfs_onremove.go
Normal file
85
server/pkg/go-nfs/nfs_onremove.go
Normal 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(ctx, 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{timeoutStatus, 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(ctx, 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{timeoutStatus, err}
|
||||
}
|
||||
return &NFSStatusError{NFSStatusIO, err}
|
||||
}
|
||||
|
||||
if err := userHandle.InvalidateHandle(ctx, 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
server/pkg/go-nfs/nfs_onrename.go
Normal file
120
server/pkg/go-nfs/nfs_onrename.go
Normal 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(ctx, 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(ctx, 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{timeoutStatus, 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{timeoutStatus, err}
|
||||
}
|
||||
return &NFSStatusError{NFSStatusIO, err}
|
||||
}
|
||||
if !toDirInfo.IsDir() {
|
||||
return &NFSStatusError{NFSStatusNotDir, nil}
|
||||
}
|
||||
preDestData := ToFileAttribute(toDirInfo, toDirPath).AsCache()
|
||||
|
||||
oldHandle := userHandle.ToHandle(ctx, 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{timeoutStatus, err}
|
||||
}
|
||||
return &NFSStatusError{NFSStatusIO, err}
|
||||
}
|
||||
|
||||
if err := userHandle.InvalidateHandle(ctx, 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
|
||||
}
|
9
server/pkg/go-nfs/nfs_onrmdir.go
Normal file
9
server/pkg/go-nfs/nfs_onrmdir.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package nfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func onRmDir(ctx context.Context, w *response, userHandle Handler) error {
|
||||
return onRemove(ctx, w, userHandle)
|
||||
}
|
80
server/pkg/go-nfs/nfs_onsetattr.go
Normal file
80
server/pkg/go-nfs/nfs_onsetattr.go
Normal 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(ctx, 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{timeoutStatus, 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
|
||||
}
|
88
server/pkg/go-nfs/nfs_onsymlink.go
Normal file
88
server/pkg/go-nfs/nfs_onsymlink.go
Normal 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(ctx, 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(ctx, 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
|
||||
}
|
117
server/pkg/go-nfs/nfs_onwrite.go
Normal file
117
server/pkg/go-nfs/nfs_onwrite.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
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(ctx, 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{timeoutStatus, 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}
|
||||
}
|
||||
defer file.Close(ctx)
|
||||
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
|
||||
}
|
294
server/pkg/go-nfs/nfs_test.go
Normal file
294
server/pkg/go-nfs/nfs_test.go
Normal file
|
@ -0,0 +1,294 @@
|
|||
package nfs_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
nfs "git.kmsign.ru/royalcat/tstor/server/pkg/go-nfs"
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/go-nfs/helpers"
|
||||
"git.kmsign.ru/royalcat/tstor/server/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.OpenFile(ctx, "/helloworld.txt", os.O_RDONLY, 0)
|
||||
buf := make([]byte, len(b))
|
||||
if _, err = mf.ReadAt(ctx, buf[:], 0); 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
|
||||
}
|
190
server/pkg/go-nfs/nfsinterface.go
Normal file
190
server/pkg/go-nfs/nfsinterface.go
Normal file
|
@ -0,0 +1,190 @@
|
|||
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"
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutStatus = NFSStatusIO
|
||||
|
||||
// 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
server/pkg/go-nfs/server.go
Normal file
102
server/pkg/go-nfs/server.go
Normal 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
server/pkg/go-nfs/time.go
Normal file
32
server/pkg/go-nfs/time.go
Normal 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)
|
||||
}
|
71
server/pkg/ioutils/cachereader.go
Normal file
71
server/pkg/ioutils/cachereader.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package ioutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/royalcat/ctxio"
|
||||
)
|
||||
|
||||
type FileReader interface {
|
||||
ctxio.ReaderAt
|
||||
ctxio.Reader
|
||||
ctxio.Closer
|
||||
}
|
||||
|
||||
type CacheReader struct {
|
||||
m sync.Mutex
|
||||
|
||||
fo int64
|
||||
fr *FileBuffer
|
||||
to int64
|
||||
tr ctxio.Reader
|
||||
}
|
||||
|
||||
var _ FileReader = (*CacheReader)(nil)
|
||||
|
||||
func NewCacheReader(r ctxio.Reader) (FileReader, error) {
|
||||
fr := NewFileBuffer(nil)
|
||||
tr := ctxio.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 := ctxio.CopyN(ctx, ctxio.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.(ctxio.ReadCloser); ok {
|
||||
closeerr = rc.Close(ctx)
|
||||
}
|
||||
|
||||
return errors.Join(frcloser, closeerr)
|
||||
}
|
72
server/pkg/ioutils/disk.go
Normal file
72
server/pkg/ioutils/disk.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package ioutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/royalcat/ctxio"
|
||||
)
|
||||
|
||||
type DiskCacheReader struct {
|
||||
m sync.Mutex
|
||||
|
||||
fo int64
|
||||
fr *os.File
|
||||
to int64
|
||||
tr ctxio.Reader
|
||||
}
|
||||
|
||||
var _ ctxio.ReaderAt = (*DiskCacheReader)(nil)
|
||||
var _ ctxio.Reader = (*DiskCacheReader)(nil)
|
||||
var _ ctxio.Closer = (*DiskCacheReader)(nil)
|
||||
|
||||
func NewDiskCacheReader(r ctxio.Reader) (*DiskCacheReader, error) {
|
||||
tempDir, err := os.MkdirTemp("/tmp", "tstor")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fr, err := os.CreateTemp(tempDir, "dtb_tmp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr := ctxio.TeeReader(r, ctxio.WrapIoWriter(fr))
|
||||
return &DiskCacheReader{fr: fr, tr: tr}, nil
|
||||
}
|
||||
|
||||
func (dtr *DiskCacheReader) 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 := ctxio.CopyN(ctx, ctxio.Discard, dtr.tr, tb-dtr.fo)
|
||||
dtr.to += w
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
n, err := dtr.fr.ReadAt(p, off)
|
||||
dtr.fo += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (dtr *DiskCacheReader) 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 *DiskCacheReader) Close(ctx context.Context) error {
|
||||
if err := dtr.fr.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Remove(dtr.fr.Name())
|
||||
}
|
205
server/pkg/ioutils/filebuffer.go
Normal file
205
server/pkg/ioutils/filebuffer.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
package ioutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/royalcat/ctxio"
|
||||
)
|
||||
|
||||
// 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
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var _ FileReader = (*FileBuffer)(nil)
|
||||
var _ ctxio.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 ctxio.Reader) (*FileBuffer, error) {
|
||||
data, err := ctxio.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 {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
if f.isClosed || f.index >= int64(f.buff.Len()) {
|
||||
return []byte{}
|
||||
}
|
||||
return bytes.Clone(f.buff.Bytes()[f.index:])
|
||||
}
|
||||
|
||||
// String implements the Stringer interface
|
||||
func (f *FileBuffer) String() string {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
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) {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
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) {
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
|
||||
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) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
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) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
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.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
f.isClosed = true
|
||||
f.buff = nil
|
||||
return nil
|
||||
}
|
49
server/pkg/ioutils/readerat.go
Normal file
49
server/pkg/ioutils/readerat.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package ioutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/royalcat/ctxio"
|
||||
)
|
||||
|
||||
type ReaderReaderAtWrapper struct {
|
||||
mu sync.Mutex
|
||||
rat ctxio.ReaderAt
|
||||
offset int64
|
||||
}
|
||||
|
||||
func NewReaderReaderAtWrapper(rat ctxio.ReaderAt) *ReaderReaderAtWrapper {
|
||||
return &ReaderReaderAtWrapper{
|
||||
rat: rat,
|
||||
}
|
||||
}
|
||||
|
||||
var _ ctxio.Reader = (*ReaderReaderAtWrapper)(nil)
|
||||
var _ ctxio.ReaderAt = (*ReaderReaderAtWrapper)(nil)
|
||||
var _ ctxio.Closer = (*ReaderReaderAtWrapper)(nil)
|
||||
|
||||
// Read implements Reader.
|
||||
func (r *ReaderReaderAtWrapper) Read(ctx context.Context, p []byte) (n int, err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
n, err = r.rat.ReadAt(ctx, p, r.offset)
|
||||
r.offset += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ReadAt implements ReaderAt.
|
||||
func (r *ReaderReaderAtWrapper) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
|
||||
return r.rat.ReadAt(ctx, p, off)
|
||||
}
|
||||
|
||||
// Close implements Closer.
|
||||
func (r *ReaderReaderAtWrapper) Close(ctx context.Context) (err error) {
|
||||
if c, ok := r.rat.(ctxio.Closer); ok {
|
||||
err = c.Close(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
104
server/pkg/ioutils/seeker.go
Normal file
104
server/pkg/ioutils/seeker.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package ioutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/royalcat/ctxio"
|
||||
)
|
||||
|
||||
type ioSeekerWrapper struct {
|
||||
ctx context.Context
|
||||
|
||||
mu sync.Mutex
|
||||
pos int64
|
||||
size int64
|
||||
|
||||
r ctxio.ReaderAt
|
||||
}
|
||||
|
||||
func WrapIoReadSeeker(ctx context.Context, r ctxio.ReaderAt, size int64) io.ReadSeeker {
|
||||
return &ioSeekerWrapper{
|
||||
ctx: ctx,
|
||||
r: r,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ioSeekerWrapper) Seek(offset int64, whence int) (int64, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
r.pos = offset
|
||||
case io.SeekCurrent:
|
||||
r.pos = r.pos + offset
|
||||
case io.SeekEnd:
|
||||
r.pos = r.size + offset
|
||||
}
|
||||
|
||||
return r.pos, nil
|
||||
}
|
||||
|
||||
func (r *ioSeekerWrapper) Read(p []byte) (int, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
n, err := r.r.ReadAt(r.ctx, p, r.pos)
|
||||
r.pos += int64(n)
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
var _ io.ReadSeekCloser = (*ioSeekerCloserWrapper)(nil)
|
||||
|
||||
type ioSeekerCloserWrapper struct {
|
||||
ctx context.Context
|
||||
|
||||
mu sync.Mutex
|
||||
pos int64
|
||||
size int64
|
||||
|
||||
r FileReader
|
||||
}
|
||||
|
||||
func IoReadSeekCloserWrapper(ctx context.Context, r FileReader, size int64) io.ReadSeekCloser {
|
||||
return &ioSeekerCloserWrapper{
|
||||
ctx: ctx,
|
||||
r: r,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ioSeekerCloserWrapper) Seek(offset int64, whence int) (int64, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
r.pos = offset
|
||||
case io.SeekCurrent:
|
||||
r.pos = r.pos + offset
|
||||
case io.SeekEnd:
|
||||
r.pos = r.size + offset
|
||||
}
|
||||
|
||||
return r.pos, nil
|
||||
}
|
||||
|
||||
func (r *ioSeekerCloserWrapper) Read(p []byte) (int, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
n, err := r.r.ReadAt(r.ctx, p, r.pos)
|
||||
r.pos += int64(n)
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close implements io.ReadSeekCloser.
|
||||
func (r *ioSeekerCloserWrapper) Close() error {
|
||||
return r.r.Close(r.ctx)
|
||||
}
|
28
server/pkg/kvsingle/single.go
Normal file
28
server/pkg/kvsingle/single.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package kvsingle
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/royalcat/kv"
|
||||
)
|
||||
|
||||
type Value[K, V any] struct {
|
||||
Key K
|
||||
db kv.Store[K, V]
|
||||
}
|
||||
|
||||
func New[K, V any](db kv.Store[K, V], key K) *Value[K, V] {
|
||||
return &Value[K, V]{Key: key, db: db}
|
||||
}
|
||||
|
||||
func (s *Value[K, V]) Get(ctx context.Context) (V, error) {
|
||||
return s.db.Get(ctx, s.Key)
|
||||
}
|
||||
|
||||
func (s *Value[K, V]) Set(ctx context.Context, value V) error {
|
||||
return s.db.Set(ctx, s.Key, value)
|
||||
}
|
||||
|
||||
func (s *Value[K, V]) Edit(ctx context.Context, edit kv.Edit[V]) error {
|
||||
return s.db.Edit(ctx, s.Key, edit)
|
||||
}
|
150
server/pkg/kvtrace/kvmetrics.go
Normal file
150
server/pkg/kvtrace/kvmetrics.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package kvtrace
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/royalcat/kv"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/royalcat/kv/tracer")
|
||||
|
||||
type traceStore[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 &traceStore[K, V]{
|
||||
kv: kv,
|
||||
attrs: attrs,
|
||||
}
|
||||
}
|
||||
|
||||
// Close implements kv.Store.
|
||||
func (m *traceStore[K, V]) Close(ctx context.Context) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "Close", trace.WithAttributes(m.attrs...))
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err != nil && err != kv.ErrKeyNotFound {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
}()
|
||||
|
||||
return m.kv.Close(ctx)
|
||||
}
|
||||
|
||||
// Delete implements kv.Store.
|
||||
func (m *traceStore[K, V]) Delete(ctx context.Context, k K) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "Delete", trace.WithAttributes(m.attrs...))
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err != nil && err != kv.ErrKeyNotFound {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
}()
|
||||
|
||||
return m.kv.Delete(ctx, k)
|
||||
}
|
||||
|
||||
// Get implements kv.Store.
|
||||
func (m *traceStore[K, V]) Get(ctx context.Context, k K) (v V, err error) {
|
||||
ctx, span := tracer.Start(ctx, "Get", trace.WithAttributes(m.attrs...))
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err != nil && err != kv.ErrKeyNotFound {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
}()
|
||||
|
||||
return m.kv.Get(ctx, k)
|
||||
}
|
||||
|
||||
// Get implements kv.Store.
|
||||
func (m *traceStore[K, V]) Edit(ctx context.Context, k K, edit kv.Edit[V]) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "Get", trace.WithAttributes(m.attrs...))
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err != nil && err != kv.ErrKeyNotFound {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
}()
|
||||
|
||||
return m.kv.Edit(ctx, k, edit)
|
||||
}
|
||||
|
||||
// Range implements kv.Store.
|
||||
func (m *traceStore[K, V]) Range(ctx context.Context, iter kv.Iter[K, V]) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "Range", trace.WithAttributes(m.attrs...))
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
}()
|
||||
|
||||
count := 0
|
||||
iterCount := func(k K, v V) error {
|
||||
count++
|
||||
return iter(k, v)
|
||||
}
|
||||
defer func() {
|
||||
span.SetAttributes(attribute.Int("count", count))
|
||||
}()
|
||||
|
||||
return m.kv.Range(ctx, iterCount)
|
||||
}
|
||||
|
||||
// RangeWithPrefix implements kv.Store.
|
||||
func (m *traceStore[K, V]) RangeWithPrefix(ctx context.Context, k K, iter kv.Iter[K, V]) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "RangeWithPrefix", trace.WithAttributes(m.attrs...))
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
}()
|
||||
|
||||
count := 0
|
||||
iterCount := func(k K, v V) error {
|
||||
count++
|
||||
return iter(k, v)
|
||||
}
|
||||
defer func() {
|
||||
span.SetAttributes(attribute.Int("count", count))
|
||||
}()
|
||||
|
||||
return m.kv.Range(ctx, iterCount)
|
||||
}
|
||||
|
||||
// Set implements kv.Store.
|
||||
func (m *traceStore[K, V]) Set(ctx context.Context, k K, v V) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "Set", trace.WithAttributes(m.attrs...))
|
||||
defer span.End()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
}()
|
||||
|
||||
return m.kv.Set(ctx, k, v)
|
||||
}
|
||||
|
||||
var _ kv.Store[any, any] = (*traceStore[any, any])(nil)
|
114
server/pkg/maxcache/cache.go
Normal file
114
server/pkg/maxcache/cache.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package maxcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/goware/singleflight"
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
func NewCache[K comparable, V any](size int, freshFor, ttl time.Duration) *Cache[K, V] {
|
||||
values, _ := lru.New[K, value[V]](size)
|
||||
return &Cache[K, V]{
|
||||
values: values,
|
||||
}
|
||||
}
|
||||
|
||||
type Cache[K comparable, V any] struct {
|
||||
values *lru.Cache[K, value[V]]
|
||||
|
||||
mu sync.RWMutex
|
||||
callGroup singleflight.Group[K, V]
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) Get(ctx context.Context, key K, fn singleflight.DoFunc[V]) (V, error) {
|
||||
return c.get(ctx, key, false, fn)
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) GetFresh(ctx context.Context, key K, fn singleflight.DoFunc[V]) (V, error) {
|
||||
return c.get(ctx, key, true, fn)
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) Set(ctx context.Context, key K, fn singleflight.DoFunc[V]) (V, bool, error) {
|
||||
v, err, shared := c.callGroup.Do(key, c.set(key, fn))
|
||||
return v, shared, err
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) get(ctx context.Context, key K, freshOnly bool, fn singleflight.DoFunc[V]) (V, error) {
|
||||
c.mu.RLock()
|
||||
val, ok := c.values.Get(key)
|
||||
c.mu.RUnlock()
|
||||
|
||||
// value exists and is fresh - just return
|
||||
if ok && val.IsFresh() {
|
||||
return val.Value(), nil
|
||||
}
|
||||
|
||||
// value exists and is stale, and we're OK with serving it stale while updating in the background
|
||||
// note: stale means its still okay, but not fresh. but if its expired, then it means its useless.
|
||||
if ok && !freshOnly && !val.IsExpired() {
|
||||
// TODO: technically could be a stampede of goroutines here if the value is expired
|
||||
// and we're OK with serving it stale
|
||||
go c.Set(ctx, key, fn)
|
||||
return val.Value(), nil
|
||||
}
|
||||
|
||||
// value doesn't exist or is expired, or is stale and we need it fresh (freshOnly:true) - sync update
|
||||
v, _, err := c.Set(ctx, key, fn)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) set(key K, fn singleflight.DoFunc[V]) singleflight.DoFunc[V] {
|
||||
return singleflight.DoFunc[V](func() (V, error) {
|
||||
val, err := fn()
|
||||
if err != nil {
|
||||
return val, err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.values.Add(key, value[V]{
|
||||
v: val,
|
||||
})
|
||||
c.mu.Unlock()
|
||||
|
||||
return val, nil
|
||||
})
|
||||
}
|
||||
|
||||
type value[V any] struct {
|
||||
v V
|
||||
|
||||
bestBefore time.Time // cache entry freshness cutoff
|
||||
expiry time.Time // cache entry time to live cutoff
|
||||
}
|
||||
|
||||
func (v *value[V]) IsFresh() bool {
|
||||
return v.bestBefore.After(time.Now())
|
||||
}
|
||||
|
||||
func (v *value[V]) IsExpired() bool {
|
||||
return v.expiry.Before(time.Now())
|
||||
}
|
||||
|
||||
func (v *value[V]) Value() V {
|
||||
return v.v
|
||||
}
|
||||
|
||||
func BytesToHash(b ...[]byte) uint64 {
|
||||
d := xxhash.New()
|
||||
for _, v := range b {
|
||||
d.Write(v)
|
||||
}
|
||||
return d.Sum64()
|
||||
}
|
||||
|
||||
func StringToHash(s ...string) uint64 {
|
||||
d := xxhash.New()
|
||||
for _, v := range s {
|
||||
d.WriteString(v)
|
||||
}
|
||||
return d.Sum64()
|
||||
}
|
204
server/pkg/maxcache/cache_test.go
Normal file
204
server/pkg/maxcache/cache_test.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
package maxcache_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/stampede"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
var count uint64
|
||||
cache := stampede.NewCache(512, time.Duration(2*time.Second), time.Duration(5*time.Second))
|
||||
|
||||
// repeat test multiple times
|
||||
for x := 0; x < 5; x++ {
|
||||
// time.Sleep(1 * time.Second)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := runtime.NumGoroutine()
|
||||
|
||||
n := 10
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
t.Logf("numGoroutines now %d", runtime.NumGoroutine())
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
val, err := cache.Get(ctx, "t1", func() (any, error) {
|
||||
t.Log("cache.Get(t1, ...)")
|
||||
|
||||
// some extensive op..
|
||||
time.Sleep(2 * time.Second)
|
||||
atomic.AddUint64(&count, 1)
|
||||
|
||||
return "result1", nil
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result1", val)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// ensure single call
|
||||
assert.Equal(t, uint64(1), count)
|
||||
|
||||
// confirm same before/after num of goroutines
|
||||
t.Logf("numGoroutines now %d", runtime.NumGoroutine())
|
||||
assert.Equal(t, numGoroutines, runtime.NumGoroutine())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
var numRequests = 30
|
||||
|
||||
var hits uint32
|
||||
var expectedStatus int = 201
|
||||
var expectedBody = []byte("hi")
|
||||
|
||||
app := func(w http.ResponseWriter, r *http.Request) {
|
||||
// log.Println("app handler..")
|
||||
|
||||
atomic.AddUint32(&hits, 1)
|
||||
|
||||
hitsNow := atomic.LoadUint32(&hits)
|
||||
if hitsNow > 1 {
|
||||
// panic("uh oh")
|
||||
}
|
||||
|
||||
// time.Sleep(100 * time.Millisecond) // slow handler
|
||||
w.Header().Set("X-Httpjoin", "test")
|
||||
w.WriteHeader(expectedStatus)
|
||||
w.Write(expectedBody)
|
||||
}
|
||||
|
||||
var count uint32
|
||||
counter := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddUint32(&count, 1)
|
||||
next.ServeHTTP(w, r)
|
||||
atomic.AddUint32(&count, ^uint32(0))
|
||||
// log.Println("COUNT:", atomic.LoadUint32(&count))
|
||||
})
|
||||
}
|
||||
|
||||
recoverer := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("recovered panicing request:", r)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
h := stampede.Handler(512, 1*time.Second)
|
||||
|
||||
ts := httptest.NewServer(counter(recoverer(h(http.HandlerFunc(app)))))
|
||||
defer ts.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numRequests; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
resp, err := http.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// log.Println("got resp:", resp, "len:", len(body), "body:", string(body))
|
||||
|
||||
if string(body) != string(expectedBody) {
|
||||
t.Error("expecting response body:", string(expectedBody))
|
||||
}
|
||||
|
||||
if resp.StatusCode != expectedStatus {
|
||||
t.Error("expecting response status:", expectedStatus)
|
||||
}
|
||||
|
||||
assert.Equal(t, "test", resp.Header.Get("X-Httpjoin"), "expecting x-httpjoin test header")
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
totalHits := atomic.LoadUint32(&hits)
|
||||
// if totalHits > 1 {
|
||||
// t.Error("handler was hit more than once. hits:", totalHits)
|
||||
// }
|
||||
log.Println("total hits:", totalHits)
|
||||
|
||||
finalCount := atomic.LoadUint32(&count)
|
||||
if finalCount > 0 {
|
||||
t.Error("queue count was expected to be empty, but count:", finalCount)
|
||||
}
|
||||
log.Println("final count:", finalCount)
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
h1 := stampede.BytesToHash([]byte{1, 2, 3})
|
||||
assert.Equal(t, uint64(8376154270085342629), h1)
|
||||
|
||||
h2 := stampede.StringToHash("123")
|
||||
assert.Equal(t, uint64(4353148100880623749), h2)
|
||||
}
|
||||
|
||||
func TestPanic(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
middleware := stampede.Handler(100, 1*time.Hour)
|
||||
mux.Handle("/", middleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
t.Log(r.Method, r.URL)
|
||||
})))
|
||||
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
t.Log(resp.StatusCode)
|
||||
}
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
t.Log(resp.StatusCode)
|
||||
}
|
||||
}
|
142
server/pkg/rlog/rlog.go
Normal file
142
server/pkg/rlog/rlog.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package rlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
slogmulti "github.com/samber/slog-multi"
|
||||
slogzerolog "github.com/samber/slog-zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
zl = zerolog.New(&zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
handlers = []slog.Handler{
|
||||
slogzerolog.Option{Logger: &zl}.NewZerologHandler(),
|
||||
}
|
||||
handler = slogmulti.Fanout(handlers...)
|
||||
defaultLogger = slog.New(handler)
|
||||
)
|
||||
|
||||
func init() {
|
||||
slog.SetDefault(defaultLogger)
|
||||
}
|
||||
|
||||
func AddHandler(nh slog.Handler) {
|
||||
handlers = append(handlers, nh)
|
||||
handler = slogmulti.Fanout(handlers...)
|
||||
defaultLogger = slog.New(handler)
|
||||
slog.SetDefault(defaultLogger)
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
handler slog.Handler
|
||||
callNesting int
|
||||
component []string
|
||||
}
|
||||
|
||||
const functionKey = "function"
|
||||
|
||||
const componentKey = "component"
|
||||
const componentSep = "."
|
||||
|
||||
func (l *Logger) log(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(3+l.callNesting, pcs[:])
|
||||
pc := pcs[0]
|
||||
f := runtime.FuncForPC(pc)
|
||||
if f != nil {
|
||||
attrs = append(attrs, slog.String(functionKey, f.Name()))
|
||||
}
|
||||
|
||||
if len(l.component) > 0 {
|
||||
attrs = append(attrs, slog.String(componentKey, strings.Join(l.component, componentSep)))
|
||||
}
|
||||
|
||||
r := slog.NewRecord(time.Now(), level, msg, pc)
|
||||
r.AddAttrs(attrs...)
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
_ = l.handler.Handle(ctx, r)
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(ctx context.Context, msg string, attrs ...slog.Attr) {
|
||||
l.log(ctx, slog.LevelDebug, msg, attrs...)
|
||||
}
|
||||
|
||||
func (l *Logger) Info(ctx context.Context, msg string, attrs ...slog.Attr) {
|
||||
l.log(ctx, slog.LevelInfo, msg, attrs...)
|
||||
}
|
||||
|
||||
func (l *Logger) Warn(ctx context.Context, msg string, attrs ...slog.Attr) {
|
||||
l.log(ctx, slog.LevelWarn, msg, attrs...)
|
||||
}
|
||||
|
||||
func (l *Logger) Error(ctx context.Context, msg string, attrs ...slog.Attr) {
|
||||
l.log(ctx, slog.LevelError, msg, attrs...)
|
||||
}
|
||||
|
||||
func (log *Logger) WithComponent(name string) *Logger {
|
||||
return &Logger{
|
||||
handler: log.handler,
|
||||
component: append(log.component, name),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) With(attrs ...slog.Attr) *Logger {
|
||||
return &Logger{
|
||||
handler: l.handler.WithAttrs(attrs),
|
||||
component: l.component,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Nested(callNesting int) *Logger {
|
||||
return &Logger{
|
||||
handler: l.handler,
|
||||
component: l.component,
|
||||
callNesting: callNesting,
|
||||
}
|
||||
}
|
||||
|
||||
// returns a new slog logger with the same attribures as the original logger
|
||||
// TODO currently not logging function name
|
||||
func (l *Logger) Slog() *slog.Logger {
|
||||
return slog.New(l.handler)
|
||||
}
|
||||
|
||||
const errKey = "error"
|
||||
|
||||
func Error(err error) slog.Attr {
|
||||
return slog.Attr{Key: errKey, Value: errValue(err)}
|
||||
}
|
||||
|
||||
// errValue returns a slog.GroupValue with keys "msg" and "trace". If the error
|
||||
// does not implement interface { StackTrace() errors.StackTrace }, the "trace"
|
||||
// key is omitted.
|
||||
func errValue(err error) slog.Value {
|
||||
if err == nil {
|
||||
return slog.AnyValue(nil)
|
||||
}
|
||||
|
||||
var groupValues []slog.Attr
|
||||
|
||||
groupValues = append(groupValues,
|
||||
slog.String("msg", err.Error()),
|
||||
slog.Any("value", err),
|
||||
)
|
||||
|
||||
return slog.GroupValue(groupValues...)
|
||||
}
|
||||
|
||||
func Component(name ...string) *Logger {
|
||||
return &Logger{
|
||||
handler: handler,
|
||||
component: name,
|
||||
}
|
||||
}
|
38
server/pkg/slicesutils/intersections.go
Normal file
38
server/pkg/slicesutils/intersections.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package slicesutils
|
||||
|
||||
func Intersection[T comparable](slices ...[]T) []T {
|
||||
counts := map[T]int{}
|
||||
result := []T{}
|
||||
|
||||
for _, slice := range slices {
|
||||
for _, val := range slice {
|
||||
counts[val]++
|
||||
}
|
||||
}
|
||||
|
||||
for val, count := range counts {
|
||||
if count == len(slices) {
|
||||
|
||||
result = append(result, val)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
func IntersectionFunc[T any](s1 []T, s2 []T, cmp func(T, T) bool) []T {
|
||||
set := make([]T, 0)
|
||||
|
||||
for _, a := range s1 {
|
||||
for _, b := range s2 {
|
||||
if cmp(a, b) {
|
||||
set = append(set, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
|
||||
}
|
112
server/pkg/uring/file.go
Normal file
112
server/pkg/uring/file.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
package uring
|
||||
|
||||
// import (
|
||||
// "context"
|
||||
// "os"
|
||||
|
||||
// "github.com/iceber/iouring-go"
|
||||
// "go.opentelemetry.io/otel"
|
||||
// "go.opentelemetry.io/otel/attribute"
|
||||
// "go.opentelemetry.io/otel/trace"
|
||||
// )
|
||||
|
||||
// var tracer = otel.Tracer("github.com/royalcat/tstor/pkg/uring")
|
||||
|
||||
// type FS struct {
|
||||
// ur *iouring.IOURing
|
||||
// }
|
||||
|
||||
// func NewFS(ur *iouring.IOURing) *FS {
|
||||
// return &FS{
|
||||
// ur: ur,
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (o *FS) OpenFile(ctx context.Context, name string) (File, error) {
|
||||
// ctx, span := tracer.Start(ctx, "uring.FS.OpenFile", trace.WithAttributes(attribute.String("name", name)))
|
||||
// defer span.End()
|
||||
|
||||
// f, err := os.Open(name)
|
||||
// if err != nil {
|
||||
// return File{}, err
|
||||
// }
|
||||
|
||||
// return File{
|
||||
// ur: o.ur,
|
||||
// f: f,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// func NewFile(ur *iouring.IOURing, f *os.File) *File {
|
||||
// return &File{
|
||||
// ur: ur,
|
||||
// f: f,
|
||||
// }
|
||||
// }
|
||||
|
||||
// type File struct {
|
||||
// ur *iouring.IOURing
|
||||
// f *os.File
|
||||
// }
|
||||
|
||||
// func (o *File) pread(ctx context.Context, b []byte, off uint64) (int, error) {
|
||||
// ctx, span := tracer.Start(ctx, "uring.File.pread", trace.WithAttributes(attribute.Int("size", len(b))))
|
||||
// defer span.End()
|
||||
|
||||
// req, err := o.ur.Pread(o.f, b, off, nil)
|
||||
// if err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
|
||||
// select {
|
||||
// case <-req.Done():
|
||||
// return req.GetRes()
|
||||
// case <-ctx.Done():
|
||||
// if _, err := req.Cancel(); err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
// <-req.Done()
|
||||
// return 0, ctx.Err()
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (f *File) ReadAt(ctx context.Context, b []byte, off int64) (n int, err error) {
|
||||
// ctx, span := tracer.Start(ctx, "uring.File.ReadAt", trace.WithAttributes(attribute.Int("size", len(b))))
|
||||
// defer span.End()
|
||||
|
||||
// return f.f.ReadAt(b, off)
|
||||
|
||||
// for len(b) > 0 {
|
||||
// if ctx.Err() != nil {
|
||||
// err = ctx.Err()
|
||||
// break
|
||||
// }
|
||||
|
||||
// m, e := f.pread(ctx, b, uint64(off))
|
||||
// if e != nil {
|
||||
// err = e
|
||||
// break
|
||||
// }
|
||||
// n += m
|
||||
// b = b[m:]
|
||||
// off += int64(m)
|
||||
// }
|
||||
|
||||
// return n, err
|
||||
// }
|
||||
|
||||
// func (o *File) Close(ctx context.Context) error {
|
||||
// return o.f.Close()
|
||||
// }
|
||||
|
||||
// func waitRequest(ctx context.Context, req iouring.Request) (int, error) {
|
||||
// select {
|
||||
// case <-req.Done():
|
||||
// return req.GetRes()
|
||||
// case <-ctx.Done():
|
||||
// if _, err := req.Cancel(); err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
// return 0, ctx.Err()
|
||||
// }
|
||||
// }
|
102
server/pkg/uuid/uuid.go
Normal file
102
server/pkg/uuid/uuid.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
fuuid "github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
var Nil = UUID{}
|
||||
|
||||
type UUIDList = []UUID
|
||||
|
||||
type UUID struct {
|
||||
fuuid.UUID
|
||||
}
|
||||
|
||||
func New() UUID {
|
||||
return UUID{fuuid.Must(fuuid.NewV7())}
|
||||
}
|
||||
|
||||
func NewFromTime(t time.Time) UUID {
|
||||
gen := fuuid.NewGenWithOptions(
|
||||
fuuid.WithEpochFunc(func() time.Time { return t }),
|
||||
)
|
||||
return UUID{fuuid.Must(gen.NewV7())}
|
||||
}
|
||||
|
||||
func NewP() *UUID {
|
||||
return &UUID{fuuid.Must(fuuid.NewV7())}
|
||||
}
|
||||
|
||||
func FromString(text string) (UUID, error) {
|
||||
u, err := fuuid.FromString(text)
|
||||
if err != nil {
|
||||
return Nil, err
|
||||
}
|
||||
|
||||
return UUID{u}, nil
|
||||
}
|
||||
|
||||
func MustFromString(text string) UUID {
|
||||
u, err := fuuid.FromString(text)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return UUID{u}
|
||||
}
|
||||
|
||||
func FromBytes(input []byte) (UUID, error) {
|
||||
u, err := fuuid.FromBytes(input)
|
||||
if err != nil {
|
||||
return Nil, err
|
||||
}
|
||||
|
||||
return UUID{u}, nil
|
||||
}
|
||||
|
||||
func (a *UUID) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
a.UUID = fuuid.Nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return a.UUID.Parse(s)
|
||||
}
|
||||
|
||||
func (a UUID) MarshalJSON() ([]byte, error) {
|
||||
if a.IsNil() {
|
||||
return json.Marshal("")
|
||||
}
|
||||
|
||||
return json.Marshal(a.UUID)
|
||||
}
|
||||
|
||||
// UnmarshalGQL implements the graphql.Unmarshaler interface
|
||||
func (u *UUID) UnmarshalGQL(v interface{}) error {
|
||||
id, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("uuid must be a string")
|
||||
}
|
||||
|
||||
return u.Parse(id)
|
||||
}
|
||||
|
||||
// MarshalGQL implements the graphql.Marshaler interface
|
||||
func (u UUID) MarshalGQL(w io.Writer) {
|
||||
b := []byte(strconv.Quote(u.String()))
|
||||
_, err := w.Write(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
9
server/pkg/ytdlp/client.go
Normal file
9
server/pkg/ytdlp/client.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package ytdlp
|
||||
|
||||
type Client struct {
|
||||
binary string
|
||||
}
|
||||
|
||||
func New() (*Client, error) {
|
||||
return &Client{binary: "yt-dlp"}, nil
|
||||
}
|
105
server/pkg/ytdlp/download.go
Normal file
105
server/pkg/ytdlp/download.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package ytdlp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/royalcat/ctxprogress"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type DownloadStatus string
|
||||
|
||||
const (
|
||||
StatusDownloading DownloadStatus = "downloading"
|
||||
StatusFinished DownloadStatus = "finished"
|
||||
StatusErrored DownloadStatus = "error"
|
||||
)
|
||||
|
||||
// Progress for the Running call
|
||||
type DownloadProgress struct {
|
||||
Status DownloadStatus `json:"status"`
|
||||
Filename string `json:"filename"`
|
||||
TmpFilename string `json:"tmpfilename"`
|
||||
DownloadedBytes int64 `json:"downloaded_bytes"`
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
TotalBytesEstimate float64 `json:"total_bytes_estimate"`
|
||||
Elapsed float64 `json:"elapsed"`
|
||||
ETA float64 `json:"eta"`
|
||||
Speed float64 `json:"speed"`
|
||||
FragmentIndex int64 `json:"fragment_index"`
|
||||
FragmentCount int64 `json:"fragment_count"`
|
||||
}
|
||||
|
||||
// Current implements ctxprogress.Progress.
|
||||
func (d DownloadProgress) Progress() (int, int) {
|
||||
if d.TotalBytes != -1 && d.TotalBytes != 0 && d.DownloadedBytes != -1 {
|
||||
return int(d.DownloadedBytes), int(d.TotalBytes)
|
||||
}
|
||||
|
||||
if d.TotalBytesEstimate != -1 && d.TotalBytesEstimate != 0 && d.DownloadedBytes != -1 {
|
||||
return int(d.DownloadedBytes), int(d.TotalBytesEstimate)
|
||||
}
|
||||
|
||||
return int(d.FragmentIndex), int(d.FragmentCount)
|
||||
}
|
||||
|
||||
const rawProgressTemplate = `download:
|
||||
%{
|
||||
"status":"%(progress.status)s",
|
||||
"eta":%(progress.eta|-1)s,
|
||||
"speed":%(progress.speed|0)s,
|
||||
"downloaded_bytes":%(progress.downloaded_bytes|-1)s,
|
||||
"total_bytes": %(progress.total_bytes|-1)s,
|
||||
"total_bytes_estimate": %(progress.total_bytes_estimate|-1)s,
|
||||
"fragment_index":%(progress.fragment_index|-1)s,
|
||||
"fragment_count":%(progress.fragment_count|-1)s
|
||||
}`
|
||||
|
||||
var progressTemplate = strings.NewReplacer("\n", "", "\t", "", " ", "").Replace(rawProgressTemplate)
|
||||
|
||||
func (c *Client) Download(ctx context.Context, url string, w io.Writer) error {
|
||||
args := []string{
|
||||
"--progress", "--newline", "--progress-template", progressTemplate,
|
||||
"-o", "-",
|
||||
url,
|
||||
}
|
||||
|
||||
group, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
stderr, lines, err := lineReader(group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, c.binary, args...)
|
||||
|
||||
cmd.Stdout = w
|
||||
cmd.Stderr = stderr
|
||||
|
||||
group.Go(func() error {
|
||||
err := cmd.Run()
|
||||
stderr.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
for line := range lines {
|
||||
if line, ok := strings.CutPrefix(line, "%"); ok {
|
||||
p := DownloadProgress{}
|
||||
err = json.Unmarshal([]byte(line), &p)
|
||||
if err != nil {
|
||||
//TODO: handle error
|
||||
continue
|
||||
}
|
||||
|
||||
ctxprogress.Set(ctx, p)
|
||||
}
|
||||
}
|
||||
|
||||
return group.Wait()
|
||||
}
|
27
server/pkg/ytdlp/download_test.go
Normal file
27
server/pkg/ytdlp/download_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package ytdlp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/ytdlp"
|
||||
"github.com/royalcat/ctxprogress"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDownload(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := ytdlp.New()
|
||||
require.NoError(err)
|
||||
ctx = ctxprogress.New(ctx)
|
||||
ctxprogress.AddCallback(ctx, func(p ctxprogress.Progress) {
|
||||
cur, total := p.Progress()
|
||||
fmt.Printf("%d/%d\n", cur, total)
|
||||
})
|
||||
err = c.Download(ctx, "https://www.youtube.com/watch?v=dQw4w9WgXcQ", io.Discard)
|
||||
require.NoError(err)
|
||||
}
|
33
server/pkg/ytdlp/info.go
Normal file
33
server/pkg/ytdlp/info.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package ytdlp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (c *Client) Info(ctx context.Context, url string) (*Info, error) {
|
||||
args := []string{
|
||||
"-q", "-J", url,
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, c.binary, args...)
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var info Info
|
||||
err = json.Unmarshal(stdout.Bytes(), &info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
389
server/pkg/ytdlp/model.go
Normal file
389
server/pkg/ytdlp/model.go
Normal file
|
@ -0,0 +1,389 @@
|
|||
package ytdlp
|
||||
|
||||
type Info struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Availability string `json:"availability"`
|
||||
ChannelFollowerCount *int64 `json:"channel_follower_count"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
Thumbnails []Thumbnail `json:"thumbnails"`
|
||||
ModifiedDate *string `json:"modified_date,omitempty"`
|
||||
ViewCount int64 `json:"view_count"`
|
||||
PlaylistCount *int64 `json:"playlist_count,omitempty"`
|
||||
Channel string `json:"channel"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
UploaderID string `json:"uploader_id"`
|
||||
Uploader string `json:"uploader"`
|
||||
ChannelURL string `json:"channel_url"`
|
||||
UploaderURL string `json:"uploader_url"`
|
||||
Type string `json:"_type"`
|
||||
Entries []Entry `json:"entries,omitempty"`
|
||||
ExtractorKey string `json:"extractor_key"`
|
||||
Extractor string `json:"extractor"`
|
||||
WebpageURL string `json:"webpage_url"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
WebpageURLBasename string `json:"webpage_url_basename"`
|
||||
WebpageURLDomain string `json:"webpage_url_domain"`
|
||||
ReleaseYear interface{} `json:"release_year"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
FilesToMove *FilesToMove `json:"__files_to_move,omitempty"`
|
||||
Version Version `json:"_version"`
|
||||
Formats []Format `json:"formats,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
Duration *int64 `json:"duration,omitempty"`
|
||||
AverageRating interface{} `json:"average_rating"`
|
||||
AgeLimit *int64 `json:"age_limit,omitempty"`
|
||||
Categories []string `json:"categories,omitempty"`
|
||||
PlayableInEmbed *bool `json:"playable_in_embed,omitempty"`
|
||||
LiveStatus *string `json:"live_status,omitempty"`
|
||||
ReleaseTimestamp interface{} `json:"release_timestamp"`
|
||||
FormatSortFields []string `json:"_format_sort_fields,omitempty"`
|
||||
AutomaticCaptions map[string][]AutomaticCaption `json:"automatic_captions,omitempty"`
|
||||
Subtitles *FilesToMove `json:"subtitles,omitempty"`
|
||||
CommentCount *int64 `json:"comment_count,omitempty"`
|
||||
Chapters interface{} `json:"chapters"`
|
||||
Heatmap []Heatmap `json:"heatmap,omitempty"`
|
||||
LikeCount *int64 `json:"like_count,omitempty"`
|
||||
ChannelIsVerified *bool `json:"channel_is_verified,omitempty"`
|
||||
UploadDate *string `json:"upload_date,omitempty"`
|
||||
Timestamp *int64 `json:"timestamp,omitempty"`
|
||||
Playlist interface{} `json:"playlist"`
|
||||
PlaylistIndex interface{} `json:"playlist_index"`
|
||||
DisplayID *string `json:"display_id,omitempty"`
|
||||
Fulltitle *string `json:"fulltitle,omitempty"`
|
||||
DurationString *string `json:"duration_string,omitempty"`
|
||||
IsLive *bool `json:"is_live,omitempty"`
|
||||
WasLive *bool `json:"was_live,omitempty"`
|
||||
RequestedSubtitles interface{} `json:"requested_subtitles"`
|
||||
HasDRM interface{} `json:"_has_drm"`
|
||||
RequestedDownloads []RequestedDownload `json:"requested_downloads,omitempty"`
|
||||
RequestedFormats []Format `json:"requested_formats,omitempty"`
|
||||
Format *string `json:"format,omitempty"`
|
||||
FormatID *string `json:"format_id,omitempty"`
|
||||
EXT *MediaEXT `json:"ext,omitempty"`
|
||||
Protocol *string `json:"protocol,omitempty"`
|
||||
Language *Language `json:"language,omitempty"`
|
||||
FormatNote *string `json:"format_note,omitempty"`
|
||||
FilesizeApprox *int64 `json:"filesize_approx,omitempty"`
|
||||
Tbr *float64 `json:"tbr,omitempty"`
|
||||
Width *int64 `json:"width,omitempty"`
|
||||
Height *int64 `json:"height,omitempty"`
|
||||
Resolution *Resolution `json:"resolution,omitempty"`
|
||||
FPS *int64 `json:"fps,omitempty"`
|
||||
DynamicRange *DynamicRange `json:"dynamic_range,omitempty"`
|
||||
Vcodec *string `json:"vcodec,omitempty"`
|
||||
Vbr *float64 `json:"vbr,omitempty"`
|
||||
StretchedRatio interface{} `json:"stretched_ratio"`
|
||||
AspectRatio *float64 `json:"aspect_ratio,omitempty"`
|
||||
Acodec *Acodec `json:"acodec,omitempty"`
|
||||
ABR *float64 `json:"abr,omitempty"`
|
||||
ASR *int64 `json:"asr,omitempty"`
|
||||
AudioChannels *int64 `json:"audio_channels,omitempty"`
|
||||
}
|
||||
|
||||
type AutomaticCaption struct {
|
||||
EXT AutomaticCaptionEXT `json:"ext"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Formats []Format `json:"formats"`
|
||||
Thumbnails []Thumbnail `json:"thumbnails"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Description string `json:"description"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ChannelURL string `json:"channel_url"`
|
||||
Duration int64 `json:"duration"`
|
||||
ViewCount int64 `json:"view_count"`
|
||||
AverageRating interface{} `json:"average_rating"`
|
||||
AgeLimit int64 `json:"age_limit"`
|
||||
WebpageURL string `json:"webpage_url"`
|
||||
Categories []string `json:"categories"`
|
||||
Tags []string `json:"tags"`
|
||||
PlayableInEmbed bool `json:"playable_in_embed"`
|
||||
LiveStatus string `json:"live_status"`
|
||||
ReleaseTimestamp interface{} `json:"release_timestamp"`
|
||||
FormatSortFields []string `json:"_format_sort_fields"`
|
||||
AutomaticCaptions map[string][]AutomaticCaption `json:"automatic_captions"`
|
||||
Subtitles FilesToMove `json:"subtitles"`
|
||||
CommentCount int64 `json:"comment_count"`
|
||||
Chapters interface{} `json:"chapters"`
|
||||
Heatmap interface{} `json:"heatmap"`
|
||||
LikeCount int64 `json:"like_count"`
|
||||
Channel string `json:"channel"`
|
||||
ChannelFollowerCount int64 `json:"channel_follower_count"`
|
||||
Uploader string `json:"uploader"`
|
||||
UploaderID string `json:"uploader_id"`
|
||||
UploaderURL string `json:"uploader_url"`
|
||||
UploadDate string `json:"upload_date"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Availability string `json:"availability"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
WebpageURLBasename string `json:"webpage_url_basename"`
|
||||
WebpageURLDomain string `json:"webpage_url_domain"`
|
||||
Extractor string `json:"extractor"`
|
||||
ExtractorKey string `json:"extractor_key"`
|
||||
PlaylistCount int64 `json:"playlist_count"`
|
||||
Playlist string `json:"playlist"`
|
||||
PlaylistID string `json:"playlist_id"`
|
||||
PlaylistTitle string `json:"playlist_title"`
|
||||
PlaylistUploader string `json:"playlist_uploader"`
|
||||
PlaylistUploaderID string `json:"playlist_uploader_id"`
|
||||
NEntries int64 `json:"n_entries"`
|
||||
PlaylistIndex int64 `json:"playlist_index"`
|
||||
LastPlaylistIndex int64 `json:"__last_playlist_index"`
|
||||
PlaylistAutonumber int64 `json:"playlist_autonumber"`
|
||||
DisplayID string `json:"display_id"`
|
||||
Fulltitle string `json:"fulltitle"`
|
||||
DurationString string `json:"duration_string"`
|
||||
ReleaseYear interface{} `json:"release_year"`
|
||||
IsLive bool `json:"is_live"`
|
||||
WasLive bool `json:"was_live"`
|
||||
RequestedSubtitles interface{} `json:"requested_subtitles"`
|
||||
HasDRM interface{} `json:"_has_drm"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
RequestedDownloads []RequestedDownload `json:"requested_downloads"`
|
||||
RequestedFormats []Format `json:"requested_formats"`
|
||||
Format string `json:"format"`
|
||||
FormatID string `json:"format_id"`
|
||||
EXT string `json:"ext"`
|
||||
Protocol string `json:"protocol"`
|
||||
Language *Language `json:"language"`
|
||||
FormatNote string `json:"format_note"`
|
||||
FilesizeApprox int64 `json:"filesize_approx"`
|
||||
Tbr float64 `json:"tbr"`
|
||||
Width int64 `json:"width"`
|
||||
Height int64 `json:"height"`
|
||||
Resolution Resolution `json:"resolution"`
|
||||
FPS int64 `json:"fps"`
|
||||
DynamicRange DynamicRange `json:"dynamic_range"`
|
||||
Vcodec string `json:"vcodec"`
|
||||
Vbr float64 `json:"vbr"`
|
||||
StretchedRatio interface{} `json:"stretched_ratio"`
|
||||
AspectRatio float64 `json:"aspect_ratio"`
|
||||
Acodec Acodec `json:"acodec"`
|
||||
ABR float64 `json:"abr"`
|
||||
ASR int64 `json:"asr"`
|
||||
AudioChannels int64 `json:"audio_channels"`
|
||||
}
|
||||
|
||||
type Format struct {
|
||||
FormatID string `json:"format_id"`
|
||||
FormatNote *FormatNote `json:"format_note,omitempty"`
|
||||
EXT MediaEXT `json:"ext"`
|
||||
Protocol Protocol `json:"protocol"`
|
||||
Acodec *Acodec `json:"acodec,omitempty"`
|
||||
Vcodec string `json:"vcodec"`
|
||||
URL string `json:"url"`
|
||||
Width *int64 `json:"width"`
|
||||
Height *int64 `json:"height"`
|
||||
FPS *float64 `json:"fps"`
|
||||
Rows *int64 `json:"rows,omitempty"`
|
||||
Columns *int64 `json:"columns,omitempty"`
|
||||
Fragments []Fragment `json:"fragments,omitempty"`
|
||||
Resolution Resolution `json:"resolution"`
|
||||
AspectRatio *float64 `json:"aspect_ratio"`
|
||||
FilesizeApprox *int64 `json:"filesize_approx"`
|
||||
HTTPHeaders HTTPHeaders `json:"http_headers"`
|
||||
AudioEXT MediaEXT `json:"audio_ext"`
|
||||
VideoEXT MediaEXT `json:"video_ext"`
|
||||
Vbr *float64 `json:"vbr"`
|
||||
ABR *float64 `json:"abr"`
|
||||
Tbr *float64 `json:"tbr"`
|
||||
Format string `json:"format"`
|
||||
FormatIndex interface{} `json:"format_index"`
|
||||
ManifestURL *string `json:"manifest_url,omitempty"`
|
||||
Language *Language `json:"language"`
|
||||
Preference interface{} `json:"preference"`
|
||||
Quality *float64 `json:"quality,omitempty"`
|
||||
HasDRM *bool `json:"has_drm,omitempty"`
|
||||
SourcePreference *int64 `json:"source_preference,omitempty"`
|
||||
ASR *int64 `json:"asr"`
|
||||
Filesize *int64 `json:"filesize"`
|
||||
AudioChannels *int64 `json:"audio_channels"`
|
||||
LanguagePreference *int64 `json:"language_preference,omitempty"`
|
||||
DynamicRange *DynamicRange `json:"dynamic_range"`
|
||||
Container *Container `json:"container,omitempty"`
|
||||
DownloaderOptions *DownloaderOptions `json:"downloader_options,omitempty"`
|
||||
}
|
||||
|
||||
type DownloaderOptions struct {
|
||||
HTTPChunkSize int64 `json:"http_chunk_size"`
|
||||
}
|
||||
|
||||
type Fragment struct {
|
||||
URL string `json:"url"`
|
||||
Duration float64 `json:"duration"`
|
||||
}
|
||||
|
||||
type HTTPHeaders struct {
|
||||
UserAgent string `json:"User-Agent"`
|
||||
Accept Accept `json:"Accept"`
|
||||
AcceptLanguage AcceptLanguage `json:"Accept-Language"`
|
||||
SECFetchMode SECFetchMode `json:"Sec-Fetch-Mode"`
|
||||
}
|
||||
|
||||
type RequestedDownload struct {
|
||||
RequestedFormats []Format `json:"requested_formats"`
|
||||
Format string `json:"format"`
|
||||
FormatID string `json:"format_id"`
|
||||
EXT string `json:"ext"`
|
||||
Protocol string `json:"protocol"`
|
||||
FormatNote string `json:"format_note"`
|
||||
FilesizeApprox int64 `json:"filesize_approx"`
|
||||
Tbr float64 `json:"tbr"`
|
||||
Width int64 `json:"width"`
|
||||
Height int64 `json:"height"`
|
||||
Resolution Resolution `json:"resolution"`
|
||||
FPS int64 `json:"fps"`
|
||||
DynamicRange DynamicRange `json:"dynamic_range"`
|
||||
Vcodec string `json:"vcodec"`
|
||||
Vbr float64 `json:"vbr"`
|
||||
AspectRatio float64 `json:"aspect_ratio"`
|
||||
Acodec Acodec `json:"acodec"`
|
||||
ABR float64 `json:"abr"`
|
||||
ASR int64 `json:"asr"`
|
||||
AudioChannels int64 `json:"audio_channels"`
|
||||
FilenameOld string `json:"_filename"`
|
||||
Filename string `json:"filename"`
|
||||
WriteDownloadArchive bool `json:"__write_download_archive"`
|
||||
Language *Language `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
type FilesToMove struct {
|
||||
}
|
||||
|
||||
type Thumbnail struct {
|
||||
URL string `json:"url"`
|
||||
Preference *int64 `json:"preference,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Height *int64 `json:"height,omitempty"`
|
||||
Width *int64 `json:"width,omitempty"`
|
||||
Resolution *string `json:"resolution,omitempty"`
|
||||
}
|
||||
|
||||
type Heatmap struct {
|
||||
StartTime float64 `json:"start_time"`
|
||||
EndTime float64 `json:"end_time"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
Version string `json:"version"`
|
||||
CurrentGitHead interface{} `json:"current_git_head"`
|
||||
ReleaseGitHead string `json:"release_git_head"`
|
||||
Repository string `json:"repository"`
|
||||
}
|
||||
|
||||
type Acodec string
|
||||
|
||||
const (
|
||||
AcodecNone Acodec = "none"
|
||||
Mp4A402 Acodec = "mp4a.40.2"
|
||||
Mp4A405 Acodec = "mp4a.40.5"
|
||||
Opus Acodec = "opus"
|
||||
)
|
||||
|
||||
type AutomaticCaptionEXT string
|
||||
|
||||
const (
|
||||
Json3 AutomaticCaptionEXT = "json3"
|
||||
Srv1 AutomaticCaptionEXT = "srv1"
|
||||
Srv2 AutomaticCaptionEXT = "srv2"
|
||||
Srv3 AutomaticCaptionEXT = "srv3"
|
||||
Ttml AutomaticCaptionEXT = "ttml"
|
||||
Vtt AutomaticCaptionEXT = "vtt"
|
||||
)
|
||||
|
||||
type DynamicRange string
|
||||
|
||||
const (
|
||||
SDR DynamicRange = "SDR"
|
||||
HDR DynamicRange = "HDR"
|
||||
)
|
||||
|
||||
type MediaEXT string
|
||||
|
||||
const (
|
||||
EXTNone MediaEXT = "none"
|
||||
EXTMhtml MediaEXT = "mhtml"
|
||||
M4A MediaEXT = "m4a"
|
||||
Mp4 MediaEXT = "mp4"
|
||||
Webm MediaEXT = "webm"
|
||||
)
|
||||
|
||||
type Container string
|
||||
|
||||
const (
|
||||
M4ADash Container = "m4a_dash"
|
||||
Mp4Dash Container = "mp4_dash"
|
||||
WebmDash Container = "webm_dash"
|
||||
)
|
||||
|
||||
type FormatNote string
|
||||
|
||||
const (
|
||||
Default FormatNote = "Default"
|
||||
Low FormatNote = "low"
|
||||
Medium FormatNote = "medium"
|
||||
Premium FormatNote = "Premium"
|
||||
Storyboard FormatNote = "storyboard"
|
||||
The1080P FormatNote = "1080p"
|
||||
The144P FormatNote = "144p"
|
||||
The240P FormatNote = "240p"
|
||||
The360P FormatNote = "360p"
|
||||
The480P FormatNote = "480p"
|
||||
The720P FormatNote = "720p"
|
||||
)
|
||||
|
||||
type Accept string
|
||||
|
||||
const (
|
||||
TextHTMLApplicationXHTMLXMLApplicationXMLQ09Q08 Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||
)
|
||||
|
||||
type AcceptLanguage string
|
||||
|
||||
const (
|
||||
EnUsEnQ05 AcceptLanguage = "en-us,en;q=0.5"
|
||||
)
|
||||
|
||||
type SECFetchMode string
|
||||
|
||||
const (
|
||||
Navigate SECFetchMode = "navigate"
|
||||
)
|
||||
|
||||
type Language string
|
||||
|
||||
const (
|
||||
En Language = "en"
|
||||
)
|
||||
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
HTTPS Protocol = "https"
|
||||
M3U8Native Protocol = "m3u8_native"
|
||||
ProtocolMhtml Protocol = "mhtml"
|
||||
)
|
||||
|
||||
type Resolution string
|
||||
|
||||
const (
|
||||
AudioOnly Resolution = "audio only"
|
||||
The1280X720 Resolution = "1280x720"
|
||||
The160X90 Resolution = "160x90"
|
||||
The1920X1080 Resolution = "1920x1080"
|
||||
The256X144 Resolution = "256x144"
|
||||
The320X180 Resolution = "320x180"
|
||||
The426X240 Resolution = "426x240"
|
||||
The48X27 Resolution = "48x27"
|
||||
The640X360 Resolution = "640x360"
|
||||
The80X45 Resolution = "80x45"
|
||||
The854X480 Resolution = "854x480"
|
||||
)
|
115
server/pkg/ytdlp/playlist.go
Normal file
115
server/pkg/ytdlp/playlist.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
package ytdlp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/royalcat/ctxprogress"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// Progress implements ctxprogress.Progress.
|
||||
func (p Entry) Progress() (current int, total int) {
|
||||
return int(p.PlaylistIndex), int(p.PlaylistCount)
|
||||
}
|
||||
|
||||
// func (p PlaylistEntry) Url() string {
|
||||
// if p.URL != "" {
|
||||
// return p.URL
|
||||
// }
|
||||
// if p.WebpageURL != "" {
|
||||
// return p.WebpageURL
|
||||
// }
|
||||
// if p.OriginalURL != "" {
|
||||
// return p.OriginalURL
|
||||
// }
|
||||
|
||||
// return ""
|
||||
// }
|
||||
|
||||
func (yt *Client) Playlist(ctx context.Context, url string) ([]Entry, error) {
|
||||
group, ctx := errgroup.WithContext(ctx)
|
||||
w, lines, err := lineReader(group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, yt.binary, "-j", url)
|
||||
|
||||
cmd.Stdout = w
|
||||
|
||||
group.Go(func() error {
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
})
|
||||
|
||||
playlists := []Entry{}
|
||||
for line := range lines {
|
||||
entry := Entry{}
|
||||
err = json.Unmarshal([]byte(line), &entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playlists = append(playlists, entry)
|
||||
}
|
||||
|
||||
return playlists, nil
|
||||
}
|
||||
|
||||
func lineReader(group *errgroup.Group) (io.WriteCloser, <-chan string, error) {
|
||||
lines := make(chan string)
|
||||
var r io.Reader
|
||||
r, w := io.Pipe()
|
||||
|
||||
r = io.TeeReader(r, os.Stdout)
|
||||
|
||||
group.Go(func() error {
|
||||
defer close(lines)
|
||||
|
||||
reader := bufio.NewReader(r)
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return w.Close()
|
||||
}
|
||||
return errors.Join(err, w.Close())
|
||||
}
|
||||
line = strings.Trim(line, " \r\t")
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lines <- line
|
||||
}
|
||||
})
|
||||
|
||||
return w, lines, nil
|
||||
}
|
||||
|
||||
var _ ctxprogress.Progress = (*Entry)(nil)
|
||||
|
||||
var _ ctxprogress.Progress = (*DownloadProgress)(nil)
|
||||
|
||||
func parseProgress(line string) (ctxprogress.Progress, error) {
|
||||
line = strings.Trim(line, " \r\t")
|
||||
|
||||
p := &DownloadProgress{}
|
||||
err := json.Unmarshal([]byte(line), p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
27
server/pkg/ytdlp/playlist_test.go
Normal file
27
server/pkg/ytdlp/playlist_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package ytdlp_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/ytdlp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlaylist(t *testing.T) {
|
||||
require := require.New(t)
|
||||
ctx := context.Background()
|
||||
if deadline, ok := t.Deadline(); ok {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadlineCause(ctx, deadline, errors.New("test deadline done"))
|
||||
defer cancel()
|
||||
}
|
||||
client, err := ytdlp.New()
|
||||
require.NoError(err)
|
||||
entries, err := client.Playlist(ctx, "https://www.youtube.com/playlist?list=PLUay9m6GhoyCXdloEa-VYtnVeshaKl4AW")
|
||||
require.NoError(err)
|
||||
|
||||
require.NotEmpty(entries)
|
||||
require.Len(entries, int(entries[0].PlaylistCount))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue