From 54cd7d67508fdb2639a1fb7219665eccd0588d7d Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Sun, 24 May 2026 17:08:49 +0100 Subject: [PATCH] serve sftp: implement statvfs@openssh.com to report disk usage The statvfs@openssh.com extension was advertised but returned an unsupported status, so clients couldn't query the amount of free and used space. Implement it using the VFS Statfs method, which reports the backend's usage where the backend supports About. --- cmd/serve/sftp/handler.go | 20 ++++++++++++++++++++ cmd/serve/sftp/handler_test.go | 15 +++++++++++++++ cmd/serve/sftp/sftp_test.go | 9 +++++---- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/cmd/serve/sftp/handler.go b/cmd/serve/sftp/handler.go index 559580064..04adaf432 100644 --- a/cmd/serve/sftp/handler.go +++ b/cmd/serve/sftp/handler.go @@ -116,6 +116,26 @@ func (v vfsHandler) Filecmd(r *sftp.Request) error { return nil } +// StatVFS implements the statvfs@openssh.com extension, returning filesystem +// usage information from the VFS. It satisfies sftp.StatVFSFileCmder. +func (v vfsHandler) StatVFS(r *sftp.Request) (*sftp.StatVFS, error) { + const blockSize = 4096 + total, _, free := v.Statfs() + blocks := uint64(total) / blockSize + bfree := uint64(free) / blockSize + return &sftp.StatVFS{ + Bsize: blockSize, + Frsize: blockSize, + Blocks: blocks, + Bfree: bfree, + Bavail: bfree, + Files: 1e9, // total file inodes - made up as the VFS has no concept of these + Ffree: 1e9, // free file inodes + Favail: 1e9, // free file inodes for non-root + Namemax: 255, // maximum filename length + }, nil +} + type listerat []os.FileInfo // Modeled after strings.Reader's ReadAt() implementation diff --git a/cmd/serve/sftp/handler_test.go b/cmd/serve/sftp/handler_test.go index b41d09cc9..f85af9543 100644 --- a/cmd/serve/sftp/handler_test.go +++ b/cmd/serve/sftp/handler_test.go @@ -168,6 +168,21 @@ func TestSetstatTruncate(t *testing.T) { assert.Equal(t, strings.Repeat("A", 10), string(got)) } +// Test that the statvfs@openssh.com extension returns filesystem usage from +// the VFS rather than an unsupported status. +func TestStatVFS(t *testing.T) { + vfsOpt := vfscommon.Opt + vfsOpt.CacheMode = vfscommon.CacheModeWrites + client := startTestServer(t, &vfsOpt) + + st, err := client.StatVFS("/") + require.NoError(t, err) + + assert.Greater(t, st.TotalSpace(), uint64(0), "expected non-zero total space") + assert.LessOrEqual(t, st.FreeSpace(), st.TotalSpace(), "free space should not exceed total") + assert.Equal(t, uint64(255), st.Namemax) +} + // writeFile writes contents to fileName via the client truncating any existing // data, the way a normal upload does. func writeFile(client *sftp.Client, fileName, contents string) error { diff --git a/cmd/serve/sftp/sftp_test.go b/cmd/serve/sftp/sftp_test.go index e18d6b86d..ecbe500cb 100644 --- a/cmd/serve/sftp/sftp_test.go +++ b/cmd/serve/sftp/sftp_test.go @@ -33,10 +33,11 @@ const ( // check interfaces var ( - _ sftp.FileReader = vfsHandler{} - _ sftp.FileWriter = vfsHandler{} - _ sftp.FileCmder = vfsHandler{} - _ sftp.FileLister = vfsHandler{} + _ sftp.FileReader = vfsHandler{} + _ sftp.FileWriter = vfsHandler{} + _ sftp.FileCmder = vfsHandler{} + _ sftp.FileLister = vfsHandler{} + _ sftp.StatVFSFileCmder = vfsHandler{} ) // TestSftp runs the sftp server then runs the unit tests for the