diff options
| author | Shulhan <ms@kilabit.info> | 2021-07-11 20:44:47 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2021-07-12 02:23:47 +0700 |
| commit | dcc872983f80cc7e5516212dfad727ed2f9fdfa4 (patch) | |
| tree | 58965a2f86bcb667e489291eaf6e729563bd432f /lib/ssh/sftp | |
| parent | 2f6bdb4e2cce7b5bdf4255b21f2588b1d6b7444f (diff) | |
| download | pakakeh.go-dcc872983f80cc7e5516212dfad727ed2f9fdfa4.tar.xz | |
ssh/sftp: make the package compatible with standard fs package
List of changes,
* Rename Node type to dirEntry and implement fs.DirEntry on it
* Change the Client Readdir, Readlink, and Realpath to return fs.DirEntry
* Make the response packet garbage collected by storing the result
in returned type and setting the response packet fields to nil
* Add field name to FileAttrs, which store the remote file name
* Implement fs.FileInfo interface in FileAttrs
* Store the remote path on FileHandle
Diffstat (limited to 'lib/ssh/sftp')
| -rw-r--r-- | lib/ssh/sftp/client.go | 53 | ||||
| -rw-r--r-- | lib/ssh/sftp/client_test.go | 11 | ||||
| -rw-r--r-- | lib/ssh/sftp/dir_entry.go | 33 | ||||
| -rw-r--r-- | lib/ssh/sftp/file_attrs.go | 92 | ||||
| -rw-r--r-- | lib/ssh/sftp/file_attrs_test.go | 42 | ||||
| -rw-r--r-- | lib/ssh/sftp/file_handle.go | 3 | ||||
| -rw-r--r-- | lib/ssh/sftp/node.go | 14 | ||||
| -rw-r--r-- | lib/ssh/sftp/packet.go | 10 |
8 files changed, 210 insertions, 48 deletions
diff --git a/lib/ssh/sftp/client.go b/lib/ssh/sftp/client.go index babd1dfa..bf61c347 100644 --- a/lib/ssh/sftp/client.go +++ b/lib/ssh/sftp/client.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "sync" "time" @@ -157,8 +158,10 @@ func (cl *Client) Fstat(fh *FileHandle) (fa *FileAttrs, err error) { if res.kind != packetKindFxpAttrs { return nil, ErrUnexpectedResponse(packetKindFxpAttrs, res.kind) } - - return res.fa, nil + fa = res.fa + fa.name = fh.remotePath + res.fa = nil + return fa, nil } // @@ -232,8 +235,10 @@ func (cl *Client) Lstat(remoteFile string) (fa *FileAttrs, err error) { if res.kind != packetKindFxpAttrs { return nil, ErrUnexpectedResponse(packetKindFxpAttrs, res.kind) } - - return res.fa, nil + fa = res.fa + fa.name = remoteFile + res.fa = nil + return fa, nil } // @@ -289,6 +294,7 @@ func (cl *Client) Opendir(path string) (fh *FileHandle, err error) { return nil, ErrUnexpectedResponse(packetKindFxpHandle, res.kind) } fh = res.fh + fh.remotePath = path res.fh = nil return fh, nil } @@ -375,14 +381,15 @@ func (cl *Client) Read(fh *FileHandle, offset uint64) (data []byte, err error) { if res.kind != packetKindFxpData { return nil, ErrUnexpectedResponse(packetKindFxpData, res.kind) } - - return res.data, nil + data = res.data + res.data = nil + return data, nil } // // Readdir list files and/or directories inside the handle. // -func (cl *Client) Readdir(fh *FileHandle) (nodes []*Node, err error) { +func (cl *Client) Readdir(fh *FileHandle) (nodes []fs.DirEntry, err error) { var ( logp = "Readdir" req = cl.generatePacket() @@ -400,7 +407,9 @@ func (cl *Client) Readdir(fh *FileHandle) (nodes []*Node, err error) { if res.kind != packetKindFxpName { return nil, ErrUnexpectedResponse(packetKindFxpName, res.kind) } - nodes = res.nodes + for _, node := range res.nodes { + nodes = append(nodes, node) + } res.nodes = nil return nodes, nil } @@ -408,7 +417,7 @@ func (cl *Client) Readdir(fh *FileHandle) (nodes []*Node, err error) { // // Readlink read the target of a symbolic link. // -func (cl *Client) Readlink(linkPath string) (node *Node, err error) { +func (cl *Client) Readlink(linkPath string) (node fs.DirEntry, err error) { var ( logp = "Readlink" req = cl.generatePacket() @@ -422,8 +431,9 @@ func (cl *Client) Readlink(linkPath string) (node *Node, err error) { if res.kind != packetKindFxpName { return nil, ErrUnexpectedResponse(packetKindFxpName, res.kind) } - - return res.nodes[0], nil + node = res.nodes[0] + res.nodes = nil + return node, nil } // @@ -431,7 +441,7 @@ func (cl *Client) Readlink(linkPath string) (node *Node, err error) { // This is useful for converting path names containing ".." components or // relative pathnames without a leading slash into absolute paths. // -func (cl *Client) Realpath(path string) (node *Node, err error) { +func (cl *Client) Realpath(path string) (node fs.DirEntry, err error) { var ( logp = "Realpath" req = cl.generatePacket() @@ -445,8 +455,9 @@ func (cl *Client) Realpath(path string) (node *Node, err error) { if res.kind != packetKindFxpName { return nil, ErrUnexpectedResponse(packetKindFxpName, res.kind) } - - return res.nodes[0], nil + node = res.nodes[0] + res.nodes = nil + return node, nil } // @@ -572,8 +583,10 @@ func (cl *Client) Stat(remoteFile string) (fa *FileAttrs, err error) { if res.kind != packetKindFxpAttrs { return nil, ErrUnexpectedResponse(packetKindFxpAttrs, res.kind) } - - return res.fa, nil + fa = res.fa + fa.name = remoteFile + res.fa = nil + return fa, nil } // @@ -654,7 +667,7 @@ func (cl *Client) init() (err error) { return nil } -func (cl *Client) open(remoteFile string, pflags uint32, fa *FileAttrs) (h *FileHandle, err error) { +func (cl *Client) open(remoteFile string, pflags uint32, fa *FileAttrs) (fh *FileHandle, err error) { var ( logp = "open" req = cl.generatePacket() @@ -674,8 +687,10 @@ func (cl *Client) open(remoteFile string, pflags uint32, fa *FileAttrs) (h *File err = fmt.Errorf("%s: %d %d %s", logp, res.kind, res.code, res.message) return nil, err } - - return res.fh, nil + fh = res.fh + fh.remotePath = remoteFile + res.fh = nil + return fh, nil } func (cl *Client) read() (res []byte, err error) { diff --git a/lib/ssh/sftp/client_test.go b/lib/ssh/sftp/client_test.go index aed7ddd7..79d1d4e7 100644 --- a/lib/ssh/sftp/client_test.go +++ b/lib/ssh/sftp/client_test.go @@ -70,7 +70,7 @@ func TestClient_Get(t *testing.T) { t.Skipf("%s not set", envNameTestManual) } - err := testClient.Get("/tmp/id_ed25519.pub", "testdata/id_ed25519.pub.get") + err := testClient.Get("/etc/hosts", "testdata/etc-hosts.get") if err != nil { t.Fatal(err) } @@ -136,7 +136,8 @@ func TestClient_Readdir(t *testing.T) { t.Logf("List of files inside the %s:\n", path) for x, node := range nodes { - t.Logf("%02d: %+v\n", x, node.LongName) + fi, _ := node.Info() + t.Logf("%02d: %s %+v\n", x, fi.Mode().String(), node.Name()) } } @@ -151,7 +152,7 @@ func TestClient_Realpath(t *testing.T) { } exp := "/etc/hosts" - test.Assert(t, "Realpath", exp, node.FileName) + test.Assert(t, "Realpath", exp, node.Name()) } func TestClient_Rename(t *testing.T) { @@ -189,6 +190,8 @@ func TestClient_Rename(t *testing.T) { t.Fatal(err) } + expAttrs.name = newPath + test.Assert(t, "Rename", expAttrs, gotAttrs) } @@ -277,5 +280,5 @@ func TestClient_Symlink(t *testing.T) { t.Fatal(err) } - test.Assert(t, "Readlink", targetPath, node.FileName) + test.Assert(t, "Readlink", targetPath, node.Name()) } diff --git a/lib/ssh/sftp/dir_entry.go b/lib/ssh/sftp/dir_entry.go new file mode 100644 index 00000000..fe41cf5d --- /dev/null +++ b/lib/ssh/sftp/dir_entry.go @@ -0,0 +1,33 @@ +// Copyright 2021, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sftp + +import "os" + +// +// dirEntry represent the internal data returned from Readdir, Readlink, or +// Realpath. +// +type dirEntry struct { + fileName string + longName string + attrs *FileAttrs +} + +func (de *dirEntry) Name() string { + return de.fileName +} + +func (de *dirEntry) IsDir() bool { + return de.attrs.IsDir() +} + +func (de *dirEntry) Type() os.FileMode { + return os.FileMode(de.attrs.permissions & fileTypeMask) +} + +func (de *dirEntry) Info() (os.FileInfo, error) { + return de.attrs, nil +} diff --git a/lib/ssh/sftp/file_attrs.go b/lib/ssh/sftp/file_attrs.go index a7c14e21..28c73381 100644 --- a/lib/ssh/sftp/file_attrs.go +++ b/lib/ssh/sftp/file_attrs.go @@ -8,6 +8,21 @@ import ( "encoding/binary" "io" "io/fs" + "time" +) + +const ( + fileModeSticky uint32 = 0001000 + fileModeSetgid uint32 = 0002000 + fileModeSetuid uint32 = 0004000 + fileTypeFifo uint32 = 0010000 + fileTypeCharDevice uint32 = 0020000 + fileTypeDirectory uint32 = 0040000 + fileTypeBlockDevice uint32 = 0060000 + fileTypeRegular uint32 = 0100000 + fileTypeSymlink uint32 = 0120000 + fileTypeSocket uint32 = 0140000 + fileTypeMask uint32 = 0170000 ) // List of valid values for FileAttrs.flags. @@ -23,6 +38,7 @@ const ( // FileAttrs define the attributes for opening or creating file on the remote. // type FileAttrs struct { + name string flags uint32 size uint64 // attr_SIZE uid uint32 // attr_UIDGID @@ -31,13 +47,16 @@ type FileAttrs struct { atime uint32 // attr_ACMODTIME mtime uint32 // attr_ACMODTIME exts extensions // attr_EXTENDED + fsMode fs.FileMode } // // NewFileAttrs create and initialize FileAttrs from FileInfo. // func NewFileAttrs(fi fs.FileInfo) (fa *FileAttrs) { - fa = &FileAttrs{} + fa = &FileAttrs{ + name: fi.Name(), + } mode := fi.Mode() mtime := fi.ModTime() @@ -77,6 +96,7 @@ func unpackFileAttrs(payload []byte) (fa *FileAttrs, length int) { fa.permissions = binary.BigEndian.Uint32(payload) payload = payload[4:] length += 4 + fa.updateFsmode() } if fa.flags&attr_ACMODTIME != 0 { fa.atime = binary.BigEndian.Uint32(payload) @@ -167,14 +187,32 @@ func (fa *FileAttrs) Gid() uint32 { } // -// AccessTime return the remote file modified time. +// IsDir return true if the file is a directory. +// +func (fa *FileAttrs) IsDir() bool { + return fa.fsMode.IsDir() +} + // -func (fa *FileAttrs) ModifiedTime() uint32 { - return fa.mtime +// ModTime return the remote file modified time. +// +func (fa *FileAttrs) ModTime() time.Time { + return time.Unix(int64(fa.mtime), 0) +} + +// +// Mode return the file mode bits as standard fs.FileMode type. +// +func (fa *FileAttrs) Mode() fs.FileMode { + return fa.fsMode +} + +func (fa *FileAttrs) Name() string { + return fa.name } // -// Permissions return the remote file permissions. +// Permissions return the remote file mode and permissions. // func (fa *FileAttrs) Permissions() uint32 { return fa.permissions @@ -221,6 +259,7 @@ func (fa *FileAttrs) SetModifiedTime(v uint32) { func (fa *FileAttrs) SetPermissions(v uint32) { fa.flags |= attr_PERMISSIONS fa.permissions = v + fa.updateFsmode() } // @@ -240,8 +279,51 @@ func (fa *FileAttrs) SetUid(uid uint32) { } // +// Size return the file size information. +// +func (fa *FileAttrs) Size() int64 { + return int64(fa.size) +} + +// +// Sys return the pointer to FileAttrs itself. +// +func (fa *FileAttrs) Sys() interface{} { + return fa +} + +// // Uid return the user ID of file. // func (fa *FileAttrs) Uid() uint32 { return fa.uid } + +func (fa *FileAttrs) updateFsmode() { + fa.fsMode = fs.FileMode(fa.permissions & 0777) + switch fa.permissions & fileTypeMask { + case fileTypeFifo: + fa.fsMode |= fs.ModeNamedPipe + case fileTypeCharDevice: + fa.fsMode |= fs.ModeDevice | fs.ModeCharDevice + case fileTypeDirectory: + fa.fsMode |= fs.ModeDir + case fileTypeBlockDevice: + fa.fsMode |= fs.ModeDevice + case fileTypeRegular: + // NOOP + case fileTypeSymlink: + fa.fsMode |= fs.ModeSymlink + case fileTypeSocket: + fa.fsMode |= fs.ModeSocket + } + if fa.permissions&fileModeSetgid != 0 { + fa.fsMode |= fs.ModeSetgid + } + if fa.permissions&fileModeSetuid != 0 { + fa.fsMode |= fs.ModeSetuid + } + if fa.permissions&fileModeSticky != 0 { + fa.fsMode |= fs.ModeSticky + } +} diff --git a/lib/ssh/sftp/file_attrs_test.go b/lib/ssh/sftp/file_attrs_test.go new file mode 100644 index 00000000..95bf893b --- /dev/null +++ b/lib/ssh/sftp/file_attrs_test.go @@ -0,0 +1,42 @@ +// Copyright 2021, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sftp + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/shuLhan/share/lib/test" +) + +func TestFileAttrs_Mode(t *testing.T) { + if !isTestManual { + t.Skipf("%s not set", envNameTestManual) + } + + cases := []struct { + path string + }{{ + path: "/etc", + }, { + path: "/etc/hosts", + }} + + for _, c := range cases { + exp, err := fs.Stat(os.DirFS(filepath.Dir(c.path)), filepath.Base(c.path)) + if err != nil { + t.Fatal(err) + } + + got, err := testClient.Stat(c.path) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, "Stat", exp.Mode(), got.Mode()) + } +} diff --git a/lib/ssh/sftp/file_handle.go b/lib/ssh/sftp/file_handle.go index 36467188..71c4ed81 100644 --- a/lib/ssh/sftp/file_handle.go +++ b/lib/ssh/sftp/file_handle.go @@ -5,5 +5,6 @@ package sftp type FileHandle struct { - v []byte + remotePath string // The remote path. + v []byte // The handle value returned from open(). } diff --git a/lib/ssh/sftp/node.go b/lib/ssh/sftp/node.go deleted file mode 100644 index dc038d9b..00000000 --- a/lib/ssh/sftp/node.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2021, Shulhan <ms@kilabit.info>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package sftp - -// -// Node represent the data returned from Readlink. -// -type Node struct { - FileName string - LongName string - Attrs *FileAttrs -} diff --git a/lib/ssh/sftp/packet.go b/lib/ssh/sftp/packet.go index 93a658d8..99299e0e 100644 --- a/lib/ssh/sftp/packet.go +++ b/lib/ssh/sftp/packet.go @@ -65,7 +65,7 @@ type packet struct { data []byte // FxpName - nodes []*Node + nodes []*dirEntry // FxpAttrs fa *FileAttrs @@ -131,19 +131,19 @@ func unpackPacket(payload []byte) (pac *packet, err error) { n := binary.BigEndian.Uint32(payload) payload = payload[4:] for x := uint32(0); x < n; x++ { - node := &Node{} + node := &dirEntry{} v = binary.BigEndian.Uint32(payload) payload = payload[4:] - node.FileName = string(payload[:v]) + node.fileName = string(payload[:v]) payload = payload[v:] v = binary.BigEndian.Uint32(payload) payload = payload[4:] - node.LongName = string(payload[:v]) + node.longName = string(payload[:v]) payload = payload[v:] - node.Attrs, length = unpackFileAttrs(payload) + node.attrs, length = unpackFileAttrs(payload) payload = payload[length:] pac.nodes = append(pac.nodes, node) |
