Fixes #184; better fix for file batching
I didn't like how the initial fix for #184 required the handler author
to track the token/offset for the filelist. I came up with a way to do
it that mimic'd the FileReader handler interface using a method
something like ReaderAt except for file lists.
changed MaxFileinfoList to a var for testing and tweaking
Renaming FileInfoer interface to FileLister.
FileInfoer -> FileLister
Fileinfo -> Filelist
diff --git a/request-example.go b/request-example.go
index 3333a8d..c355fbd 100644
--- a/request-example.go
+++ b/request-example.go
@@ -11,7 +11,6 @@
"os"
"path/filepath"
"sort"
- "strconv"
"sync"
"time"
)
@@ -101,20 +100,27 @@
return nil
}
-func (fs *root) Fileinfo(r Request) ([]os.FileInfo, error) {
+type listerat []os.FileInfo
+
+// Modeled after strings.Reader's ReadAt() implementation
+func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) {
+ var n int
+ if offset >= int64(len(f)) {
+ return 0, io.EOF
+ }
+ n = copy(ls, f[offset:])
+ if n < len(ls) {
+ return n, io.EOF
+ }
+ return n, nil
+}
+
+func (fs *root) Filelist(r Request) (ListerAt, error) {
fs.filesLock.Lock()
defer fs.filesLock.Unlock()
+
switch r.Method {
case "List":
- var err error
- batch_size := 10
- current_offset := 0
- if token := r.LsNext(); token != "" {
- current_offset, err = strconv.Atoi(token)
- if err != nil {
- return nil, os.ErrInvalid
- }
- }
ordered_names := []string{}
for fn, _ := range fs.files {
if filepath.Dir(fn) == r.Filepath {
@@ -126,21 +132,13 @@
for i, fn := range ordered_names {
list[i] = fs.files[fn]
}
- if len(list) < current_offset {
- return nil, io.EOF
- }
- new_offset := current_offset + batch_size
- if new_offset > len(list) {
- new_offset = len(list)
- }
- r.LsSave(strconv.Itoa(new_offset))
- return list[current_offset:new_offset], nil
+ return listerat(list), nil
case "Stat":
file, err := fs.fetch(r.Filepath)
if err != nil {
return nil, err
}
- return []os.FileInfo{file}, nil
+ return listerat([]os.FileInfo{file}), nil
case "Readlink":
file, err := fs.fetch(r.Filepath)
if err != nil {
@@ -152,7 +150,7 @@
return nil, err
}
}
- return []os.FileInfo{file}, nil
+ return listerat([]os.FileInfo{file}), nil
}
return nil, nil
}
diff --git a/request-interfaces.go b/request-interfaces.go
index 46db449..f7afb14 100644
--- a/request-interfaces.go
+++ b/request-interfaces.go
@@ -23,8 +23,16 @@
Filecmd(Request) error
}
-// FileInfoer should return file listing info and errors (readdir, stat)
-// note stat requests would return a list of 1
-type FileInfoer interface {
- Fileinfo(Request) ([]os.FileInfo, error)
+// FileLister should return file info interface and errors (readdir, stat)
+type FileLister interface {
+ Filelist(Request) (ListerAt, error)
+}
+
+// ListerAt does for file lists what io.ReaderAt does for files.
+// ListerAt should return the number of entries copied and an io.EOF
+// error if at end of list. This is testable by comparing how many you
+// copied to how many could be copied (eg. n < len(ls) below).
+// The copy() builtin is best for the copying.
+type ListerAt interface {
+ ListAt([]os.FileInfo, int64) (int, error)
}
diff --git a/request-server.go b/request-server.go
index b51a10b..b0cad92 100644
--- a/request-server.go
+++ b/request-server.go
@@ -21,7 +21,7 @@
FileGet FileReader
FilePut FileWriter
FileCmd FileCmder
- FileInfo FileInfoer
+ FileList FileLister
}
// RequestServer abstracts the sftp protocol with an http request-like protocol
diff --git a/request-server_test.go b/request-server_test.go
index ee9621b..60aed2a 100644
--- a/request-server_test.go
+++ b/request-server_test.go
@@ -315,6 +315,7 @@
func TestRequestReaddir(t *testing.T) {
p := clientRequestServerPair(t)
+ MaxFilelist = 22 // make not divisible by our test amount (100)
defer p.Close()
for i := 0; i < 100; i++ {
fname := fmt.Sprintf("/foo_%02d", i)
diff --git a/request.go b/request.go
index 6d7d6ff..795f800 100644
--- a/request.go
+++ b/request.go
@@ -11,6 +11,9 @@
"github.com/pkg/errors"
)
+// MaxFilelist is the max number of files to return in a readdir batch.
+var MaxFilelist int64 = 100
+
// Request contains the data and state for the incoming service request.
type Request struct {
// Get, Put, Setstat, Stat, Rename, Remove
@@ -29,10 +32,11 @@
}
type state struct {
- writerAt io.WriterAt
- readerAt io.ReaderAt
- endofdir bool // in case handler doesn't use EOF on file list
- readdirToken string
+ writerAt io.WriterAt
+ readerAt io.ReaderAt
+ listerAt ListerAt
+ endofdir bool // in case handler doesn't use EOF on file list
+ lsoffset int64
}
type packet_data struct {
@@ -68,20 +72,13 @@
return request
}
-// LsSave takes a token to keep track of file list batches. Openssh uses a
-// batch size of 100, so I suggest sticking close to that.
-func (r Request) LsSave(token string) {
+// Returns current offset for file list, and sets next offset
+func (r Request) lsNext(offset int64) (current int64) {
r.stateLock.RLock()
defer r.stateLock.RUnlock()
- r.state.readdirToken = token
-}
-
-// LsNext should return the token from the previous call to know which batch
-// to return next.
-func (r Request) LsNext() string {
- r.stateLock.RLock()
- defer r.stateLock.RUnlock()
- return r.state.readdirToken
+ current = r.state.lsoffset
+ r.state.lsoffset = r.state.lsoffset + offset
+ return current
}
// manage file read/write state
@@ -93,7 +90,10 @@
r.state.writerAt = s
case io.ReaderAt:
r.state.readerAt = s
-
+ case ListerAt:
+ r.state.listerAt = s
+ case int64:
+ r.state.lsoffset = s
}
}
@@ -109,6 +109,12 @@
return r.state.readerAt
}
+func (r Request) getLister() ListerAt {
+ r.stateLock.RLock()
+ defer r.stateLock.RUnlock()
+ return r.state.listerAt
+}
+
// For backwards compatibility. The Handler didn't have batch handling at
// first, and just always assumed 1 batch. This preserves that behavior.
func (r Request) setEOD(eod bool) {
@@ -157,7 +163,7 @@
case "Setstat", "Rename", "Rmdir", "Mkdir", "Symlink", "Remove":
rpkt, err = filecmd(handlers.FileCmd, r)
case "List", "Stat", "Readlink":
- rpkt, err = fileinfo(handlers.FileInfo, r)
+ rpkt, err = filelist(handlers.FileList, r)
default:
return rpkt, errors.Errorf("unexpected method: %s", r.Method)
}
@@ -179,6 +185,7 @@
pd := r.popPacket()
data := make([]byte, clamp(pd.length, maxTxPacket))
n, err := reader.ReadAt(data, pd.offset)
+ // only return EOF erro if no data left to read
if err != nil && (err != io.EOF || n == 0) {
return nil, err
}
@@ -226,21 +233,45 @@
}}, nil
}
-// wrap FileInfoer handler
-func fileinfo(h FileInfoer, r Request) (responsePacket, error) {
- if r.getEOD() {
- return nil, io.EOF
+// wrap FileLister handler
+func filelist(h FileLister, r Request) (responsePacket, error) {
+ var err error
+ lister := r.getLister()
+ if lister == nil {
+ lister, err = h.Filelist(r)
+ if err != nil {
+ return nil, err
+ }
+ r.setFileState(lister)
}
- finfo, err := h.Fileinfo(r)
- if err != nil {
+
+ offset := r.lsNext(MaxFilelist)
+ finfo := make([]os.FileInfo, MaxFilelist)
+ n, err := lister.ListAt(finfo, offset)
+ // ignore EOF as we only return it when there are no results
+ if err != nil && err != io.EOF {
return nil, err
}
+ finfo = finfo[:n] // avoid need for nil tests below
+
+ // no results
+ if n == 0 {
+ switch r.Method {
+ case "List":
+ return nil, io.EOF
+ case "Stat", "Readlink":
+ err = &os.PathError{Op: "readlink", Path: r.Filepath,
+ Err: syscall.ENOENT}
+ return nil, err
+ }
+ }
switch r.Method {
case "List":
pd := r.popPacket()
dirname := path.Base(r.Filepath)
ret := &sshFxpNamePacket{ID: pd.id}
+
for _, fi := range finfo {
ret.NameAttrs = append(ret.NameAttrs, sshFxpNameAttr{
Name: fi.Name(),
@@ -248,31 +279,13 @@
Attrs: []interface{}{fi},
})
}
- // No entries means we should return EOF as the Handler didn't.
- if len(finfo) == 0 {
- return nil, io.EOF
- }
- // If files are returned but no token is set, return EOF next call.
- if r.LsNext() == "" {
- r.setEOD(true)
- }
return ret, nil
case "Stat":
- if len(finfo) == 0 {
- err = &os.PathError{Op: "stat", Path: r.Filepath,
- Err: syscall.ENOENT}
- return nil, err
- }
return &sshFxpStatResponse{
ID: r.pkt_id,
info: finfo[0],
}, nil
case "Readlink":
- if len(finfo) == 0 {
- err = &os.PathError{Op: "readlink", Path: r.Filepath,
- Err: syscall.ENOENT}
- return nil, err
- }
filename := finfo[0].Name()
return &sshFxpNamePacket{
ID: r.pkt_id,
@@ -282,8 +295,9 @@
Attrs: emptyFileStat,
}},
}, nil
+ default:
+ return nil, errors.Errorf("unexpected method: %s", r.Method)
}
- return nil, err
}
// file data for additional read/write packets
diff --git a/request_test.go b/request_test.go
index e537fc1..d08d54b 100644
--- a/request_test.go
+++ b/request_test.go
@@ -39,7 +39,7 @@
return nil
}
-func (t *testHandler) Fileinfo(r Request) ([]os.FileInfo, error) {
+func (t *testHandler) Filelist(r Request) (ListerAt, error) {
if t.err != nil {
return nil, t.err
}
@@ -51,7 +51,7 @@
if err != nil {
return nil, err
}
- return []os.FileInfo{fi}, nil
+ return listerat([]os.FileInfo{fi}), nil
}
// make sure len(fakefile) == len(filecontents)
@@ -98,7 +98,7 @@
FileGet: handler,
FilePut: handler,
FileCmd: handler,
- FileInfo: handler,
+ FileList: handler,
}
}