Repository: pkg/sftp
Pull request #386 (2020-10-11 12:08:37)

Add Client.Sync method
This uses the `fsync@openssh.com` extension: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL, §3.6.


Patch:
diff --git a/client.go b/client.go
index 86e6254..37557c5 100644
--- a/client.go
+++ b/client.go
@@ -1294,6 +1294,26 @@ func (f *File) Chmod(mode os.FileMode) error {
 	return f.c.Chmod(f.path, mode)
 }
 
+// Sync requests a flush of the contents of a File to stable storage.
+//
+// Sync requires the server to support the fsync@openssh.com extension.
+func (f *File) Sync() error {
+	id := f.c.nextID()
+	typ, data, err := f.c.sendPacket(sshFxpFsyncPacket{
+		ID:     id,
+		Handle: f.handle,
+	})
+
+	switch {
+	case err != nil:
+		return err
+	case typ == sshFxpStatus:
+		return normaliseError(unmarshalStatus(id, data))
+	default:
+		return &unexpectedPacketErr{want: sshFxpStatus, got: typ}
+	}
+}
+
 // Truncate sets the size of the current file. Although it may be safely assumed
 // that if the size is less than its current size it will be truncated to fit,
 // the SFTP protocol does not specify what behavior the server should do when setting
diff --git a/packet.go b/packet.go
index ea74cd9..8393e31 100644
--- a/packet.go
+++ b/packet.go
@@ -917,6 +917,26 @@ func (p *StatVFS) MarshalBinary() ([]byte, error) {
 	return buf.Bytes(), err
 }
 
+type sshFxpFsyncPacket struct {
+	ID     uint32
+	Handle string
+}
+
+func (p sshFxpFsyncPacket) id() uint32 { return p.ID }
+
+func (p sshFxpFsyncPacket) MarshalBinary() ([]byte, error) {
+	l := 1 + 4 + // type (byte) + ID (uint32)
+		4 + len("fsync@openssh.com") +
+		4 + len(p.Handle)
+
+	b := make([]byte, 0, l)
+	b = append(b, sshFxpExtended)
+	b = marshalUint32(b, p.ID)
+	b = marshalString(b, "fsync@openssh.com")
+	b = marshalString(b, p.Handle)
+	return b, nil
+}
+
 type sshFxpExtendedPacket struct {
 	ID              uint32
 	ExtendedRequest string


Test patch:
diff --git a/client_integration_test.go b/client_integration_test.go
index 5797e34..c6b7473 100644
--- a/client_integration_test.go
+++ b/client_integration_test.go
@@ -26,6 +26,8 @@ import (
 	"time"
 
 	"github.com/kr/fs"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -1434,6 +1436,49 @@ func clientReadDeadlock(t *testing.T, N int, badfunc func(*File)) {
 	badfunc(r)
 }
 
+func TestClientSyncGo(t *testing.T) {
+	if !*testServerImpl {
+		t.Skipf("skipping without -testserver")
+	}
+	err := testClientSync(t)
+
+	// Since Server does not support the fsync extension, we can only
+	// check that we get the right error.
+	require.NotNil(t, err)
+
+	switch err := err.(type) {
+	case *StatusError:
+		assert.Equal(t, ErrSSHFxOpUnsupported, err.FxCode())
+	default:
+		t.Error(err)
+	}
+}
+
+func TestClientSyncSFTP(t *testing.T) {
+	if *testServerImpl {
+		t.Skipf("skipping with -testserver")
+	}
+	err := testClientSync(t)
+	assert.Nil(t, err)
+}
+
+func testClientSync(t *testing.T) error {
+	sftp, cmd := testClient(t, READWRITE, NODELAY)
+	defer cmd.Wait()
+	defer sftp.Close()
+
+	d, err := ioutil.TempDir("", "sftptest.sync")
+	require.Nil(t, err)
+	defer os.RemoveAll(d)
+
+	f := path.Join(d, "syncTest")
+	w, err := sftp.Create(f)
+	require.Nil(t, err)
+	defer w.Close()
+
+	return w.Sync()
+}
+
 // taken from github.com/kr/fs/walk_test.go
 
 type Node struct {