Skip to content

Commit 06ab92e

Browse files
authored
Merge pull request pkg#373 from drakkan/fsetstat
request server: add support for SSH_FXP_FSETSTAT
2 parents 2c44234 + 07229f2 commit 06ab92e

File tree

5 files changed

+88
-8
lines changed

5 files changed

+88
-8
lines changed

.travis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ go_import_path: github.com/pkg/sftp
44
# current and previous stable releases, plus tip
55
# remember to exclude previous and tip for macs below
66
go:
7-
- 1.13.x
87
- 1.14.x
8+
- 1.15.x
99
- tip
1010

1111
os:
@@ -15,7 +15,7 @@ os:
1515
matrix:
1616
exclude:
1717
- os: osx
18-
go: 1.13.x
18+
go: 1.14.x
1919
- os: osx
2020
go: tip
2121

client.go

+26-6
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,25 @@ func (c *Client) Symlink(oldname, newname string) error {
432432
}
433433
}
434434

435+
func (c *Client) setfstat(handle string, flags uint32, attrs interface{}) error {
436+
id := c.nextID()
437+
typ, data, err := c.sendPacket(sshFxpFsetstatPacket{
438+
ID: id,
439+
Handle: handle,
440+
Flags: flags,
441+
Attrs: attrs,
442+
})
443+
if err != nil {
444+
return err
445+
}
446+
switch typ {
447+
case sshFxpStatus:
448+
return normaliseError(unmarshalStatus(id, data))
449+
default:
450+
return unimplementedPacketErr(typ)
451+
}
452+
}
453+
435454
// setstat is a convience wrapper to allow for changing of various parts of the file descriptor.
436455
func (c *Client) setstat(path string, flags uint32, attrs interface{}) error {
437456
id := c.nextID()
@@ -817,7 +836,7 @@ type File struct {
817836
path string
818837
handle string
819838

820-
mu sync.Mutex
839+
mu sync.Mutex
821840
offset uint64 // current offset within remote file
822841
}
823842

@@ -845,13 +864,13 @@ func (f *File) Read(b []byte) (int, error) {
845864
f.mu.Lock()
846865
defer f.mu.Unlock()
847866

848-
r, err := f.ReadAt(b, int64( f.offset ))
867+
r, err := f.ReadAt(b, int64(f.offset))
849868
f.offset += uint64(r)
850869
return r, err
851870
}
852871

853-
// ReadAt reads up to len(b) byte from the File at a given offset `off`. It returns
854-
// the number of bytes read and an error, if any. ReadAt follows io.ReaderAt semantics,
872+
// ReadAt reads up to len(b) byte from the File at a given offset `off`. It returns
873+
// the number of bytes read and an error, if any. ReadAt follows io.ReaderAt semantics,
855874
// so the file offset is not altered during the read.
856875
func (f *File) ReadAt(b []byte, off int64) (n int, err error) {
857876
// Split the read into multiple maxPacket sized concurrent reads
@@ -860,7 +879,7 @@ func (f *File) ReadAt(b []byte, off int64) (n int, err error) {
860879
// overlapping round trip times.
861880
inFlight := 0
862881
desiredInFlight := 1
863-
offset := uint64( off )
882+
offset := uint64(off)
864883
// maxConcurrentRequests buffer to deal with broadcastErr() floods
865884
// also must have a buffer of max value of (desiredInFlight - inFlight)
866885
ch := make(chan result, f.c.maxConcurrentRequests+1)
@@ -1280,8 +1299,9 @@ func (f *File) Chmod(mode os.FileMode) error {
12801299
// that if the size is less than its current size it will be truncated to fit,
12811300
// the SFTP protocol does not specify what behavior the server should do when setting
12821301
// size greater than the current size.
1302+
// We send a SSH_FXP_FSETSTAT here since we have a file handle
12831303
func (f *File) Truncate(size int64) error {
1284-
return f.c.Truncate(f.path, size)
1304+
return f.c.setfstat(f.handle, sshFileXferAttrSize, uint64(size))
12851305
}
12861306

12871307
func min(a, b int) int {

request-example.go

+19
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ func (fs *root) Filecmd(r *Request) error {
7878
defer fs.filesLock.Unlock()
7979
switch r.Method {
8080
case "Setstat":
81+
file, err := fs.fetch(r.Filepath)
82+
if err != nil {
83+
return err
84+
}
85+
if r.AttrFlags().Size {
86+
return file.Truncate(int64(r.Attributes().Size))
87+
}
8188
return nil
8289
case "Rename":
8390
file, err := fs.fetch(r.Filepath)
@@ -302,6 +309,18 @@ func (f *memFile) WriteAt(p []byte, off int64) (int, error) {
302309
return len(p), nil
303310
}
304311

312+
func (f *memFile) Truncate(size int64) error {
313+
f.contentLock.Lock()
314+
defer f.contentLock.Unlock()
315+
grow := size - int64(len(f.content))
316+
if grow <= 0 {
317+
f.content = f.content[:size]
318+
} else {
319+
f.content = append(f.content, make([]byte, grow)...)
320+
}
321+
return nil
322+
}
323+
305324
func (f *memFile) TransferError(err error) {
306325
f.transferError = err
307326
}

request-server.go

+9
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ func (rs *RequestServer) packetWorker(
214214
request = NewRequest("Stat", request.Filepath)
215215
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
216216
}
217+
case *sshFxpFsetstatPacket:
218+
handle := pkt.getHandle()
219+
request, ok := rs.getRequest(handle)
220+
if !ok {
221+
rpkt = statusFromError(pkt, syscall.EBADF)
222+
} else {
223+
request = NewRequest("Setstat", request.Filepath)
224+
rpkt = request.call(rs.Handlers, pkt, rs.pktMgr.alloc, orderID)
225+
}
217226
case *sshFxpExtendedPacketPosixRename:
218227
request := NewRequest("Rename", pkt.Oldpath)
219228
request.Target = pkt.Newpath

request-server_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
1213
)
1314

1415
var _ = fmt.Print
@@ -345,6 +346,37 @@ func TestRequestFstat(t *testing.T) {
345346
checkRequestServerAllocator(t, p)
346347
}
347348

349+
func TestRequestFsetstat(t *testing.T) {
350+
p := clientRequestServerPair(t)
351+
defer p.Close()
352+
_, err := putTestFile(p.cli, "/foo", "hello")
353+
require.NoError(t, err)
354+
fp, err := p.cli.OpenFile("/foo", os.O_WRONLY)
355+
require.NoError(t, err)
356+
err = fp.Truncate(2)
357+
fi, err := fp.Stat()
358+
require.NoError(t, err)
359+
assert.Equal(t, fi.Name(), "foo")
360+
assert.Equal(t, fi.Size(), int64(2))
361+
err = fp.Truncate(5)
362+
require.NoError(t, err)
363+
fi, err = fp.Stat()
364+
require.NoError(t, err)
365+
assert.Equal(t, fi.Name(), "foo")
366+
assert.Equal(t, fi.Size(), int64(5))
367+
err = fp.Close()
368+
assert.NoError(t, err)
369+
rf, err := p.cli.Open("/foo")
370+
assert.NoError(t, err)
371+
defer rf.Close()
372+
contents := make([]byte, 20)
373+
n, err := rf.Read(contents)
374+
assert.EqualError(t, err, io.EOF.Error())
375+
assert.Equal(t, 5, n)
376+
assert.Equal(t, []byte{'h', 'e', 0, 0, 0}, contents[0:n])
377+
checkRequestServerAllocator(t, p)
378+
}
379+
348380
func TestRequestStatFail(t *testing.T) {
349381
p := clientRequestServerPair(t)
350382
defer p.Close()

0 commit comments

Comments
 (0)