From 1e0b3a2a8c1e48852ccb37ee035946be0a36bb69 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 9 May 2022 19:09:43 +0200 Subject: [PATCH] web client: add share mode read/write Signed-off-by: Nicola Murino --- cmd/genman.go | 4 +- dataprovider/share.go | 13 ++- ftpd/ftpd_test.go | 4 +- ftpd/internal_test.go | 4 +- httpd/api_http_user.go | 2 +- httpd/api_shares.go | 53 +++++++--- httpd/api_utils.go | 17 +++- httpd/httpd_test.go | 147 +++++++++++++++++++++++++--- httpd/internal_test.go | 8 +- httpd/webclient.go | 29 ++++-- logger/logger.go | 3 +- sftpd/server.go | 5 +- sftpd/sftpd_test.go | 60 ++++++------ templates/webadmin/mfa.html | 7 +- templates/webclient/mfa.html | 9 +- templates/webclient/share.html | 7 +- templates/webclient/sharefiles.html | 133 +++++++++++++++++++++++++ util/util.go | 14 ++- vfs/osfs.go | 14 +-- vfs/sftpfs.go | 12 ++- webdavd/internal_test.go | 16 +-- webdavd/webdavd_test.go | 4 +- 22 files changed, 457 insertions(+), 108 deletions(-) diff --git a/cmd/genman.go b/cmd/genman.go index 3b8cde29..e9d94593 100644 --- a/cmd/genman.go +++ b/cmd/genman.go @@ -1,7 +1,9 @@ package cmd import ( + "errors" "fmt" + "io/fs" "os" "github.com/rs/zerolog" @@ -25,7 +27,7 @@ current directory. Run: func(cmd *cobra.Command, args []string) { logger.DisableLogger() logger.EnableConsoleLogger(zerolog.DebugLevel) - if _, err := os.Stat(manDir); os.IsNotExist(err) { + if _, err := os.Stat(manDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(manDir, os.ModePerm) if err != nil { logger.WarnToConsole("Unable to generate man page files: %v", err) diff --git a/dataprovider/share.go b/dataprovider/share.go index b2bb9d48..e4ec1193 100644 --- a/dataprovider/share.go +++ b/dataprovider/share.go @@ -21,6 +21,7 @@ type ShareScope int const ( ShareScopeRead ShareScope = iota + 1 ShareScopeWrite + ShareScopeReadWrite ) const ( @@ -64,10 +65,12 @@ type Share struct { // Used in web pages func (s *Share) GetScopeAsString() string { switch s.Scope { - case ShareScopeRead: - return "Read" - default: + case ShareScopeWrite: return "Write" + case ShareScopeReadWrite: + return "Read/Write" + default: + return "Read" } } @@ -194,7 +197,7 @@ func (s *Share) validatePaths() error { s.Paths[idx] = util.CleanPath(s.Paths[idx]) } s.Paths = util.RemoveDuplicates(s.Paths) - if s.Scope == ShareScopeWrite && len(s.Paths) != 1 { + if s.Scope >= ShareScopeWrite && len(s.Paths) != 1 { return util.NewValidationError("the write share scope requires exactly one path") } // check nested paths @@ -220,7 +223,7 @@ func (s *Share) validate() error { if s.Name == "" { return util.NewValidationError("name is mandatory") } - if s.Scope != ShareScopeRead && s.Scope != ShareScopeWrite { + if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite { return util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)) } if err := s.validatePaths(); err != nil { diff --git a/ftpd/ftpd_test.go b/ftpd/ftpd_test.go index d9d5874d..35b4542c 100644 --- a/ftpd/ftpd_test.go +++ b/ftpd/ftpd_test.go @@ -6,8 +6,10 @@ import ( "crypto/tls" "encoding/hex" "encoding/json" + "errors" "fmt" "io" + "io/fs" "net" "net/http" "os" @@ -3463,7 +3465,7 @@ func getExitCodeScriptContent(exitCode int) []byte { func createTestFile(path string, size int64) error { baseDir := filepath.Dir(path) - if _, err := os.Stat(baseDir); os.IsNotExist(err) { + if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(baseDir, os.ModePerm) if err != nil { return err diff --git a/ftpd/internal_test.go b/ftpd/internal_test.go index 2af3313f..f454a1dd 100644 --- a/ftpd/internal_test.go +++ b/ftpd/internal_test.go @@ -3,7 +3,9 @@ package ftpd import ( "crypto/tls" "crypto/x509" + "errors" "fmt" + "io/fs" "net" "os" "path/filepath" @@ -735,7 +737,7 @@ func TestAVBLErrors(t *testing.T) { assert.NoError(t, err) _, err = connection.GetAvailableSpace("/missing-path") assert.Error(t, err) - assert.True(t, os.IsNotExist(err)) + assert.True(t, errors.Is(err, fs.ErrNotExist)) } func TestUploadOverwriteErrors(t *testing.T) { diff --git a/httpd/api_http_user.go b/httpd/api_http_user.go index 09dacff7..a9d3cb02 100644 --- a/httpd/api_http_user.go +++ b/httpd/api_http_user.go @@ -290,7 +290,7 @@ func doUploadFiles(w http.ResponseWriter, r *http.Request, connection *Connectio } defer file.Close() - filePath := path.Join(parentDir, f.Filename) + filePath := path.Join(parentDir, path.Base(util.CleanPath(f.Filename))) writer, err := connection.getFileWriter(filePath) if err != nil { sendAPIResponse(w, r, err, fmt.Sprintf("Unable to write file %#v", f.Filename), getMappedStatusCode(err)) diff --git a/httpd/api_shares.go b/httpd/api_shares.go index 44d9e39a..8f648354 100644 --- a/httpd/api_shares.go +++ b/httpd/api_shares.go @@ -153,7 +153,8 @@ func deleteShare(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } @@ -183,7 +184,8 @@ func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http. func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } @@ -232,7 +234,8 @@ func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } @@ -264,7 +267,7 @@ func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) connection.Log(logger.LevelInfo, "denying share read due to quota limits") sendAPIResponse(w, r, err, "", getMappedStatusCode(err)) } - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"share-%v.zip\"", share.ShareID)) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"share-%v.zip\"", share.Name)) renderCompressedFiles(w, connection, "/", share.Paths, &share) return } @@ -287,12 +290,17 @@ func (s *httpdServer) uploadFileToShare(w http.ResponseWriter, r *http.Request) r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize) } name := getURLParam(r, "name") - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } - filePath := path.Join(share.Paths[0], name) - if path.Dir(filePath) != share.Paths[0] { + filePath := util.CleanPath(path.Join(share.Paths[0], name)) + expectedPrefix := share.Paths[0] + if !strings.HasSuffix(expectedPrefix, "/") { + expectedPrefix += "/" + } + if !strings.HasPrefix(filePath, expectedPrefix) { sendAPIResponse(w, r, err, "Uploading outside the share is not allowed", http.StatusForbidden) return } @@ -312,7 +320,8 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request) if maxUploadFileSize > 0 { r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize) } - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, false) if err != nil { return } @@ -361,7 +370,7 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request) } } -func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope, +func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, validScopes []dataprovider.ShareScope, isWebClient bool, ) (dataprovider.Share, *Connection, error) { renderError := func(err error, message string, statusCode int) { @@ -382,7 +391,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s renderError(err, "", statusCode) return share, nil, err } - if share.Scope != shareShope { + if !util.Contains(validScopes, share.Scope) { renderError(nil, "Invalid share scope", http.StatusForbidden) return share, nil, errors.New("invalid share scope") } @@ -406,16 +415,11 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s return share, nil, dataprovider.ErrInvalidCredentials } } - user, err := dataprovider.GetUserWithGroupSettings(share.Username) + user, err := getUserForShare(share) if err != nil { renderError(err, "", getRespStatus(err)) return share, nil, err } - if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) { - err := util.NewMethodDisabledError("two-factor authentication requirements not met") - renderError(err, "", getRespStatus(err)) - return share, nil, err - } connID := xid.New().String() connection := &Connection{ BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTPShare, util.GetHTTPLocalAddress(r), @@ -426,6 +430,23 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s return share, connection, nil } +func getUserForShare(share dataprovider.Share) (dataprovider.User, error) { + user, err := dataprovider.GetUserWithGroupSettings(share.Username) + if err != nil { + return user, err + } + if !user.CanManageShares() { + return user, util.NewRecordNotFoundError("this share does not exist") + } + if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) { + return user, fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission) + } + if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) { + return user, util.NewMethodDisabledError("two-factor authentication requirements not met") + } + return user, nil +} + func validateBrowsableShare(share dataprovider.Share, connection *Connection) error { if len(share.Paths) != 1 { return util.NewValidationError("a share with multiple paths is not browsable") diff --git a/httpd/api_utils.go b/httpd/api_utils.go index ef97a952..4e716056 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/fs" "mime" "net/http" "net/url" @@ -79,10 +80,10 @@ func getRespStatus(err error) int { if _, ok := err.(*util.RecordNotFoundError); ok { return http.StatusNotFound } - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return http.StatusBadRequest } - if os.IsPermission(err) || errors.Is(err, dataprovider.ErrLoginNotAllowedFromIP) { + if errors.Is(err, fs.ErrPermission) || errors.Is(err, dataprovider.ErrLoginNotAllowedFromIP) { return http.StatusForbidden } if errors.Is(err, plugin.ErrNoSearcher) || errors.Is(err, dataprovider.ErrNotImplemented) { @@ -241,7 +242,11 @@ func addZipEntry(wr *zip.Writer, conn *Connection, entryPath, baseDir string) er return err } if info.IsDir() { - _, err := wr.Create(getZipEntryName(entryPath, baseDir) + "/") + _, err := wr.CreateHeader(&zip.FileHeader{ + Name: getZipEntryName(entryPath, baseDir) + "/", + Method: zip.Deflate, + Modified: info.ModTime(), + }) if err != nil { conn.Log(logger.LevelDebug, "unable to create zip entry %#v: %v", entryPath, err) return err @@ -271,7 +276,11 @@ func addZipEntry(wr *zip.Writer, conn *Connection, entryPath, baseDir string) er } defer reader.Close() - f, err := wr.Create(getZipEntryName(entryPath, baseDir)) + f, err := wr.CreateHeader(&zip.FileHeader{ + Name: getZipEntryName(entryPath, baseDir), + Method: zip.Deflate, + Modified: info.ModTime(), + }) if err != nil { conn.Log(logger.LevelDebug, "unable to create zip entry %#v: %v", entryPath, err) return err diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 429a8b8b..885c4fef 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "math" "mime/multipart" "net" @@ -8561,7 +8562,7 @@ func TestStartQuotaScanMock(t *testing.T) { waitForUsersQuotaScan(t, token) _, err = os.Stat(user.HomeDir) - if err != nil && os.IsNotExist(err) { + if err != nil && errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(user.HomeDir, os.ModePerm) assert.NoError(t, err) } @@ -8725,7 +8726,7 @@ func TestStartFolderQuotaScanMock(t *testing.T) { assert.True(t, common.QuotaScans.RemoveVFolderQuotaScan(folderName)) // and now a real quota scan _, err = os.Stat(mappedPath) - if err != nil && os.IsNotExist(err) { + if err != nil && errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(mappedPath, os.ModePerm) assert.NoError(t, err) } @@ -10137,6 +10138,10 @@ func TestShareUsage(t *testing.T) { checkResponseCode(t, http.StatusForbidden, rr) assert.Contains(t, rr.Body.String(), "permission denied") + user.Permissions["/"] = []string{dataprovider.PermAny} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + body = new(bytes.Buffer) writer = multipart.NewWriter(body) part, err := writer.CreateFormFile("filename", "file1.txt") @@ -10155,7 +10160,37 @@ func TestShareUsage(t *testing.T) { checkResponseCode(t, http.StatusBadRequest, rr) assert.Contains(t, rr.Body.String(), "No files uploaded!") - share.Scope = dataprovider.ShareScopeRead + user.Filters.WebClient = []string{sdk.WebClientSharesDisabled} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + + req, err = http.NewRequest(http.MethodPost, sharesPath+"/"+objectID, reader) + assert.NoError(t, err) + req.Header.Add("Content-Type", writer.FormDataContentType()) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusNotFound, rr) + + user.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + share.Password = "" + err = dataprovider.UpdateShare(&share, user.Username, "") + assert.NoError(t, err) + + req, err = http.NewRequest(http.MethodPost, sharesPath+"/"+objectID, reader) + assert.NoError(t, err) + req.Header.Add("Content-Type", writer.FormDataContentType()) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + assert.Contains(t, rr.Body.String(), "sharing without a password was disabled") + + user.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled} + user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + + share.Scope = dataprovider.ShareScopeReadWrite share.Paths = []string{"/missing"} err = dataprovider.UpdateShare(&share, user.Username, "") assert.NoError(t, err) @@ -10347,12 +10382,6 @@ func TestShareUploadSingle(t *testing.T) { if assert.NoError(t, err) { assert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000)) } - // we don't allow to create the file in subdirectories - req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "%2Fdir%2Ffile1.txt"), bytes.NewBuffer(content)) - assert.NoError(t, err) - req.SetBasicAuth(defaultUsername, defaultPassword) - rr = executeRequest(req) - checkResponseCode(t, http.StatusForbidden, rr) req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "dir", "file.dat"), bytes.NewBuffer(content)) assert.NoError(t, err) @@ -10391,6 +10420,76 @@ func TestShareUploadSingle(t *testing.T) { checkResponseCode(t, http.StatusNotFound, rr) } +func TestShareReadWrite(t *testing.T) { + u := getTestUser() + u.Filters.StartDirectory = path.Join("/start", "dir") + user, _, err := httpdtest.AddUser(u, http.StatusCreated) + assert.NoError(t, err) + token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword) + assert.NoError(t, err) + testFileName := "test.txt" + + share := dataprovider.Share{ + Name: "test share rw", + Scope: dataprovider.ShareScopeReadWrite, + Paths: []string{user.Filters.StartDirectory}, + Password: defaultPassword, + MaxTokens: 0, + } + asJSON, err := json.Marshal(share) + assert.NoError(t, err) + req, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON)) + assert.NoError(t, err) + setBearerForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusCreated, rr) + objectID := rr.Header().Get("X-Object-ID") + assert.NotEmpty(t, objectID) + + content := []byte("shared rw content") + req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, testFileName), bytes.NewBuffer(content)) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusCreated, rr) + assert.FileExists(t, filepath.Join(user.GetHomeDir(), user.Filters.StartDirectory, testFileName)) + + req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path="+testFileName), nil) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + contentDisposition := rr.Header().Get("Content-Disposition") + assert.NotEmpty(t, contentDisposition) + + req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("../"+testFileName), + bytes.NewBuffer(content)) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + assert.Contains(t, rr.Body.String(), "Uploading outside the share is not allowed") + + req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("/../../"+testFileName), + bytes.NewBuffer(content)) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + assert.Contains(t, rr.Body.String(), "Uploading outside the share is not allowed") + + _, err = httpdtest.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) +} + func TestShareUncompressed(t *testing.T) { user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) @@ -10799,7 +10898,7 @@ func TestBrowseShares(t *testing.T) { req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "dirs"), nil) assert.NoError(t, err) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr) + checkResponseCode(t, http.StatusBadRequest, rr) assert.Contains(t, rr.Body.String(), "unable to check the share directory") // share multiple paths share = dataprovider.Share{ @@ -10868,6 +10967,32 @@ func TestBrowseShares(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) assert.Contains(t, rr.Body.String(), "two-factor authentication requirements not met") + user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH} + _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") + assert.NoError(t, err) + // share read/write + share.Scope = dataprovider.ShareScopeReadWrite + asJSON, err = json.Marshal(share) + assert.NoError(t, err) + req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON)) + assert.NoError(t, err) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusCreated, rr) + objectID = rr.Header().Get("X-Object-ID") + assert.NotEmpty(t, objectID) + req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil) + assert.NoError(t, err) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + // on upload we should be redirected + req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "upload"), nil) + assert.NoError(t, err) + req.SetBasicAuth(defaultUsername, defaultPassword) + rr = executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + location := rr.Header().Get("Location") + assert.Equal(t, path.Join(webClientPubSharesPath, objectID, "browse"), location) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -18853,7 +18978,7 @@ func checkResponseCode(t *testing.T, expected int, rr *httptest.ResponseRecorder func createTestFile(path string, size int64) error { baseDir := filepath.Dir(path) - if _, err := os.Stat(baseDir); os.IsNotExist(err) { + if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(baseDir, os.ModePerm) if err != nil { return err diff --git a/httpd/internal_test.go b/httpd/internal_test.go index e3c30ce3..d7e8b988 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -2291,7 +2291,13 @@ func TestMetadataAPI(t *testing.T) { func TestBrowsableSharePaths(t *testing.T) { share := dataprovider.Share{ - Paths: []string{"/"}, + Paths: []string{"/"}, + Username: defaultAdminUsername, + } + _, err := getUserForShare(share) + if assert.Error(t, err) { + _, ok := err.(*util.RecordNotFoundError) + assert.True(t, ok) } req, err := http.NewRequest(http.MethodGet, "/share", nil) require.NoError(t, err) diff --git a/httpd/webclient.go b/httpd/webclient.go index 2068314b..7e1835a2 100644 --- a/httpd/webclient.go +++ b/httpd/webclient.go @@ -143,12 +143,14 @@ type filesPage struct { type shareFilesPage struct { baseClientPage - CurrentDir string - DirsURL string - FilesURL string - DownloadURL string - Error string - Paths []dirMapping + CurrentDir string + DirsURL string + FilesURL string + DownloadURL string + UploadBaseURL string + Error string + Paths []dirMapping + Scope dataprovider.ShareScope } type shareUploadPage struct { @@ -512,8 +514,10 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque DirsURL: path.Join(webClientPubSharesPath, share.ShareID, "dirs"), FilesURL: currentURL, DownloadURL: path.Join(webClientPubSharesPath, share.ShareID), + UploadBaseURL: path.Join(webClientPubSharesPath, share.ShareID, url.PathEscape(dirName)), Error: error, Paths: getDirMapping(dirName, currentURL), + Scope: share.Scope, } renderClientTemplate(w, templateShareFiles, data) } @@ -625,7 +629,8 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http. func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, true) if err != nil { return } @@ -674,16 +679,22 @@ func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.R func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, _, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, true) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite} + share, _, err := s.checkPublicShare(w, r, validScopes, true) if err != nil { return } + if share.Scope == dataprovider.ShareScopeReadWrite { + http.Redirect(w, r, path.Join(webClientPubSharesPath, share.ShareID, "browse"), http.StatusFound) + return + } s.renderUploadToSharePage(w, r, share) } func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true) + validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} + share, connection, err := s.checkPublicShare(w, r, validScopes, true) if err != nil { return } diff --git a/logger/logger.go b/logger/logger.go index 38aa9d55..ac72d0cd 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -11,6 +11,7 @@ package logger import ( "errors" "fmt" + "io/fs" "os" "path/filepath" "time" @@ -57,7 +58,7 @@ func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge SetLogTime(logUTCTime) if isLogFilePathValid(logFilePath) { logDir := filepath.Dir(logFilePath) - if _, err := os.Stat(logDir); os.IsNotExist(err) { + if _, err := os.Stat(logDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(logDir, os.ModePerm) if err != nil { fmt.Printf("unable to create log dir %#v: %v", logDir, err) diff --git a/sftpd/server.go b/sftpd/server.go index d0ad805d..a93e8fe3 100644 --- a/sftpd/server.go +++ b/sftpd/server.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net" "os" "path" @@ -755,7 +756,7 @@ func (c *Configuration) generateDefaultHostKeys(configDir string) error { defaultHostKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName, defaultPrivateEd25519KeyName} for _, k := range defaultHostKeys { autoFile := filepath.Join(configDir, k) - if _, err = os.Stat(autoFile); os.IsNotExist(err) { + if _, err = os.Stat(autoFile); errors.Is(err, fs.ErrNotExist) { logger.Info(logSender, "", "No host keys configured and %#v does not exist; try to create a new host key", autoFile) logger.InfoToConsole("No host keys configured and %#v does not exist; try to create a new host key", autoFile) if k == defaultPrivateRSAKeyName { @@ -780,7 +781,7 @@ func (c *Configuration) generateDefaultHostKeys(configDir string) error { func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error { for _, k := range c.HostKeys { if filepath.IsAbs(k) { - if _, err := os.Stat(k); os.IsNotExist(err) { + if _, err := os.Stat(k); errors.Is(err, fs.ErrNotExist) { keyName := filepath.Base(k) switch keyName { case defaultPrivateRSAKeyName: diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 1050a1ce..f3d28346 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -10,9 +10,11 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" + "errors" "fmt" "hash" "io" + "io/fs" "math" "net" "net/http" @@ -1463,11 +1465,11 @@ func TestStat(t *testing.T) { assert.NoError(t, err) _, err = client.Stat(testFileName) assert.NoError(t, err) - // stat a missing path we should get an os.IsNotExist error + // stat a missing path we should get an fs.ErrNotExist error _, err = client.Stat("missing path") - assert.True(t, os.IsNotExist(err)) + assert.True(t, errors.Is(err, fs.ErrNotExist)) _, err = client.Lstat("missing path") - assert.True(t, os.IsNotExist(err)) + assert.True(t, errors.Is(err, fs.ErrNotExist)) // mode 0666 and 0444 works on Windows too newPerm := os.FileMode(0666) err = client.Chmod(testFileName, newPerm) @@ -6924,7 +6926,7 @@ func TestOpenError(t *testing.T) { err = os.Chmod(filepath.Join(user.GetHomeDir(), testDir), 0000) assert.NoError(t, err) err = client.Rename(testFileName, path.Join(testDir, testFileName)) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = os.Chmod(filepath.Join(user.GetHomeDir(), testDir), os.ModePerm) assert.NoError(t, err) err = os.Remove(localDownloadPath) @@ -7253,7 +7255,7 @@ func TestPermRename(t *testing.T) { err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = client.Rename(testFileName, testFileName+".rename") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) _, err = client.Stat(testFileName) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7289,7 +7291,7 @@ func TestPermRenameOverwrite(t *testing.T) { err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = client.Rename(testFileName, testFileName+".rename") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Remove(testFileName) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7474,13 +7476,13 @@ func TestSubDirsUploads(t *testing.T) { err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = sftpUploadFile(testFilePath, testFileNameSub, testFileSize, client) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink(testFileName, testFileNameSub+".link") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink(testFileName, testFileName+".link") assert.NoError(t, err) err = client.Rename(testFileName, testFileNameSub+".rename") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Rename(testFileName, testFileName+".rename") assert.NoError(t, err) err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) @@ -7500,7 +7502,7 @@ func TestSubDirsUploads(t *testing.T) { err = client.Remove(testDir) assert.NoError(t, err) err = client.Remove(path.Join("/subdir", "file.dat")) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Remove(testFileName + ".rename") assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7532,7 +7534,7 @@ func TestSubDirsOverwrite(t *testing.T) { err = createTestFile(testFileSFTPPath, 16384) assert.NoError(t, err) err = sftpUploadFile(testFilePath, testFileName+".new", testFileSize, client) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7566,17 +7568,17 @@ func TestSubDirsDownloads(t *testing.T) { assert.NoError(t, err) localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Chtimes(testFileName, time.Now(), time.Now()) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Rename(testFileName, testFileName+".rename") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink(testFileName, testFileName+".link") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Remove(testFileName) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = os.Remove(localDownloadPath) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7611,9 +7613,9 @@ func TestPermsSubDirsSetstat(t *testing.T) { err = sftpUploadFile(testFilePath, testFileName, testFileSize, client) assert.NoError(t, err) err = client.Chtimes("/subdir/", time.Now(), time.Now()) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Chtimes("subdir/", time.Now(), time.Now()) - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Chtimes(testFileName, time.Now(), time.Now()) assert.NoError(t, err) err = os.Remove(testFilePath) @@ -7674,19 +7676,19 @@ func TestPermsSubDirsCommands(t *testing.T) { _, err = client.ReadDir("/") assert.NoError(t, err) _, err = client.ReadDir("/subdir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.RemoveDirectory("/subdir/dir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Mkdir("/subdir/otherdir/dir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Mkdir("/otherdir") assert.NoError(t, err) err = client.Mkdir("/subdir/otherdir") assert.NoError(t, err) err = client.Rename("/otherdir", "/subdir/otherdir/adir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink("/otherdir", "/subdir/otherdir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink("/otherdir", "/otherdir_link") assert.NoError(t, err) err = client.Rename("/otherdir", "/otherdir1") @@ -7718,11 +7720,11 @@ func TestRootDirCommands(t *testing.T) { defer conn.Close() defer client.Close() err = client.Rename("/", "rootdir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.Symlink("/", "rootdir") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) err = client.RemoveDirectory("/") - assert.True(t, os.IsPermission(err)) + assert.True(t, errors.Is(err, fs.ErrPermission)) } if user.Username == defaultUsername { err = os.RemoveAll(user.GetHomeDir()) @@ -8200,7 +8202,7 @@ func TestStatVFS(t *testing.T) { _, err = client.StatVFS("missing-path") assert.Error(t, err) - assert.True(t, os.IsNotExist(err)) + assert.True(t, errors.Is(err, fs.ErrNotExist)) } user.QuotaFiles = 100 user.Filters.DisableFsChecks = true @@ -10524,7 +10526,7 @@ func getCustomAuthSftpClient(user dataprovider.User, authMethods []ssh.AuthMetho func createTestFile(path string, size int64) error { baseDir := filepath.Dir(path) - if _, err := os.Stat(baseDir); os.IsNotExist(err) { + if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(baseDir, os.ModePerm) if err != nil { return err diff --git a/templates/webadmin/mfa.html b/templates/webadmin/mfa.html index b0106ed5..ebc0444e 100644 --- a/templates/webadmin/mfa.html +++ b/templates/webadmin/mfa.html @@ -2,6 +2,10 @@ {{define "title"}}{{.Title}}{{end}} +{{define "extra_css"}} + +{{end}} + {{define "page_body"}}
@@ -26,7 +30,7 @@
- {{range .TOTPConfigs}} @@ -127,6 +131,7 @@ {{end}} {{define "extra_js"}} + + @@ -171,6 +202,94 @@ } $(document).ready(function () { + $('#spinnerModal').on('shown.bs.modal', function () { + if (spinnerDone){ + $('#spinnerModal').modal('hide'); + } + }); + + $("#upload_files_form").submit(function (event){ + event.preventDefault(); + var files = $("#files_name")[0].files; + var has_errors = false; + var index = 0; + var success = 0; + spinnerDone = false; + + $('#uploadFilesModal').modal('hide'); + $('#spinnerModal').modal('show'); + + function uploadFile() { + if (index >= files.length || has_errors){ + $('#spinnerModal').modal('hide'); + spinnerDone = true; + if (!has_errors){ + location.reload(); + } + return; + } + + async function saveFile() { + var errorMessage = "Error uploading files"; + let response; + try { + var f = files[index]; + var uploadPath = '{{.UploadBaseURL}}'+fixedEncodeURIComponent("/"+f.name); + var lastModified; + try { + lastModified = f.lastModified; + } catch (e) { + console.log("unable to get last modified time from file: "+e.message); + lastModified = ""; + } + response = await fetch(uploadPath, { + method: 'POST', + headers: { + 'X-SFTPGO-MTIME': lastModified + }, + credentials: 'same-origin', + redirect: 'error', + body: f + }); + } catch (e){ + throw Error(errorMessage+": " +e.message); + } + if (response.status == 201){ + index++; + success++; + uploadFile(); + } else { + let jsonResponse; + try { + jsonResponse = await response.json(); + } catch(e){ + throw Error(errorMessage); + } + if (jsonResponse.message) { + errorMessage = jsonResponse.message; + } + if (jsonResponse.error) { + errorMessage += ": " + jsonResponse.error; + } + throw Error(errorMessage); + } + } + + saveFile().catch(function(error){ + index++; + has_errors = true; + $('#errorTxt').text(error.message); + $('#errorMsg').show(); + setTimeout(function () { + $('#errorMsg').hide(); + }, 10000); + uploadFile(); + }); + } + + uploadFile(); + }); + $.fn.dataTable.ext.buttons.refresh = { text: '', name: 'refresh', @@ -191,6 +310,17 @@ } }; + $.fn.dataTable.ext.buttons.addFiles = { + text: '', + name: 'addFiles', + titleAttr: "Upload files", + action: function (e, dt, node, config) { + document.getElementById("files_name").value = null; + $('#uploadFilesModal').modal('show'); + }, + enabled: true + }; + var table = $('#dataTable').DataTable({ "ajax": { "url": "{{.DirsURL}}?path={{.CurrentDir}}", @@ -283,6 +413,9 @@ table.button().add(0, 'refresh'); table.button().add(0, 'pageLength'); table.button().add(0, 'download'); + {{if gt .Scope 1}} + table.button().add(0, 'addFiles'); + {{end}} table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container()); }, "orderFixed": [0, 'asc'], diff --git a/util/util.go b/util/util.go index 1357fbf0..a5e7831f 100644 --- a/util/util.go +++ b/util/util.go @@ -15,6 +15,7 @@ import ( "fmt" "html/template" "io" + "io/fs" "net" "net/http" "net/url" @@ -45,7 +46,18 @@ var ( trueClientIP = http.CanonicalHeaderKey("True-Client-IP") ) +// Contains reports whether v is present in elems. +func Contains[T comparable](elems []T, v T) bool { + for _, s := range elems { + if v == s { + return true + } + } + return false +} + // IsStringInSlice searches a string in a slice and returns true if the string is found +// TODO: replace with Contains above func IsStringInSlice(obj string, list []string) bool { for i := 0; i < len(list); i++ { if list[i] == obj { @@ -390,7 +402,7 @@ func CleanDirInput(dirInput string) string { func createDirPathIfMissing(file string, perm os.FileMode) error { dirPath := filepath.Dir(file) - if _, err := os.Stat(dirPath); os.IsNotExist(err) { + if _, err := os.Stat(dirPath); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(dirPath, perm) if err != nil { return err diff --git a/vfs/osfs.go b/vfs/osfs.go index 6266c425..ea2541ad 100644 --- a/vfs/osfs.go +++ b/vfs/osfs.go @@ -1,8 +1,10 @@ package vfs import ( + "errors" "fmt" "io" + "io/fs" "net/http" "os" "path" @@ -200,7 +202,7 @@ func (*OsFs) IsAtomicUploadSupported() bool { // IsNotExist returns a boolean indicating whether the error is known to // report that a file or directory does not exist func (*OsFs) IsNotExist(err error) bool { - return os.IsNotExist(err) + return errors.Is(err, fs.ErrNotExist) } // IsPermission returns a boolean indicating whether the error is known to @@ -209,8 +211,7 @@ func (*OsFs) IsPermission(err error) bool { if _, ok := err.(*pathResolutionError); ok { return true } - - return os.IsPermission(err) + return errors.Is(err, fs.ErrPermission) } // IsNotSupported returns true if the error indicate an unsupported operation @@ -297,9 +298,10 @@ func (fs *OsFs) ResolvePath(virtualPath string) (string, error) { if isInvalidNameError(err) { err = os.ErrNotExist } - if err != nil && !os.IsNotExist(err) { + isNotExist := fs.IsNotExist(err) + if err != nil && !isNotExist { return "", err - } else if os.IsNotExist(err) { + } else if isNotExist { // The requested path doesn't exist, so at this point we need to iterate up the // path chain until we hit a directory that _does_ exist and can be validated. _, err = fs.findFirstExistingDir(r) @@ -349,7 +351,7 @@ func (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) { parent := filepath.Dir(cleanPath) _, err := os.Stat(parent) - for os.IsNotExist(err) { + for fs.IsNotExist(err) { results = append(results, parent) parent = filepath.Dir(parent) _, err = os.Stat(parent) diff --git a/vfs/sftpfs.go b/vfs/sftpfs.go index 3b90b971..fe78f348 100644 --- a/vfs/sftpfs.go +++ b/vfs/sftpfs.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net" "net/http" "os" @@ -431,7 +432,7 @@ func (fs *SFTPFs) IsAtomicUploadSupported() bool { // IsNotExist returns a boolean indicating whether the error is known to // report that a file or directory does not exist func (*SFTPFs) IsNotExist(err error) bool { - return os.IsNotExist(err) + return errors.Is(err, fs.ErrNotExist) } // IsPermission returns a boolean indicating whether the error is known to @@ -440,7 +441,7 @@ func (*SFTPFs) IsPermission(err error) bool { if _, ok := err.(*pathResolutionError); ok { return true } - return os.IsPermission(err) + return errors.Is(err, fs.ErrPermission) } // IsNotSupported returns true if the error indicate an unsupported operation @@ -559,12 +560,13 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) { var validatedPath string var err error validatedPath, err = fs.getRealPath(fsPath) - if err != nil && !os.IsNotExist(err) { + isNotExist := fs.IsNotExist(err) + if err != nil && !isNotExist { fsLog(fs, logger.LevelError, "Invalid path resolution, original path %v resolved %#v err: %v", virtualPath, fsPath, err) return "", err - } else if os.IsNotExist(err) { - for os.IsNotExist(err) { + } else if isNotExist { + for fs.IsNotExist(err) { validatedPath = path.Dir(validatedPath) if validatedPath == "/" { err = nil diff --git a/webdavd/internal_test.go b/webdavd/internal_test.go index 3fd11507..b01c559b 100644 --- a/webdavd/internal_test.go +++ b/webdavd/internal_test.go @@ -644,13 +644,13 @@ func TestRemoveDirTree(t *testing.T) { p := filepath.Join(user.HomeDir, "adir", "missing") err := connection.removeDirTree(fs, p, vpath) if assert.Error(t, err) { - assert.True(t, os.IsNotExist(err)) + assert.True(t, fs.IsNotExist(err)) } fs = newMockOsFs(nil, false, "mockID", user.HomeDir, nil) err = connection.removeDirTree(fs, p, vpath) if assert.Error(t, err) { - assert.True(t, os.IsNotExist(err), "unexpected error: %v", err) + assert.True(t, fs.IsNotExist(err), "unexpected error: %v", err) } errFake := errors.New("fake err") @@ -663,7 +663,7 @@ func TestRemoveDirTree(t *testing.T) { fs = newMockOsFs(errWalkDir, true, "mockID", user.HomeDir, nil) err = connection.removeDirTree(fs, p, vpath) if assert.Error(t, err) { - assert.True(t, os.IsPermission(err), "unexpected error: %v", err) + assert.True(t, fs.IsPermission(err), "unexpected error: %v", err) } fs = newMockOsFs(errWalkFile, false, "mockID", user.HomeDir, nil) @@ -766,9 +766,9 @@ func TestTransferReadWriteErrors(t *testing.T) { common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{}) davFile = newWebDavFile(baseTransfer, nil, nil) _, err = davFile.Read(p) - assert.True(t, os.IsNotExist(err)) + assert.True(t, fs.IsNotExist(err)) _, err = davFile.Stat() - assert.True(t, os.IsNotExist(err)) + assert.True(t, fs.IsNotExist(err)) baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile, common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{}) @@ -852,7 +852,7 @@ func TestTransferSeek(t *testing.T) { common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100}) davFile = newWebDavFile(baseTransfer, nil, nil) _, err = davFile.Seek(0, io.SeekCurrent) - assert.True(t, os.IsNotExist(err)) + assert.True(t, fs.IsNotExist(err)) davFile.Connection.RemoveTransfer(davFile.BaseTransfer) err = os.WriteFile(testFilePath, testFileContents, os.ModePerm) @@ -888,7 +888,7 @@ func TestTransferSeek(t *testing.T) { common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100}) davFile = newWebDavFile(baseTransfer, nil, nil) _, err = davFile.Seek(0, io.SeekEnd) - assert.True(t, os.IsNotExist(err)) + assert.True(t, fs.IsNotExist(err)) davFile.Connection.RemoveTransfer(davFile.BaseTransfer) baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile, @@ -912,7 +912,7 @@ func TestTransferSeek(t *testing.T) { davFile = newWebDavFile(baseTransfer, nil, nil) davFile.Fs = newMockOsFs(nil, true, fs.ConnectionID(), user.GetHomeDir(), nil) res, err = davFile.Seek(2, io.SeekEnd) - assert.True(t, os.IsNotExist(err)) + assert.True(t, fs.IsNotExist(err)) assert.Equal(t, int64(0), res) assert.Len(t, common.Connections.GetStats(), 0) diff --git a/webdavd/webdavd_test.go b/webdavd/webdavd_test.go index ffcc88f0..9c273fe3 100644 --- a/webdavd/webdavd_test.go +++ b/webdavd/webdavd_test.go @@ -6,8 +6,10 @@ import ( "crypto/rand" "crypto/tls" "encoding/json" + "errors" "fmt" "io" + "io/fs" "net" "net/http" "os" @@ -2916,7 +2918,7 @@ func getExitCodeScriptContent(exitCode int) []byte { func createTestFile(path string, size int64) error { baseDir := filepath.Dir(path) - if _, err := os.Stat(baseDir); os.IsNotExist(err) { + if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(baseDir, os.ModePerm) if err != nil { return err