request-server: Introduce ReadlinkFileLister

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)
This commit is contained in:
Georg Müller 2022-10-14 17:12:56 +02:00
parent eb2fffbb98
commit 9183e7fd79
4 changed files with 42 additions and 18 deletions

View File

@ -391,21 +391,6 @@ func (fs *root) Filelist(r *Request) (ListerAt, error) {
return nil, err return nil, err
} }
return listerat{file}, nil 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") return nil, errors.New("unsupported")
@ -434,7 +419,7 @@ func (fs *root) readdir(pathname string) ([]os.FileInfo, error) {
return files, nil return files, nil
} }
func (fs *root) readlink(pathname string) (string, error) { func (fs *root) Readlink(pathname string) (string, error) {
file, err := fs.lfetch(pathname) file, err := fs.lfetch(pathname)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -74,6 +74,11 @@ type StatVFSFileCmder interface {
// FileLister should return an object that fulfils the ListerAt 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. // Note in cases of an error, the error text will be sent to the client.
// Called for Methods: List, Stat, Readlink // Called for Methods: List, Stat, Readlink
//
// Since Filelist returns an os.FileInfo, this can make it non-ideal for impelmenting Readlink.
// This is because the Name receiver method defined by that interface defines that it should only return the base name.
// However, Readlink is required to be capable of returning essentially any arbitrary valid path relative or absolute.
// In order to implement this more expressive requirement, implement [ReadlinkFileLister] which will then be used instead.
type FileLister interface { type FileLister interface {
Filelist(*Request) (ListerAt, error) Filelist(*Request) (ListerAt, error)
} }
@ -94,7 +99,7 @@ type LstatFileLister interface {
// //
// Up to v1.13.5 the signature for the RealPath method was: // 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 // we have added a legacyRealPathFileLister that implements the old method
// to ensure that your code does not break. // to ensure that your code does not break.
@ -104,6 +109,14 @@ type RealPathFileLister interface {
RealPath(string) (string, error) RealPath(string) (string, error)
} }
// ReadlinkFileLister is a FileLister that implements the Readlink method.
// By implementing the Readlink method, it is possible to return any arbitrary valid path relative or absolute.
// This allows giving a better response than via the default FileLister (which is limited to os.FileInfo, whose Name method 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 // This interface is here for backward compatibility only
type legacyRealPathFileLister interface { type legacyRealPathFileLister interface {
FileLister FileLister

View File

@ -607,6 +607,10 @@ func TestRequestSymlink(t *testing.T) {
for _, s := range symlinks { for _, s := range symlinks {
err := p.cli.Symlink(s.target, s.name) err := p.cli.Symlink(s.target, s.name)
require.NoError(t, err, "Creating symlink %q with target %q failed", s.name, s.target) require.NoError(t, err, "Creating symlink %q with target %q failed", s.name, s.target)
rl, err := p.cli.ReadLink(s.name)
require.NoError(t, err, "ReadLink(%q) failed", s.name)
require.Equal(t, s.target, rl, "Unexpected result when reading symlink %q", s.name)
} }
// test fetching via symlink // test fetching via symlink

View File

@ -295,7 +295,12 @@ func (r *Request) call(handlers Handlers, pkt requestPacket, alloc *allocator, o
return filecmd(handlers.FileCmd, r, pkt) return filecmd(handlers.FileCmd, r, pkt)
case "List": case "List":
return filelist(handlers.FileList, r, pkt) 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) return filestat(handlers.FileList, r, pkt)
default: default:
return statusFromError(pkt.id(), fmt.Errorf("unexpected method: %s", r.Method)) return statusFromError(pkt.id(), fmt.Errorf("unexpected method: %s", r.Method))
@ -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 // init attributes of request object from packet data
func requestMethod(p requestPacket) (method string) { func requestMethod(p requestPacket) (method string) {
switch p.(type) { switch p.(type) {