Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,19 @@ func (cl *Client) hasExtension(ext *sshfx.ExtensionPair) bool {
return cl.exts[ext.Name] == ext.Data
}

// StatVFS retrieves VFS statistics from a remote host.
//
// It implements the [email protected] SSH_FXP_EXTENDED feature from
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
func (cl *Client) StatVFS(path string) (*openssh.StatVFSExtendedReplyPacket, error) {
resp, err := getPacket[*openssh.StatVFSExtendedReplyPacket](context.Background(), nil, cl,
&openssh.StatVFSExtendedPacket{
Path: path,
},
)
return valOrPathError("statvfs", path, resp, err)
}

// Link creates newname as a hard link to oldname file.
//
// If the server did not announce support for the "[email protected]" extension,
Expand Down
26 changes: 26 additions & 0 deletions localfs/localfs_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,32 @@ func TestReadFrom(t *testing.T) {
}
}

func TestStatVFS(t *testing.T) {
if !*testServerImpl {
t.Skip("not testing against localfs server implementation")
}

if _, ok := any(handler).(sftp.StatVFSServerHandler); !ok {
t.Skip("handler does not implement statvfs")
}

dir := t.TempDir()

targetNotExist := filepath.Join(dir, "statvfs-does-not-exist")

_, err := cl.StatVFS(toRemotePath(targetNotExist))
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("unexpected error, got %v, should be fs.ErrNotFound", err)
}

resp, err := cl.StatVFS(toRemotePath(dir))
if err != nil {
t.Fatal(err)
}

t.Logf("%+v", resp)
}

var benchBuf []byte

func benchHelperWriteTo(b *testing.B, length int) {
Expand Down
37 changes: 31 additions & 6 deletions server.go
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve checked all the other cases of returns in Server.handle(), and they all test for err != nil immediately after.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WOW, Solid change!

Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,12 @@ func Hijack[REQ sshfx.Packet](srv *Server, fn func(context.Context, REQ) error)
// This is really only useful for supporting newer versions of the SFTP standard.
func HijackWithResponse[REQ, RESP sshfx.Packet](srv *Server, fn func(context.Context, REQ) (RESP, error)) error {
wrap := wrapHandler(func(ctx context.Context, req sshfx.Packet) (sshfx.Packet, error) {
return fn(ctx, req.(REQ))
resp, err := fn(ctx, req.(REQ))
if err != nil {
// We have to convert maybe typed-zero to untyped-nil.
return nil, err
}
return resp, nil
})

var pkt REQ
Expand Down Expand Up @@ -515,6 +520,7 @@ func (srv *Server) handle(req sshfx.Packet, hint []byte, maxDataLen uint32) (ssh

if len(srv.hijacks) > 0 {
if fn := srv.hijacks[req.Type()]; fn != nil {
// Hijack takes care of wrapping the getter into an untyped-nil on error.
return get(srv, req, fn)
}
}
Expand Down Expand Up @@ -595,7 +601,13 @@ func (srv *Server) handle(req sshfx.Packet, hint []byte, maxDataLen uint32) (ssh

case *openssh.StatVFSExtendedPacket:
if statvfser, ok := srv.Handler.(StatVFSServerHandler); ok {
return get(srv, req, statvfser.StatVFS)
resp, err := get(srv, req, statvfser.StatVFS)
if err != nil {
// We have to convert typed-nil to untyped-nil.
return nil, err
}

return resp, nil
}

case interface{ GetHandle() string }:
Expand All @@ -610,15 +622,27 @@ func (srv *Server) handle(req sshfx.Packet, hint []byte, maxDataLen uint32) (ssh

case *openssh.FStatVFSExtendedPacket:
if statvfser, ok := file.(StatVFSFileHandler); ok {
return statvfser.StatVFS()
resp, err := statvfser.StatVFS()
if err != nil {
// We have to convert typed-nil to untyped-nil.
return nil, err
}

return resp, nil
}

if statvfser, ok := srv.Handler.(StatVFSServerHandler); ok {
req := &openssh.StatVFSExtendedPacket{
Path: file.Name(),
}

return get(srv, req, statvfser.StatVFS)
resp, err := get(srv, req, statvfser.StatVFS)
if err != nil {
// We have to convert typed-nil to untyped-nil.
return nil, err
}

return resp, nil
}
}
}
Expand Down Expand Up @@ -701,7 +725,7 @@ func (srv *Server) handle(req sshfx.Packet, hint []byte, maxDataLen uint32) (ssh
}

hint = slices.Grow(hint[:0], int(req.Length))[:req.Length]

n, err := file.ReadAt(hint, int64(req.Offset))
if err != nil {
// We cannot return results AND a status like SSH_FX_EOF,
Expand Down Expand Up @@ -729,7 +753,8 @@ func (srv *Server) handle(req sshfx.Packet, hint []byte, maxDataLen uint32) (ssh
return nil, io.ErrShortWrite
}

return nil, nil
// explicitly return statusOK here, rather than both nil.
return statusOK, nil

case *sshfx.FStatPacket:
attrs, err := file.Stat()
Expand Down