diff --git a/client.go b/client.go index 86e6254e..37557c5a 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/client_integration_test.go b/client_integration_test.go index 5797e349..c6b7473c 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 { diff --git a/packet.go b/packet.go index ea74cd95..8393e318 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