summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2023-12-17 01:01:56 +0700
committerShulhan <ms@kilabit.info>2023-12-17 03:12:43 +0700
commit52ddfffc17fb81d11684db9d073bc565e232c4a8 (patch)
treed97c73c0f8fad64d82f7f71afcba8b4dbda16efd
parentcc1b44567b77dda34203f0fcddeecc2d093a2480 (diff)
downloadpakakeh.go-52ddfffc17fb81d11684db9d073bc565e232c4a8.tar.xz
ssh/sftp: implement method MkdirAll on Client
The MkdirAll create directory on the server, from left to right. Each directory is separated by '/', where the left part is the parent of the right part. This method is similar to [os.MkdirAll].
-rw-r--r--lib/ssh/sftp/client.go74
-rw-r--r--lib/ssh/sftp/client_test.go146
2 files changed, 213 insertions, 7 deletions
diff --git a/lib/ssh/sftp/client.go b/lib/ssh/sftp/client.go
index 47364f4f..79c89b48 100644
--- a/lib/ssh/sftp/client.go
+++ b/lib/ssh/sftp/client.go
@@ -10,6 +10,8 @@ import (
"io"
"io/fs"
"os"
+ "path"
+ "strings"
"sync"
"time"
@@ -273,6 +275,78 @@ func (cl *Client) Mkdir(path string, fa *FileAttrs) (err error) {
return nil
}
+// MkdirAll create directory on the server, from left to right.
+// Each directory is separated by '/', where the left part is the parent of
+// the right part.
+// This method is similar to [os.MkdirAll].
+//
+// Note that using `~` as home directory is not working.
+// If you want to create directory under home, use relative path, for
+// example "a/b/c" will create directory "~/a/b/c".
+func (cl *Client) MkdirAll(dir string, fa *FileAttrs) (err error) {
+ var logp = `MkdirAll`
+
+ if fa == nil {
+ fa = newFileAttrs()
+ fa.SetPermissions(0700)
+ }
+
+ dir = path.Clean(dir)
+ var (
+ listDir = strings.Split(dir, `/`)
+
+ req *packet
+ res *packet
+ lastErr error
+ item string
+ payload []byte
+ )
+ dir = ``
+ for _, item = range listDir {
+ if item == `` && len(listDir) > 1 {
+ // The first item of "/a/b" in [strings.Split] is
+ // empty, which indicate the root.
+ item = `/`
+ }
+ dir = path.Join(dir, item)
+ if dir == `/` {
+ // Skip creating root, since it should be already
+ // exist.
+ continue
+ }
+ if dir == `` {
+ // Skip creating home directory, since it should be
+ // already exist.
+ continue
+ }
+
+ req = cl.generatePacket()
+ payload = req.fxpMkdir(dir, fa)
+
+ res, err = cl.send(payload)
+ if err != nil {
+ return fmt.Errorf(`%s: %w`, logp, err)
+ }
+ if res.kind != packetKindFxpStatus {
+ return errUnexpectedResponse(packetKindFxpStatus, res.kind)
+ }
+ if res.code != statusCodeOK {
+ if res.code == statusCodeFailure {
+ // Directory may already exist but its
+ // returned as failure, keep going.
+ continue
+ }
+ lastErr = handleStatusCode(res.code, res.message)
+ }
+ // Reset last error if its success.
+ lastErr = nil
+ }
+ if lastErr != nil {
+ return lastErr
+ }
+ return nil
+}
+
// Open the remote file for read only.
func (cl *Client) Open(remoteFile string) (fh *FileHandle, err error) {
return cl.OpenFile(remoteFile, OpenFlagRead, nil)
diff --git a/lib/ssh/sftp/client_test.go b/lib/ssh/sftp/client_test.go
index dd235cff..b10e9e8c 100644
--- a/lib/ssh/sftp/client_test.go
+++ b/lib/ssh/sftp/client_test.go
@@ -5,6 +5,7 @@
package sftp
import (
+ "io/fs"
"testing"
"github.com/shuLhan/share/lib/test"
@@ -95,15 +96,146 @@ func TestClient_Mkdir(t *testing.T) {
t.Skipf("%s not set", envNameTestManual)
}
- path := "/tmp/lib-ssh-sftp-mkdir"
- err := testClient.Mkdir(path, nil)
- if err != nil {
- t.Fatal(err)
+ type testCase struct {
+ path string
+ expError string
}
- err = testClient.Rmdir(path)
- if err != nil {
- t.Fatal(err)
+ var cases = []testCase{{
+ path: `/tmp/lib-ssh-sftp-mkdir`,
+ }, {
+ path: `/perm`,
+ expError: fs.ErrPermission.Error(),
+ }}
+
+ var (
+ c testCase
+ err error
+ )
+
+ for _, c = range cases {
+ t.Log(c.path)
+
+ err = testClient.Mkdir(c.path, nil)
+ if err != nil {
+ test.Assert(t, `error`, c.expError, err.Error())
+ continue
+ }
+
+ err = testClient.Rmdir(c.path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestClient_MkdirAll(t *testing.T) {
+ if !isTestManual {
+ t.Skipf(`%s not set`, envNameTestManual)
+ }
+
+ type testCase struct {
+ expStat *FileAttrs
+ path string
+ expError string
+ }
+
+ var cases = []testCase{{
+ path: `/tmp/a/b/c`,
+ expStat: &FileAttrs{
+ name: `/tmp/a/b/c`,
+ fsMode: fs.ModeDir | 0700,
+ flags: attrSize | attrUIDGID | attrPermissions | attrAcModtime,
+ size: 40,
+ permissions: fileTypeDirectory | 0700,
+ uid: 1000,
+ gid: 1000,
+ },
+ }, {
+ // Creating the same directory should not return an error.
+ path: `/tmp/a/b/c`,
+ expStat: &FileAttrs{
+ name: `/tmp/a/b/c`,
+ fsMode: fs.ModeDir | 0700,
+ flags: attrSize | attrUIDGID | attrPermissions | attrAcModtime,
+ size: 40,
+ permissions: fileTypeDirectory | 0700,
+ uid: 1000,
+ gid: 1000,
+ },
+ }, {
+ path: ``,
+ expStat: &FileAttrs{
+ name: `.`,
+ fsMode: fs.ModeDir | 0755,
+ flags: attrSize | attrUIDGID | attrPermissions | attrAcModtime,
+ size: 4096,
+ permissions: fileTypeDirectory | 0755,
+ uid: 1000,
+ gid: 33,
+ },
+ }, {
+ path: `.cache/a/b/c`,
+ expStat: &FileAttrs{
+ name: `.cache/a/b/c`,
+ fsMode: fs.ModeDir | 0700,
+ flags: attrSize | attrUIDGID | attrPermissions | attrAcModtime,
+ size: 4096,
+ permissions: fileTypeDirectory | 0700,
+ uid: 1000,
+ gid: 1000,
+ },
+ }, {
+ // Creating the same directory should not return an error.
+ path: `.cache/a/b/c`,
+ expStat: &FileAttrs{
+ name: `.cache/a/b/c`,
+ fsMode: fs.ModeDir | 0700,
+ flags: attrSize | attrUIDGID | attrPermissions | attrAcModtime,
+ size: 4096,
+ permissions: fileTypeDirectory | 0700,
+ uid: 1000,
+ gid: 1000,
+ },
+ }, {
+ path: `.cache/a b/c`,
+ expStat: &FileAttrs{
+ name: `.cache/a b/c`,
+ fsMode: fs.ModeDir | 0700,
+ flags: attrSize | attrUIDGID | attrPermissions | attrAcModtime,
+ size: 4096,
+ permissions: fileTypeDirectory | 0700,
+ uid: 1000,
+ gid: 1000,
+ },
+ }}
+
+ var (
+ c testCase
+ gotStat *FileAttrs
+ err error
+ )
+
+ for _, c = range cases {
+ t.Log(c.path)
+
+ err = testClient.MkdirAll(c.path, nil)
+ if err != nil {
+ test.Assert(t, `error`, c.expError, err.Error())
+ continue
+ }
+
+ gotStat, err = testClient.Stat(c.path)
+ if err != nil {
+ test.Assert(t, `error`, c.expError, err.Error())
+ continue
+ }
+
+ // Exclude access and modification times from being checked.
+ gotStat.atime = 0
+ gotStat.mtime = 0
+
+ test.Assert(t, `Stat `+c.path, c.expStat, gotStat)
}
}