diff --git a/pkg/httperrors/fserror.go b/pkg/httperrors/fserror.go new file mode 100644 index 0000000000..b4b39529b0 --- /dev/null +++ b/pkg/httperrors/fserror.go @@ -0,0 +1,53 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httperrors + +import ( + "io/fs" + "os" + + "yunion.io/x/pkg/errors" +) + +/* + ErrInvalid = fs.ErrInvalid // "invalid argument" + + ErrPermission = fs.ErrPermission // "permission denied" + ErrExist = fs.ErrExist // "file already exists" + ErrNotExist = fs.ErrNotExist // "file does not exist" + ErrClosed = fs.ErrClosed // "file already closed" + + ErrNoDeadline = errNoDeadline() // "file type does not support deadline" + ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout" +*/ +func FsErrorNormalize(err error) error { + switch errors.Cause(err) { + case fs.ErrInvalid: + return errors.Wrap(ErrInputParameter, "invalid argument") + case fs.ErrPermission: + return errors.Wrap(ErrForbidden, "permission denied") + case fs.ErrExist: + return errors.Wrap(ErrConflict, "file already exists") + case fs.ErrNotExist: + return errors.Wrap(ErrNotFound, "file does not exist") + case fs.ErrClosed: + return errors.Wrap(ErrInvalidStatus, "file already closed") + case os.ErrNoDeadline: + return errors.Wrap(ErrNotSupported, "file type does not support deadline") + case os.ErrDeadlineExceeded: + return errors.Wrap(ErrTimeout, "i/o timeout") + } + return err +} diff --git a/pkg/httperrors/fserror_test.go b/pkg/httperrors/fserror_test.go new file mode 100644 index 0000000000..960b38a27a --- /dev/null +++ b/pkg/httperrors/fserror_test.go @@ -0,0 +1,65 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httperrors + +import ( + "io/fs" + "os" + "testing" + + "yunion.io/x/pkg/errors" +) + +func TestFsErrorNormalize(t *testing.T) { + cases := []struct { + inErr error + want error + }{ + { + inErr: fs.ErrInvalid, + want: ErrInputParameter, + }, + { + inErr: fs.ErrPermission, + want: ErrForbidden, + }, + { + inErr: fs.ErrExist, + want: ErrConflict, + }, + { + inErr: fs.ErrNotExist, + want: ErrNotFound, + }, + { + inErr: fs.ErrClosed, + want: ErrInvalidStatus, + }, + { + inErr: os.ErrNoDeadline, + want: ErrNotSupported, + }, + { + inErr: os.ErrDeadlineExceeded, + want: ErrTimeout, + }, + } + for _, c := range cases { + got := FsErrorNormalize(c.inErr) + if errors.Cause(got) != c.want { + t.Errorf("inErr %s want %s got %s", c.inErr, c.want, got) + } + } +} diff --git a/pkg/webconsole/server/sftp_server.go b/pkg/webconsole/server/sftp_server.go index 740dd3df84..e3fe20ab6b 100644 --- a/pkg/webconsole/server/sftp_server.go +++ b/pkg/webconsole/server/sftp_server.go @@ -16,7 +16,6 @@ package server import ( "context" - "fmt" "io" "io/fs" "net/http" @@ -129,7 +128,7 @@ func HandleSftpList(ctx context.Context, w http.ResponseWriter, r *http.Request) } files, err := client.ReadDir(dir) if err != nil { - return nil, errors.Wrapf(err, "ReadDir %s", dir) + return nil, errors.Wrapf(httperrors.FsErrorNormalize(err), "ReadDir %s", dir) } ret := Files{} for _, f := range files { @@ -157,6 +156,8 @@ func HandleSftpList(ctx context.Context, w http.ResponseWriter, r *http.Request) vv.LinkFile.ModeNum = stat.Mode().Perm() vv.LinkFile.IsRegular = stat.Mode().IsRegular() } + } else { + err = httperrors.FsErrorNormalize(err) } } ret = append(ret, vv) @@ -195,12 +196,12 @@ func HandleSftpUpload(ctx context.Context, w http.ResponseWriter, r *http.Reques _, err = sftp.Stat(dir) if err != nil { - return errors.Wrapf(err, "stat %s", dir) + return errors.Wrapf(httperrors.FsErrorNormalize(err), "stat %s", dir) } newFile, err := sftp.Create(path.Join(dir, header.Filename)) if err != nil { - return errors.Wrapf(err, "create file") + return errors.Wrapf(httperrors.FsErrorNormalize(err), "create file") } defer file.Close() @@ -208,7 +209,7 @@ func HandleSftpUpload(ctx context.Context, w http.ResponseWriter, r *http.Reques _, err = newFile.ReadFrom(file) if err != nil { - return errors.Wrapf(err, "ReadFrom") + return errors.Wrapf(httperrors.FsErrorNormalize(err), "ReadFrom") } return nil }() @@ -235,22 +236,25 @@ func HandleSftpDownload(ctx context.Context, w http.ResponseWriter, r *http.Requ } file, err := sftp.Stat(dir) if err != nil { - return errors.Wrapf(err, "stat %s", dir) + return errors.Wrapf(httperrors.FsErrorNormalize(err), "stat %s", dir) } if file.IsDir() { - return fmt.Errorf("dir %s can not download", dir) + return errors.Wrapf(httperrors.ErrInvalidStatus, "dir %s can not be downloaded", dir) } reader, err := sftp.Open(dir) if err != nil { - return errors.Wrapf(err, "open file") + return errors.Wrapf(httperrors.FsErrorNormalize(err), "open file") } defer reader.Close() w.Header().Add("Content-Disposition", "attachment;filename*=utf-8''"+strings.ReplaceAll(url.QueryEscape(file.Name()), "+", "%20")) w.Header().Add("Content-Type", "application/octet-stream") _, err = io.Copy(w, reader) - return err + if err != nil { + return errors.Wrap(httperrors.FsErrorNormalize(err), "Copy") + } + return nil }() if err != nil { httperrors.GeneralServerError(ctx, w, err)