web client: add share mode read/write

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-05-09 19:09:43 +02:00
parent e72bb1e124
commit 1e0b3a2a8c
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
22 changed files with 457 additions and 108 deletions

View file

@ -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)

View file

@ -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 {

View file

@ -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

View file

@ -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) {

View file

@ -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))

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -2,6 +2,10 @@
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
@ -26,7 +30,7 @@
<div class="form-group row">
<label for="idConfig" class="col-sm-2 col-form-label">Configuration</label>
<div class="col-sm-10">
<select class="form-control" id="idConfig" name="config_name">
<select class="form-control selectpicker" id="idConfig" name="config_name">
<option value="">None</option>
{{range .TOTPConfigs}}
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
@ -127,6 +131,7 @@
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
function totpGenerate() {

View file

@ -2,6 +2,10 @@
{{define "title"}}{{.Title}}{{end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
@ -33,7 +37,7 @@
<div class="form-group row totpProtocols">
<label for="idProtocols" class="col-sm-3 col-form-label">Require two-factor auth for</label>
<div class="col-sm-9">
<select class="form-control" id="idProtocols" name="multi_factor_protocols" multiple>
<select class="form-control selectpicker" id="idProtocols" name="multi_factor_protocols" multiple>
{{range $protocol := .Protocols}}
<option value="{{$protocol}}" {{range $p :=$.TOTPConfig.Protocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
</option>
@ -51,7 +55,7 @@
<div class="form-group row">
<label for="idConfig" class="col-sm-3 col-form-label">Configuration</label>
<div class="col-sm-9">
<select class="form-control" id="idConfig" name="config_name">
<select class="form-control selectpicker" id="idConfig" name="config_name">
<option value="">None</option>
{{range .TOTPConfigs}}
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
@ -152,6 +156,7 @@
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
function totpGenerate() {

View file

@ -4,6 +4,7 @@
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/tempusdominus/css/tempusdominus-bootstrap-4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
@ -29,12 +30,13 @@
<div class="form-group row">
<label for="idScope" class="col-sm-2 col-form-label">Scope</label>
<div class="col-sm-10">
<select class="form-control" id="idScope" name="scope" aria-describedby="scopeHelpBlock">
<select class="form-control selectpicker" id="idScope" name="scope" aria-describedby="scopeHelpBlock">
<option value="1" {{if eq .Share.Scope 1 }}selected{{end}}>Read</option>
<option value="2" {{if eq .Share.Scope 2 }}selected{{end}}>Write</option>
<option value="3" {{if eq .Share.Scope 3 }}selected{{end}}>Read/Write</option>
</select>
<small id="scopeHelpBlock" class="form-text text-muted">
For scope "Write" you have to define one path and it must be a directory
For scope "Write" and "Read&Write" you have to define one path and it must be a directory
</small>
</div>
</div>
@ -144,6 +146,7 @@
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/moment/js/moment.min.js"></script>
<script src="{{.StaticURL}}/vendor/tempusdominus/js/tempusdominus-bootstrap-4.min.js"></script>
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {

View file

@ -38,6 +38,37 @@
</div>
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="uploadFilesModal" tabindex="-1" role="dialog" aria-labelledby="uploadFilesModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="uploadFilesModalLabel">
Upload one or more files
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="upload_files_form" action="{{.FilesURL}}?path={{.CurrentDir}}" method="POST" enctype="multipart/form-data">
<div class="modal-body">
<input type="file" class="form-control-file" id="files_name" name="filenames" required multiple>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="spinnerModal" tabindex="-1" role="dialog" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog modal-dialog-centered justify-content-center" role="document">
<span style="color: #333333;" class="fa fa-spinner fa-spin fa-3x"></span>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
@ -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: '<i class="fas fa-sync-alt"></i>',
name: 'refresh',
@ -191,6 +310,17 @@
}
};
$.fn.dataTable.ext.buttons.addFiles = {
text: '<i class="fas fa-file-upload"></i>',
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'],

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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