From c69cf46f0655ce51dd3877544177473658d041d9 Mon Sep 17 00:00:00 2001 From: Saleh Dindar Date: Wed, 4 Oct 2023 10:28:41 -0700 Subject: [PATCH] serve nfs: new `serve nfs` command Summary: Adding a new command to serve any remote over NFS. This is only useful for new macOS versions where FUSE mounts are not available. * Added willscot/go-nfs dependency and updated go.mod and go.sum Test Plan: ``` go run rclone.go serve nfs --http-url https://beta.rclone.org :http: ``` Test that it is serving correctly by mounting the NFS directory. ``` mkdir nfs-test mount -oport=58654,mountport=58654 localhost: nfs-test ``` Then we can list the mounted directory to see it is working. ``` ls nfs-test ``` --- cmd/serve/nfs/filesystem.go | 159 +++++++++++++++++++++++++++++++ cmd/serve/nfs/handler.go | 70 ++++++++++++++ cmd/serve/nfs/nfs.go | 97 +++++++++++++++++++ cmd/serve/nfs/nfs_unsupported.go | 13 +++ cmd/serve/nfs/server.go | 61 ++++++++++++ cmd/serve/serve.go | 4 + go.mod | 4 + go.sum | 10 ++ 8 files changed, 418 insertions(+) create mode 100644 cmd/serve/nfs/filesystem.go create mode 100644 cmd/serve/nfs/handler.go create mode 100644 cmd/serve/nfs/nfs.go create mode 100644 cmd/serve/nfs/nfs_unsupported.go create mode 100644 cmd/serve/nfs/server.go diff --git a/cmd/serve/nfs/filesystem.go b/cmd/serve/nfs/filesystem.go new file mode 100644 index 000000000..25916e0dd --- /dev/null +++ b/cmd/serve/nfs/filesystem.go @@ -0,0 +1,159 @@ +//go:build unix +// +build unix + +package nfs + +import ( + "os" + "path" + "strings" + "time" + + billy "github.com/go-git/go-billy/v5" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfscommon" +) + +// FS is our wrapper around the VFS to properly support billy.Filesystem interface +type FS struct { + vfs *vfs.VFS +} + +// ReadDir implements read dir +func (f *FS) ReadDir(path string) (dir []os.FileInfo, err error) { + return f.vfs.ReadDir(path) +} + +// Create implements creating new files +func (f *FS) Create(filename string) (billy.File, error) { + return f.vfs.Create(filename) +} + +// Open opens a file +func (f *FS) Open(filename string) (billy.File, error) { + return f.vfs.Open(filename) +} + +// OpenFile opens a file +func (f *FS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { + return f.vfs.OpenFile(filename, flag, perm) +} + +// Stat gets the file stat +func (f *FS) Stat(filename string) (os.FileInfo, error) { + return f.vfs.Stat(filename) +} + +// Rename renames a file +func (f *FS) Rename(oldpath, newpath string) error { + return f.vfs.Rename(oldpath, newpath) +} + +// Remove deletes a file +func (f *FS) Remove(filename string) error { + return f.vfs.Remove(filename) +} + +// Join joins path elements +func (f *FS) Join(elem ...string) string { + return path.Join(elem...) +} + +// TempFile is not implemented +func (f *FS) TempFile(dir, prefix string) (billy.File, error) { + return nil, os.ErrInvalid +} + +// MkdirAll creates a directory and all the ones above it +// it does not redirect to VFS.MkDirAll because that one doesn't +// honor the permissions +func (f *FS) MkdirAll(filename string, perm os.FileMode) error { + parts := strings.Split(filename, "/") + for i := range parts { + current := strings.Join(parts[:i+1], "/") + _, err := f.Stat(current) + if err == vfs.ENOENT { + err = f.vfs.Mkdir(current, perm) + if err != nil { + return err + } + } + } + return nil +} + +// Lstat gets the stats for symlink +func (f *FS) Lstat(filename string) (os.FileInfo, error) { + return f.vfs.Stat(filename) +} + +// Symlink is not supported over NFS +func (f *FS) Symlink(target, link string) error { + return os.ErrInvalid +} + +// Readlink is not supported +func (f *FS) Readlink(link string) (string, error) { + return "", os.ErrInvalid +} + +// Chmod changes the file modes +func (f *FS) Chmod(name string, mode os.FileMode) error { + file, err := f.vfs.Open(name) + if err != nil { + return err + } + defer func() { + if err := file.Close(); err != nil { + fs.Logf(f, "Error while closing file: %e", err) + } + }() + return file.Chmod(mode) +} + +// Lchown changes the owner of symlink +func (f *FS) Lchown(name string, uid, gid int) error { + return f.Chown(name, uid, gid) +} + +// Chown changes owner of the file +func (f *FS) Chown(name string, uid, gid int) error { + file, err := f.vfs.Open(name) + if err != nil { + return err + } + defer func() { + if err := file.Close(); err != nil { + fs.Logf(f, "Error while closing file: %e", err) + } + }() + return file.Chown(uid, gid) +} + +// Chtimes changes the acces time and modified time +func (f *FS) Chtimes(name string, atime time.Time, mtime time.Time) error { + return f.vfs.Chtimes(name, atime, mtime) +} + +// Chroot is not supported in VFS +func (f *FS) Chroot(path string) (billy.Filesystem, error) { + return nil, os.ErrInvalid +} + +// Root returns the root of a VFS +func (f *FS) Root() string { + return f.vfs.Fs().Root() +} + +// Capabilities exports the filesystem capabilities +func (f *FS) Capabilities() billy.Capability { + if f.vfs.Opt.CacheMode == vfscommon.CacheModeOff { + return billy.ReadCapability | billy.SeekCapability + } + return billy.WriteCapability | billy.ReadCapability | + billy.ReadAndWriteCapability | billy.SeekCapability | billy.TruncateCapability +} + +// Interface check +var _ billy.Filesystem = (*FS)(nil) diff --git a/cmd/serve/nfs/handler.go b/cmd/serve/nfs/handler.go new file mode 100644 index 000000000..4a34d6138 --- /dev/null +++ b/cmd/serve/nfs/handler.go @@ -0,0 +1,70 @@ +//go:build unix +// +build unix + +package nfs + +import ( + "context" + "net" + + "github.com/go-git/go-billy/v5" + "github.com/rclone/rclone/vfs" + "github.com/willscott/go-nfs" + nfshelper "github.com/willscott/go-nfs/helpers" +) + +// NewBackendAuthHandler creates a handler for the provided filesystem +func NewBackendAuthHandler(vfs *vfs.VFS) nfs.Handler { + return &BackendAuthHandler{vfs} +} + +// BackendAuthHandler returns a NFS backing that exposes a given file system in response to all mount requests. +type BackendAuthHandler struct { + vfs *vfs.VFS +} + +// Mount backs Mount RPC Requests, allowing for access control policies. +func (h *BackendAuthHandler) Mount(ctx context.Context, conn net.Conn, req nfs.MountRequest) (status nfs.MountStatus, hndl billy.Filesystem, auths []nfs.AuthFlavor) { + status = nfs.MountStatusOk + hndl = &FS{vfs: h.vfs} + auths = []nfs.AuthFlavor{nfs.AuthFlavorNull} + return +} + +// Change provides an interface for updating file attributes. +func (h *BackendAuthHandler) Change(fs billy.Filesystem) billy.Change { + if c, ok := fs.(billy.Change); ok { + return c + } + return nil +} + +// FSStat provides information about a filesystem. +func (h *BackendAuthHandler) FSStat(ctx context.Context, f billy.Filesystem, s *nfs.FSStat) error { + total, _, free := h.vfs.Statfs() + s.TotalSize = uint64(total) + s.FreeSize = uint64(free) + s.AvailableSize = uint64(free) + return nil +} + +// ToHandle handled by CachingHandler +func (h *BackendAuthHandler) ToHandle(f billy.Filesystem, s []string) []byte { + return []byte{} +} + +// FromHandle handled by CachingHandler +func (h *BackendAuthHandler) FromHandle([]byte) (billy.Filesystem, []string, error) { + return nil, []string{}, nil +} + +// HandleLimit handled by cachingHandler +func (h *BackendAuthHandler) HandleLimit() int { + return -1 +} + +func newHandler(vfs *vfs.VFS) nfs.Handler { + handler := NewBackendAuthHandler(vfs) + cacheHelper := nfshelper.NewCachingHandler(handler, 1024) + return cacheHelper +} diff --git a/cmd/serve/nfs/nfs.go b/cmd/serve/nfs/nfs.go new file mode 100644 index 000000000..3e4722cb6 --- /dev/null +++ b/cmd/serve/nfs/nfs.go @@ -0,0 +1,97 @@ +//go:build unix +// +build unix + +// Package nfs implements a server to serve a VFS remote over NFSv3 protocol +// +// There is no authentication available on this server +// and it is served on loopback interface by default. +// +// This is primarily used for mounting a VFS remote +// in macOS, where FUSE-mounting mechanisms are usually not available. +package nfs + +import ( + "context" + + "github.com/rclone/rclone/cmd" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/config/flags" + "github.com/rclone/rclone/fs/rc" + "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfsflags" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// Options contains options for the NFS Server +type Options struct { + ListenAddr string // Port to listen on +} + +var opt Options + +// AddFlags adds flags for the sftp +func AddFlags(flagSet *pflag.FlagSet, Opt *Options) { + rc.AddOption("nfs", &Opt) + flags.StringVarP(flagSet, &Opt.ListenAddr, "addr", "", Opt.ListenAddr, "IPaddress:Port or :Port to bind server to", "") +} + +func init() { + vfsflags.AddFlags(Command.Flags()) + AddFlags(Command.Flags(), &opt) +} + +// Run the command +func Run(command *cobra.Command, args []string) { + var f fs.Fs + cmd.CheckArgs(1, 1, command, args) + f = cmd.NewFsSrc(args) + cmd.Run(false, true, command, func() error { + s, err := NewServer(context.Background(), vfs.New(f, &vfsflags.Opt), &opt) + if err != nil { + return err + } + return s.Serve() + }) +} + +// Command is the definition of the command +var Command = &cobra.Command{ + Use: "nfs remote:path", + Short: `Serve the remote as an NFS mount`, + Long: `Create an NFS server that serves the given remote over the network. + +The primary purpose for this command is to enable [mount command](/commands/rclone_mount/) on recent macOS versions where +installing FUSE is very cumbersome. + +Since this is running on NFSv3, no authentication method is available. Any client +will be able to access the data. To limit access, you can use serve NFS on loopback address +and rely on secure tunnels (such as SSH). For this reason, by default, a random TCP port is chosen and loopback interface is used for the listening address; +meaning that it is only available to the local machine. If you want other machines to access the +NFS mount over local network, you need to specify the listening address and port using ` + "`--addr`" + ` flag. + +Modifying files through NFS protocol requires VFS caching. Usually you will need to specify ` + "`--vfs-cache-mode`" + ` +in order to be able to write to the mountpoint (full is recommended). If you don't specify VFS cache mode, +the mount will be read-only. + +To serve NFS over the network use following command: + + rclone serve nfs remote: --addr 0.0.0.0:$PORT --vfs-cache-mode=full + +We specify a specific port that we can use in the mount command: + +To mount the server under Linux/macOS, use the following command: + + mount -oport=$PORT,mountport=$PORT $HOSTNAME: path/to/mountpoint + +Where ` + "`$PORT`" + ` is the same port number we used in the serve nfs command. + +This feature is only available on Unix platforms. + +` + vfs.Help, + Annotations: map[string]string{ + "versionIntroduced": "v1.65", + "groups": "Filter", + }, + Run: Run, +} diff --git a/cmd/serve/nfs/nfs_unsupported.go b/cmd/serve/nfs/nfs_unsupported.go new file mode 100644 index 000000000..99b2f6df6 --- /dev/null +++ b/cmd/serve/nfs/nfs_unsupported.go @@ -0,0 +1,13 @@ +// For unsupported architectures +//go:build !unix +// +build !unix + +// Package nfs is not supported on non-Unix platforms +package nfs + +import ( + "github.com/spf13/cobra" +) + +// For unsupported platforms we just put nil +var Command *cobra.Command = nil diff --git a/cmd/serve/nfs/server.go b/cmd/serve/nfs/server.go new file mode 100644 index 000000000..c66e687a9 --- /dev/null +++ b/cmd/serve/nfs/server.go @@ -0,0 +1,61 @@ +//go:build unix +// +build unix + +package nfs + +import ( + "context" + "net" + + nfs "github.com/willscott/go-nfs" + + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfscommon" +) + +// Server contains everything to run the Server +type Server struct { + opt Options + handler nfs.Handler + ctx context.Context // for global config + listener net.Listener +} + +// NewServer creates a new server +func NewServer(ctx context.Context, vfs *vfs.VFS, opt *Options) (s *Server, err error) { + if vfs.Opt.CacheMode == vfscommon.CacheModeOff { + fs.LogPrintf(fs.LogLevelWarning, ctx, "NFS writes don't work without a cache, the filesystem will be served read-only") + } + // Our NFS server doesn't have any authentication, we run it on localhost and random port by default + if opt.ListenAddr == "" { + opt.ListenAddr = "localhost:" + } + + s = &Server{ + ctx: ctx, + opt: *opt, + } + s.handler = newHandler(vfs) + s.listener, err = net.Listen("tcp", s.opt.ListenAddr) + if err != nil { + fs.Errorf(nil, "NFS server failed to listen: %v\n", err) + } + return +} + +// Addr returns the listening address of the server +func (s *Server) Addr() net.Addr { + return s.listener.Addr() +} + +// Shutdown stops the server +func (s *Server) Shutdown() error { + return s.listener.Close() +} + +// Serve starts the server +func (s *Server) Serve() (err error) { + fs.Logf(nil, "NFS Server running at %s\n", s.listener.Addr()) + return nfs.Serve(s.listener, s.handler) +} diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index 8f523f52c..aa9da36f3 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -9,6 +9,7 @@ import ( "github.com/rclone/rclone/cmd/serve/docker" "github.com/rclone/rclone/cmd/serve/ftp" "github.com/rclone/rclone/cmd/serve/http" + "github.com/rclone/rclone/cmd/serve/nfs" "github.com/rclone/rclone/cmd/serve/restic" "github.com/rclone/rclone/cmd/serve/sftp" "github.com/rclone/rclone/cmd/serve/webdav" @@ -35,6 +36,9 @@ func init() { if docker.Command != nil { Command.AddCommand(docker.Command) } + if nfs.Command != nil { + Command.AddCommand(nfs.Command) + } cmd.Root.AddCommand(Command) } diff --git a/go.mod b/go.mod index ec6403e86..85a27dbe5 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca + github.com/willscott/go-nfs v0.0.0-20230823072803-2b8e63b4d81f github.com/winfsp/cgofuse v1.5.1-0.20221118130120-84c0898ad2e0 github.com/xanzy/ssh-agent v0.3.3 github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a @@ -116,6 +117,7 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect @@ -139,6 +141,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect + github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect github.com/relvacode/iso8601 v1.3.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -147,6 +150,7 @@ require ( github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect + github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/errs v1.3.0 // indirect diff --git a/go.sum b/go.sum index 2cc19dab3..fad29a1b9 100644 --- a/go.sum +++ b/go.sum @@ -300,6 +300,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/henrybear327/Proton-API-Bridge v0.0.0-20230908065933-5bfa15b567db h1:EpttN0oeR6BvUPis9+4iSyxIacMHXlaHrD/+IvyKPlc= github.com/henrybear327/Proton-API-Bridge v0.0.0-20230908065933-5bfa15b567db/go.mod h1:gunH16hf6U74W2b9CGDaWRadiLICsoJ6KRkSt53zLts= github.com/henrybear327/go-proton-api v0.0.0-20230907193451-e563407504ce h1:n1URi7VYiwX/3akX51keQXi6Huy4lJdVc4biJHYk3iw= @@ -398,6 +400,7 @@ github.com/ncw/go-acd v0.0.0-20201019170801-fe55f33415b1/go.mod h1:MLIrzg7gp/kzV github.com/ncw/swift/v2 v2.0.2 h1:jx282pcAKFhmoZBSdMcCRFn9VWkoBIRsCpe+yZq7vEk= github.com/ncw/swift/v2 v2.0.2/go.mod h1:z0A9RVdYPjNjXVo2pDOPxZ4eu3oarO1P91fTItcb+Kg= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= @@ -436,6 +439,8 @@ github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8 h1:Y258uzX github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8/go.mod h1:bSJjRokAHHOhA+XFxplld8w2R/dXLH7Z3BZ532vhFwU= github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI= github.com/quic-go/quic-go v0.38.0 h1:T45lASr5q/TrVwt+jrVccmqHhPL2XuSyoCLVCpfOSLc= +github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg= +github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o= github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4= @@ -507,6 +512,10 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 h1:zMsHhfK9+Wdl1F7sIKLyx3wrOFofpb3rWFbA4HgcK5k= github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw7ElaGSLOZUSwBm/GgVwMd30jWxBDdAyMOeTuc= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willscott/go-nfs v0.0.0-20230823072803-2b8e63b4d81f h1:AWEJVmIm/bnZHTNJd5GPy5yR5riCaa59FsHbvbcW+fI= +github.com/willscott/go-nfs v0.0.0-20230823072803-2b8e63b4d81f/go.mod h1:1PiZswSlNu7GfHMWekF6rF1I94c8E0p+Cwbz7px1D14= +github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 h1:Wd8wdpRzPXskyHvZLyw7Wc1fp5oCE2mhBCj7bAiibUs= +github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33/go.mod h1:cOUKSNty+RabZqKhm5yTJT5Vq/Fe83ZRWAJ5Kj8nRes= github.com/winfsp/cgofuse v1.5.1-0.20221118130120-84c0898ad2e0 h1:j3un8DqYvvAOqKI5OPz+/RRVhDFipbPKI4t2Uk5RBJw= github.com/winfsp/cgofuse v1.5.1-0.20221118130120-84c0898ad2e0/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -531,6 +540,7 @@ github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +github.com/zema1/go-nfs-client v0.0.0-20200604081958-0cf942f0e0fe/go.mod h1:im3CVJ32XM3+E+2RhY0sa5IVJVQehUrX0oE1wX4xOwU= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=