aboutsummaryrefslogtreecommitdiff
path: root/lib/ssh/sftp
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2021-07-11 20:44:47 +0700
committerShulhan <ms@kilabit.info>2021-07-12 02:23:47 +0700
commitdcc872983f80cc7e5516212dfad727ed2f9fdfa4 (patch)
tree58965a2f86bcb667e489291eaf6e729563bd432f /lib/ssh/sftp
parent2f6bdb4e2cce7b5bdf4255b21f2588b1d6b7444f (diff)
downloadpakakeh.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.go53
-rw-r--r--lib/ssh/sftp/client_test.go11
-rw-r--r--lib/ssh/sftp/dir_entry.go33
-rw-r--r--lib/ssh/sftp/file_attrs.go92
-rw-r--r--lib/ssh/sftp/file_attrs_test.go42
-rw-r--r--lib/ssh/sftp/file_handle.go3
-rw-r--r--lib/ssh/sftp/node.go14
-rw-r--r--lib/ssh/sftp/packet.go10
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)