Skip to content

Commit

Permalink
request-server: Introduce ReadlinkFileLister
Browse files Browse the repository at this point in the history
ReadlinkFileLister with its Readlink method allows returning paths without
misusing the os.FileInfo interface, whose Name() method should only return
the base name of a file.

By implementing ReadlinkFileLister, it is possible to easily return
symlinks of any kind (absolute, relative, multiple directory levels)
  • Loading branch information
georgmu committed Oct 12, 2022
1 parent db99a85 commit 40d3e29
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 19 deletions.
17 changes: 1 addition & 16 deletions request-example.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,21 +391,6 @@ func (fs *root) Filelist(r *Request) (ListerAt, error) {
return nil, err
}
return listerat{file}, nil

case "Readlink":
symlink, err := fs.readlink(r.Filepath)
if err != nil {
return nil, err
}

// SFTP-v2: The server will respond with a SSH_FXP_NAME packet containing only
// one name and a dummy attributes value.
return listerat{
&memFile{
name: symlink,
err: os.ErrNotExist, // prevent accidental use as a reader/writer.
},
}, nil
}

return nil, errors.New("unsupported")
Expand Down Expand Up @@ -434,7 +419,7 @@ func (fs *root) readdir(pathname string) ([]os.FileInfo, error) {
return files, nil
}

func (fs *root) readlink(pathname string) (string, error) {
func (fs *root) Readlink(pathname string) (string, error) {
file, err := fs.lfetch(pathname)
if err != nil {
return "", err
Expand Down
16 changes: 15 additions & 1 deletion request-interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ type StatVFSFileCmder interface {
// FileLister should return an object that fulfils the ListerAt interface
// Note in cases of an error, the error text will be sent to the client.
// Called for Methods: List, Stat, Readlink
// For Readlink, Filelist is limited to os.FileInfo response. For a more
// sophisticated Readlink response, please implement ReadlinkFileLister.
// If ReadlinkFileLister is implemented, its Readlink method is called
// for method Readlink instead of calling Filelist
type FileLister interface {
Filelist(*Request) (ListerAt, error)
}
Expand All @@ -94,7 +98,7 @@ type LstatFileLister interface {
//
// Up to v1.13.5 the signature for the RealPath method was:
//
// RealPath(string) string
// # RealPath(string) string
//
// we have added a legacyRealPathFileLister that implements the old method
// to ensure that your code does not break.
Expand All @@ -104,6 +108,16 @@ type RealPathFileLister interface {
RealPath(string) (string, error)
}

// ReadlinkFileLister is a FileLister that implements the Readlink method.
// By implementing the Readlink method, it is possible to give a better
// response than via the default FileLister, since Readlink can return
// a better result than FileLister (which is limited to os.FileInfo, whose
// Name() should only return the base name of a file)
type ReadlinkFileLister interface {
FileLister
Readlink(string) (string, error)
}

// This interface is here for backward compatibility only
type legacyRealPathFileLister interface {
FileLister
Expand Down
14 changes: 13 additions & 1 deletion request-server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,10 @@ func TestRequestSymlink(t *testing.T) {
for _, s := range symlinks {
err = p.cli.Symlink(s.target, s.name)
require.NoError(t, err, "Creating symlink '%s' with target '%s' failed", s.name, s.target)

rl, err := p.cli.ReadLink(s.name)
require.NoError(t, err, "ReadLink('%s') failed", s.name)
require.Equal(t, s.target, rl, "Unexpected result when reading symlink '%s'", s.name)
}

// test fetching via symlink
Expand Down Expand Up @@ -750,9 +754,17 @@ func TestRequestReadlink(t *testing.T) {
require.NoError(t, err)
err = p.cli.Symlink("/foo", "/bar")
require.NoError(t, err)

rl, err := p.cli.ReadLink("/bar")
assert.NoError(t, err)
assert.Equal(t, "foo", rl)
assert.Equal(t, "/foo", rl)

_, err = p.cli.ReadLink("/foo")
assert.Error(t, err, "Readlink on non-symlink should fail")

_, err = p.cli.ReadLink("/foobar")
assert.Error(t, err, "Readlink on non-existent file should fail")

checkRequestServerAllocator(t, p)
}

Expand Down
24 changes: 23 additions & 1 deletion request.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,12 @@ func (r *Request) call(handlers Handlers, pkt requestPacket, alloc *allocator, o
return filecmd(handlers.FileCmd, r, pkt)
case "List":
return filelist(handlers.FileList, r, pkt)
case "Stat", "Lstat", "Readlink":
case "Stat", "Lstat":
return filestat(handlers.FileList, r, pkt)
case "Readlink":
if readlinkFileLister, ok := handlers.FileList.(ReadlinkFileLister); ok {
return readlink(readlinkFileLister, r, pkt)
}
return filestat(handlers.FileList, r, pkt)
default:
return statusFromError(pkt.id(), fmt.Errorf("unexpected method: %s", r.Method))
Expand Down Expand Up @@ -599,6 +604,23 @@ func filestat(h FileLister, r *Request, pkt requestPacket) responsePacket {
}
}

func readlink(readlinkFileLister ReadlinkFileLister, r *Request, pkt requestPacket) responsePacket {
resolved, err := readlinkFileLister.Readlink(r.Filepath)
if err != nil {
return statusFromError(pkt.id(), err)
}
return &sshFxpNamePacket{
ID: pkt.id(),
NameAttrs: []*sshFxpNameAttr{
{
Name: resolved,
LongName: resolved,
Attrs: emptyFileStat,
},
},
}
}

// init attributes of request object from packet data
func requestMethod(p requestPacket) (method string) {
switch p.(type) {
Expand Down

0 comments on commit 40d3e29

Please sign in to comment.