diff options
Diffstat (limited to 'lib/ssh')
| -rw-r--r-- | lib/ssh/sftp/client.go | 74 | ||||
| -rw-r--r-- | lib/ssh/sftp/client_test.go | 146 |
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) } } |
