Repository: pkg/sftp
Pull request #406 (2021-02-10 18:17:22)

request-server: add StatVFS support
Besides the included test case, I also did a basic test using the request server example and the `sftp` CLI, here is the output:

```
sftp -P 2022 testuser@127.0.0.1
testuser@127.0.0.1's password: 
Connected to 127.0.0.1.
sftp> df
        Size         Used        Avail       (root)    %Capacity
   490945448    431669628     54261036     59275820          87%
```

thank you


Patch:
diff --git a/client.go b/client.go
index cdd5669..8f6e048 100644
--- a/client.go
+++ b/client.go
@@ -638,7 +638,7 @@ func (c *Client) StatVFS(path string) (*StatVFS, error) {
 
 	// the resquest failed
 	case sshFxpStatus:
-		return nil, errors.New(fxp(sshFxpStatus).String())
+		return nil, normaliseError(unmarshalStatus(id, data))
 
 	default:
 		return nil, unimplementedPacketErr(typ)
diff --git a/request-example.go b/request-example.go
index 0de41df..ab6c675 100644
--- a/request-example.go
+++ b/request-example.go
@@ -266,6 +266,14 @@ func (fs *root) PosixRename(r *Request) error {
 	return fs.rename(r.Filepath, r.Target)
 }
 
+func (fs *root) StatVFS(r *Request) (*StatVFS, error) {
+	if fs.mockErr != nil {
+		return nil, fs.mockErr
+	}
+
+	return getStatVFSForPath(r.Filepath)
+}
+
 func (fs *root) mkdir(pathname string) error {
 	dir := &memFile{
 		modtime: time.Now(),
diff --git a/request-interfaces.go b/request-interfaces.go
index e1d55f8..4529d99 100644
--- a/request-interfaces.go
+++ b/request-interfaces.go
@@ -55,7 +55,7 @@ type FileCmder interface {
 	Filecmd(*Request) error
 }
 
-// PosixRenameFileCmder is a FileCmder that implements the Lstat method.
+// PosixRenameFileCmder is a FileCmder that implements the PosixRename method.
 // If this interface is implemented PosixRename requests will call it
 // otherwise they will be handled in the same way as Rename
 type PosixRenameFileCmder interface {
@@ -63,6 +63,14 @@ type PosixRenameFileCmder interface {
 	PosixRename(*Request) error
 }
 
+// StatVFSFileCmder is a FileCmder that implements the StatVFS method.
+// You need to implement this interface if you want to handle statvfs requests.
+// Please also be sure that the statvfs@openssh.com extension is enabled
+type StatVFSFileCmder interface {
+	FileCmder
+	StatVFS(*Request) (*StatVFS, error)
+}
+
 // FileLister should return an object that fulfils the ListerAt interface
 // Note in cases of an error, the error text will be sent to the client.
 // Called for Methods: List, Stat, Readlink
diff --git a/request-server.go b/request-server.go
index 0b9d5a4..72ee3b8 100644
--- a/request-server.go
+++ b/request-server.go
@@ -237,6 +237,9 @@ func (rs *RequestServer) packetWorker(
 			request := NewRequest("PosixRename", pkt.Oldpath)
 			request.Target = pkt.Newpath
 			rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
+		case *sshFxpExtendedPacketStatVFS:
+			request := NewRequest("StatVFS", pkt.Path)
+			rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
 		case hasHandle:
 			handle := pkt.getHandle()
 			request, ok := rs.getRequest(handle)
diff --git a/request.go b/request.go
index 98ff04c..086d18d 100644
--- a/request.go
+++ b/request.go
@@ -212,7 +212,7 @@ func (r *Request) call(handlers Handlers, pkt requestPacket, alloc *allocator, o
 		return fileput(handlers.FilePut, r, pkt, alloc, orderID)
 	case "Open":
 		return fileputget(handlers.FilePut, r, pkt, alloc, orderID)
-	case "Setstat", "Rename", "Rmdir", "Mkdir", "Link", "Symlink", "Remove", "PosixRename":
+	case "Setstat", "Rename", "Rmdir", "Mkdir", "Link", "Symlink", "Remove", "PosixRename", "StatVFS":
 		return filecmd(handlers.FileCmd, r, pkt)
 	case "List":
 		return filelist(handlers.FileList, r, pkt)
@@ -371,6 +371,19 @@ func filecmd(h FileCmder, r *Request, pkt requestPacket) responsePacket {
 		return statusFromError(pkt, err)
 	}
 
+	if r.Method == "StatVFS" {
+		if statVFSCmdr, ok := h.(StatVFSFileCmder); ok {
+			stat, err := statVFSCmdr.StatVFS(r)
+			if err != nil {
+				return statusFromError(pkt, err)
+			}
+			stat.ID = pkt.id()
+			return stat
+		}
+
+		return statusFromError(pkt, ErrSSHFxOpUnsupported)
+	}
+
 	err := h.Filecmd(r)
 	return statusFromError(pkt, err)
 }
diff --git a/server_statvfs_impl.go b/server_statvfs_impl.go
index 4cf91dc..9dc793c 100644
--- a/server_statvfs_impl.go
+++ b/server_statvfs_impl.go
@@ -10,12 +10,7 @@ import (
 )
 
 func (p sshFxpExtendedPacketStatVFS) respond(svr *Server) responsePacket {
-	stat := &syscall.Statfs_t{}
-	if err := syscall.Statfs(p.Path, stat); err != nil {
-		return statusFromError(p, err)
-	}
-
-	retPkt, err := statvfsFromStatfst(stat)
+	retPkt, err := getStatVFSForPath(p.Path)
 	if err != nil {
 		return statusFromError(p, err)
 	}
@@ -23,3 +18,12 @@ func (p sshFxpExtendedPacketStatVFS) respond(svr *Server) responsePacket {
 
 	return retPkt
 }
+
+func getStatVFSForPath(name string) (*StatVFS, error) {
+	stat := &syscall.Statfs_t{}
+	if err := syscall.Statfs(name, stat); err != nil {
+		return nil, err
+	}
+
+	return statvfsFromStatfst(stat)
+}
diff --git a/server_statvfs_plan9.go b/server_statvfs_plan9.go
index 13c53f3..5a29323 100644
--- a/server_statvfs_plan9.go
+++ b/server_statvfs_plan9.go
@@ -7,3 +7,7 @@ import (
 func (p sshFxpExtendedPacketStatVFS) respond(svr *Server) responsePacket {
 	return statusFromError(p, syscall.EPLAN9)
 }
+
+func getStatVFSForPath(name string) (*StatVFS, error) {
+	return nil, syscall.EPLAN9
+}
diff --git a/server_statvfs_stubs.go b/server_statvfs_stubs.go
index 7c98e32..c1bb104 100644
--- a/server_statvfs_stubs.go
+++ b/server_statvfs_stubs.go
@@ -9,3 +9,7 @@ import (
 func (p sshFxpExtendedPacketStatVFS) respond(svr *Server) responsePacket {
 	return statusFromError(p, syscall.ENOTSUP)
 }
+
+func getStatVFSForPath(name string) (*StatVFS, error) {
+	return nil, syscall.ENOTSUP
+}
diff --git a/sftp.go b/sftp.go
index 28b1a08..912dff1 100644
--- a/sftp.go
+++ b/sftp.go
@@ -90,6 +90,7 @@ var (
 	supportedSFTPExtensions = []sshExtensionPair{
 		{"hardlink@openssh.com", "1"},
 		{"posix-rename@openssh.com", "1"},
+		{"statvfs@openssh.com", "2"},
 	}
 	sftpExtensions = supportedSFTPExtensions
 )


Test patch:
diff --git a/request-server_test.go b/request-server_test.go
index 7243dab..af60678 100644
--- a/request-server_test.go
+++ b/request-server_test.go
@@ -7,6 +7,7 @@ import (
 	"io/ioutil"
 	"net"
 	"os"
+	"runtime"
 	"testing"
 	"time"
 
@@ -725,6 +726,44 @@ func TestRequestReaddir(t *testing.T) {
 	checkRequestServerAllocator(t, p)
 }
 
+func TestRequestStatVFS(t *testing.T) {
+	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
+		t.Skip("StatVFS is implemented on linux and darwin")
+	}
+
+	p := clientRequestServerPair(t)
+	defer p.Close()
+
+	_, ok := p.cli.HasExtension("statvfs@openssh.com")
+	require.True(t, ok, "request server doesn't list statvfs extension")
+	vfs, err := p.cli.StatVFS("/")
+	require.NoError(t, err)
+	expected, err := getStatVFSForPath("/")
+	require.NoError(t, err)
+	require.NotEqual(t, 0, expected.ID)
+	// check some stats
+	require.Equal(t, expected.Bavail, vfs.Bavail)
+	require.Equal(t, expected.Bfree, vfs.Bfree)
+	require.Equal(t, expected.Blocks, vfs.Blocks)
+
+	checkRequestServerAllocator(t, p)
+}
+
+func TestRequestStatVFSError(t *testing.T) {
+	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
+		t.Skip("StatVFS is implemented on linux and darwin")
+	}
+
+	p := clientRequestServerPair(t)
+	defer p.Close()
+
+	_, err := p.cli.StatVFS("a missing path")
+	require.Error(t, err)
+	require.True(t, os.IsNotExist(err))
+
+	checkRequestServerAllocator(t, p)
+}
+
 func TestCleanDisconnect(t *testing.T) {
 	p := clientRequestServerPair(t)
 	defer p.Close()