diff --git a/dataprovider/user.go b/dataprovider/user.go index a2e5985f..3b9a3529 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -697,11 +697,53 @@ func (u *User) isFilePatternAllowed(virtualPath string) bool { } // CanManagePublicKeys return true if this user is allowed to manage public keys -// from the web client +// from the web client. Used in web client UI func (u *User) CanManagePublicKeys() bool { return !util.IsStringInSlice(sdk.WebClientPubKeyChangeDisabled, u.Filters.WebClient) } +// CanAddFilesFromWeb returns true if the client can add files from the web UI. +// The specified target is the directory where the files must be uploaded +func (u *User) CanAddFilesFromWeb(target string) bool { + if util.IsStringInSlice(sdk.WebClientWriteDisabled, u.Filters.WebClient) { + return false + } + return u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target) +} + +// CanAddDirsFromWeb returns true if the client can add directories from the web UI. +// The specified target is the directory where the new directory must be created +func (u *User) CanAddDirsFromWeb(target string) bool { + if util.IsStringInSlice(sdk.WebClientWriteDisabled, u.Filters.WebClient) { + return false + } + return u.HasPerm(PermCreateDirs, target) +} + +// CanRenameFromWeb returns true if the client can rename objects from the web UI. +// The specified src and dest are the source and target directories for the rename. +func (u *User) CanRenameFromWeb(src, dest string) bool { + if util.IsStringInSlice(sdk.WebClientWriteDisabled, u.Filters.WebClient) { + return false + } + if u.HasPerm(PermRename, src) && u.HasPerm(PermRename, dest) { + return true + } + if !u.HasPerm(PermDelete, src) { + return false + } + return u.HasPerm(PermUpload, dest) || u.HasPerm(PermCreateDirs, dest) +} + +// CanDeleteFromWeb returns true if the client can delete objects from the web UI. +// The specified target is the parent directory for the object to delete +func (u *User) CanDeleteFromWeb(target string) bool { + if util.IsStringInSlice(sdk.WebClientWriteDisabled, u.Filters.WebClient) { + return false + } + return u.HasPerm(PermDelete, target) +} + // GetSignature returns a signature for this admin. // It could change after an update func (u *User) GetSignature() string { diff --git a/httpd/httpd.go b/httpd/httpd.go index f77e990b..a31a1499 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -57,7 +57,9 @@ const ( userPwdPath = "/api/v2/user/changepwd" userPublicKeysPath = "/api/v2/user/publickeys" userFolderPath = "/api/v2/user/folder" + userDirsPath = "/api/v2/user/dirs" userFilePath = "/api/v2/user/file" + userFilesPath = "/api/v2/user/files" userStreamZipPath = "/api/v2/user/streamzip" healthzPath = "/healthz" webRootPathDefault = "/" @@ -87,7 +89,7 @@ const ( webDefenderHostsPathDefault = "/web/admin/defender/hosts" webClientLoginPathDefault = "/web/client/login" webClientFilesPathDefault = "/web/client/files" - webClientDirContentsPathDefault = "/web/client/listdir" + webClientDirsPathDefault = "/web/client/dirs" webClientDownloadZipPathDefault = "/web/client/downloadzip" webClientCredentialsPathDefault = "/web/client/credentials" webChangeClientPwdPathDefault = "/web/client/changepwd" @@ -136,7 +138,7 @@ var ( webDefenderHostsPath string webClientLoginPath string webClientFilesPath string - webClientDirContentsPath string + webClientDirsPath string webClientDownloadZipPath string webClientCredentialsPath string webChangeClientPwdPath string @@ -444,7 +446,7 @@ func updateWebClientURLs(baseURL string) { webBaseClientPath = path.Join(baseURL, webBasePathClientDefault) webClientLoginPath = path.Join(baseURL, webClientLoginPathDefault) webClientFilesPath = path.Join(baseURL, webClientFilesPathDefault) - webClientDirContentsPath = path.Join(baseURL, webClientDirContentsPathDefault) + webClientDirsPath = path.Join(baseURL, webClientDirsPathDefault) webClientDownloadZipPath = path.Join(baseURL, webClientDownloadZipPathDefault) webClientCredentialsPath = path.Join(baseURL, webClientCredentialsPathDefault) webChangeClientPwdPath = path.Join(baseURL, webChangeClientPwdPathDefault) diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 79773637..c79915d8 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -78,8 +78,8 @@ const ( logoutPath = "/api/v2/logout" userPwdPath = "/api/v2/user/changepwd" userPublicKeysPath = "/api/v2/user/publickeys" - userFolderPath = "/api/v2/user/folder" - userFilePath = "/api/v2/user/file" + userDirsPath = "/api/v2/user/dirs" + userFilesPath = "/api/v2/user/files" userStreamZipPath = "/api/v2/user/streamzip" healthzPath = "/healthz" webBasePath = "/web" @@ -104,7 +104,7 @@ const ( webBasePathClient = "/web/client" webClientLoginPath = "/web/client/login" webClientFilesPath = "/web/client/files" - webClientDirContentsPath = "/web/client/listdir" + webClientDirsPath = "/web/client/dirs" webClientDownloadZipPath = "/web/client/downloadzip" webClientCredentialsPath = "/web/client/credentials" webChangeClientPwdPath = "/web/client/changepwd" @@ -4882,14 +4882,14 @@ func TestWebAPILoginMock(t *testing.T) { webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword) assert.NoError(t, err) // a web token is not valid for API usage - req, err := http.NewRequest(http.MethodGet, userFolderPath, nil) + req, err := http.NewRequest(http.MethodGet, userDirsPath, nil) assert.NoError(t, err) setBearerForReq(req, webToken) rr := executeRequest(req) checkResponseCode(t, http.StatusUnauthorized, rr) assert.Contains(t, rr.Body.String(), "Your token audience is not valid") - req, err = http.NewRequest(http.MethodGet, userFolderPath+"/?path=%2F", nil) + req, err = http.NewRequest(http.MethodGet, userDirsPath+"/?path=%2F", nil) assert.NoError(t, err) setBearerForReq(req, apiToken) rr = executeRequest(req) @@ -4977,7 +4977,7 @@ func TestWebClientLoginMock(t *testing.T) { checkResponseCode(t, http.StatusNotFound, rr) assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") - req, _ = http.NewRequest(http.MethodGet, webClientDirContentsPath, nil) + req, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) @@ -4989,13 +4989,13 @@ func TestWebClientLoginMock(t *testing.T) { checkResponseCode(t, http.StatusNotFound, rr) assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") - req, _ = http.NewRequest(http.MethodGet, userFolderPath, nil) + req, _ = http.NewRequest(http.MethodGet, userDirsPath, nil) setBearerForReq(req, apiUserToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") - req, _ = http.NewRequest(http.MethodGet, userFilePath, nil) + req, _ = http.NewRequest(http.MethodGet, userFilesPath, nil) setBearerForReq(req, apiUserToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) @@ -5468,7 +5468,7 @@ func TestPreDownloadHook(t *testing.T) { checkResponseCode(t, http.StatusOK, rr) assert.Equal(t, testFileContents, rr.Body.Bytes()) - req, err = http.NewRequest(http.MethodGet, userFilePath+"?path="+testFileName, nil) + req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5484,7 +5484,7 @@ func TestPreDownloadHook(t *testing.T) { checkResponseCode(t, http.StatusOK, rr) assert.Contains(t, rr.Body.String(), "permission denied") - req, err = http.NewRequest(http.MethodGet, userFilePath+"?path="+testFileName, nil) + req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5530,7 +5530,7 @@ func TestPreUploadHook(t *testing.T) { reader := bytes.NewReader(body.Bytes()) _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err := http.NewRequest(http.MethodPost, userFilePath, reader) + req, err := http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -5541,7 +5541,7 @@ func TestPreUploadHook(t *testing.T) { assert.NoError(t, err) _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -5586,7 +5586,7 @@ func TestWebGetFiles(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - req, _ = http.NewRequest(http.MethodGet, webClientDirContentsPath+"?path="+testDir, nil) + req, _ = http.NewRequest(http.MethodGet, webClientDirsPath+"?path="+testDir, nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) @@ -5595,7 +5595,7 @@ func TestWebGetFiles(t *testing.T) { assert.NoError(t, err) assert.Len(t, dirContents, 1) - req, _ = http.NewRequest(http.MethodGet, userFolderPath+"?path="+testDir, nil) + req, _ = http.NewRequest(http.MethodGet, userDirsPath+"?path="+testDir, nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) @@ -5636,7 +5636,7 @@ func TestWebGetFiles(t *testing.T) { checkResponseCode(t, http.StatusInternalServerError, rr) assert.Contains(t, rr.Body.String(), "Unable to get files list") - req, _ = http.NewRequest(http.MethodGet, webClientDirContentsPath+"?path=/", nil) + req, _ = http.NewRequest(http.MethodGet, webClientDirsPath+"?path=/", nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) @@ -5645,7 +5645,7 @@ func TestWebGetFiles(t *testing.T) { assert.NoError(t, err) assert.Len(t, dirContents, len(extensions)+1) - req, _ = http.NewRequest(http.MethodGet, userFolderPath+"?path=/", nil) + req, _ = http.NewRequest(http.MethodGet, userDirsPath+"?path=/", nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) @@ -5654,13 +5654,13 @@ func TestWebGetFiles(t *testing.T) { assert.NoError(t, err) assert.Len(t, dirEntries, len(extensions)+1) - req, _ = http.NewRequest(http.MethodGet, webClientDirContentsPath+"?path=/missing", nil) + req, _ = http.NewRequest(http.MethodGet, webClientDirsPath+"?path=/missing", nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) assert.Contains(t, rr.Body.String(), "Unable to get directory contents") - req, _ = http.NewRequest(http.MethodGet, userFolderPath+"?path=missing", nil) + req, _ = http.NewRequest(http.MethodGet, userDirsPath+"?path=missing", nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) @@ -5672,25 +5672,25 @@ func TestWebGetFiles(t *testing.T) { checkResponseCode(t, http.StatusOK, rr) assert.Equal(t, testFileContents, rr.Body.Bytes()) - req, _ = http.NewRequest(http.MethodGet, userFilePath+"?path="+testFileName, nil) + req, _ = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) assert.Equal(t, testFileContents, rr.Body.Bytes()) - req, _ = http.NewRequest(http.MethodGet, userFilePath+"?path=", nil) + req, _ = http.NewRequest(http.MethodGet, userFilesPath+"?path=", nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) assert.Contains(t, rr.Body.String(), "Please set the path to a valid file") - req, _ = http.NewRequest(http.MethodGet, userFilePath+"?path="+testDir, nil) + req, _ = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testDir, nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) assert.Contains(t, rr.Body.String(), "is a directory") - req, _ = http.NewRequest(http.MethodGet, userFilePath+"?path=notafile", nil) + req, _ = http.NewRequest(http.MethodGet, userFilesPath+"?path=notafile", nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) @@ -5703,7 +5703,7 @@ func TestWebGetFiles(t *testing.T) { checkResponseCode(t, http.StatusPartialContent, rr) assert.Equal(t, testFileContents[2:], rr.Body.Bytes()) - req, _ = http.NewRequest(http.MethodGet, userFilePath+"?path="+testFileName, nil) + req, _ = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil) req.Header.Set("Range", "bytes=2-") setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5729,7 +5729,7 @@ func TestWebGetFiles(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusRequestedRangeNotSatisfiable, rr) - req, _ = http.NewRequest(http.MethodGet, userFilePath+"?path="+testFileName, nil) + req, _ = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil) req.Header.Set("Range", "bytes=2b-") setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5767,7 +5767,7 @@ func TestWebGetFiles(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusPreconditionFailed, rr) - req, _ = http.NewRequest(http.MethodHead, userFilePath+"?path="+testFileName, nil) + req, _ = http.NewRequest(http.MethodHead, userFilesPath+"?path="+testFileName, nil) req.Header.Set("If-Unmodified-Since", time.Now().UTC().Add(-120*time.Second).Format(http.TimeFormat)) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5788,17 +5788,17 @@ func TestWebGetFiles(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, _ = http.NewRequest(http.MethodGet, webClientDirContentsPath+"?path=/", nil) + req, _ = http.NewRequest(http.MethodGet, webClientDirsPath+"?path=/", nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, _ = http.NewRequest(http.MethodGet, userFilePath+"?path="+testFileName, nil) + req, _ = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, _ = http.NewRequest(http.MethodGet, userFolderPath+"?path="+testDir, nil) + req, _ = http.NewRequest(http.MethodGet, userDirsPath+"?path="+testDir, nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) @@ -5826,7 +5826,7 @@ func TestWebGetFiles(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, _ = http.NewRequest(http.MethodGet, userFolderPath+"?path="+testDir, nil) + req, _ = http.NewRequest(http.MethodGet, userDirsPath+"?path="+testDir, nil) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) @@ -5844,7 +5844,7 @@ func TestWebDirsAPI(t *testing.T) { assert.NoError(t, err) testDir := "testdir" - req, err := http.NewRequest(http.MethodGet, userFolderPath, nil) + req, err := http.NewRequest(http.MethodGet, userDirsPath, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr := executeRequest(req) @@ -5855,25 +5855,25 @@ func TestWebDirsAPI(t *testing.T) { assert.Len(t, contents, 0) // rename a missing folder - req, err = http.NewRequest(http.MethodPatch, userFolderPath+"?path="+testDir+"&target="+testDir+"new", nil) + req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) // delete a missing folder - req, err = http.NewRequest(http.MethodDelete, userFolderPath+"?path="+testDir, nil) + req, err = http.NewRequest(http.MethodDelete, userDirsPath+"?path="+testDir, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) // create a dir - req, err = http.NewRequest(http.MethodPost, userFolderPath+"?path="+testDir, nil) + req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path="+testDir, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusCreated, rr) // check the dir was created - req, err = http.NewRequest(http.MethodGet, userFolderPath, nil) + req, err = http.NewRequest(http.MethodGet, userDirsPath, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5885,19 +5885,19 @@ func TestWebDirsAPI(t *testing.T) { assert.Equal(t, testDir, contents[0]["name"]) } // rename the dir - req, err = http.NewRequest(http.MethodPatch, userFolderPath+"?path="+testDir+"&target="+testDir+"new", nil) + req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) // delete the dir - req, err = http.NewRequest(http.MethodDelete, userFolderPath+"?path="+testDir+"new", nil) + req, err = http.NewRequest(http.MethodDelete, userDirsPath+"?path="+testDir+"new", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) // the root dir cannot be created - req, err = http.NewRequest(http.MethodPost, userFolderPath, nil) + req, err = http.NewRequest(http.MethodPost, userDirsPath, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5907,7 +5907,7 @@ func TestWebDirsAPI(t *testing.T) { user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) // the user has no more the permission to create the directory - req, err = http.NewRequest(http.MethodPost, userFolderPath+"?path="+testDir, nil) + req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path="+testDir, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5919,19 +5919,19 @@ func TestWebDirsAPI(t *testing.T) { assert.NoError(t, err) // the user is deleted, any API call should fail - req, err = http.NewRequest(http.MethodPost, userFolderPath+"?path="+testDir, nil) + req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path="+testDir, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - req, err = http.NewRequest(http.MethodPatch, userFolderPath+"?path="+testDir+"&target="+testDir+"new", nil) + req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - req, err = http.NewRequest(http.MethodDelete, userFolderPath+"?path="+testDir+"new", nil) + req, err = http.NewRequest(http.MethodDelete, userDirsPath+"?path="+testDir+"new", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5958,7 +5958,7 @@ func TestWebFilesAPI(t *testing.T) { assert.NoError(t, err) reader := bytes.NewReader(body.Bytes()) - req, err := http.NewRequest(http.MethodPost, userFilePath, reader) + req, err := http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr := executeRequest(req) @@ -5967,14 +5967,14 @@ func TestWebFilesAPI(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) // set the proper content type - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusCreated, rr) // check we have 2 files - req, err = http.NewRequest(http.MethodGet, userFolderPath, nil) + req, err = http.NewRequest(http.MethodGet, userDirsPath, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -5986,13 +5986,13 @@ func TestWebFilesAPI(t *testing.T) { // overwrite the existing files _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusCreated, rr) - req, err = http.NewRequest(http.MethodGet, userFolderPath, nil) + req, err = http.NewRequest(http.MethodGet, userDirsPath, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6003,20 +6003,20 @@ func TestWebFilesAPI(t *testing.T) { assert.Len(t, contents, 2) // now create a dir and upload to that dir testDir := "tdir" - req, err = http.NewRequest(http.MethodPost, userFolderPath+"?path="+testDir, nil) + req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path="+testDir, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusCreated, rr) _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath+"?path="+testDir, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath+"?path="+testDir, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusCreated, rr) - req, err = http.NewRequest(http.MethodGet, userFolderPath, nil) + req, err = http.NewRequest(http.MethodGet, userDirsPath, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6025,7 +6025,7 @@ func TestWebFilesAPI(t *testing.T) { err = json.NewDecoder(rr.Body).Decode(&contents) assert.NoError(t, err) assert.Len(t, contents, 3) - req, err = http.NewRequest(http.MethodGet, userFolderPath+"?path="+testDir, nil) + req, err = http.NewRequest(http.MethodGet, userDirsPath+"?path="+testDir, nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6035,31 +6035,31 @@ func TestWebFilesAPI(t *testing.T) { assert.NoError(t, err) assert.Len(t, contents, 2) // rename a file - req, err = http.NewRequest(http.MethodPatch, userFilePath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil) + req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) // rename a missing file - req, err = http.NewRequest(http.MethodPatch, userFilePath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil) + req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) // delete a file - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=file2.txt", nil) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file2.txt", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) // delete a missing file - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=file2.txt", nil) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file2.txt", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) // delete a directory - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=tdir", nil) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=tdir", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6070,7 +6070,7 @@ func TestWebFilesAPI(t *testing.T) { assert.NoError(t, err) err = os.Symlink(extPath, filepath.Join(user.GetHomeDir(), "file")) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=file", nil) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6083,14 +6083,14 @@ func TestWebFilesAPI(t *testing.T) { assert.NoError(t, err) _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath+"?path=tdir", reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath+"?path=tdir", reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=%2Ftdir%2Ffile1.txt", nil) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=%2Ftdir%2Ffile1.txt", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6103,20 +6103,20 @@ func TestWebFilesAPI(t *testing.T) { // the user is deleted, any API call should fail _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - req, err = http.NewRequest(http.MethodPatch, userFilePath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil) + req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=file2.txt", nil) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file2.txt", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6155,7 +6155,7 @@ func TestWebUploadErrors(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) // zip file are not allowed within sub2 - req, err := http.NewRequest(http.MethodPost, userFilePath+"?path=sub2", reader) + req, err := http.NewRequest(http.MethodPost, userFilesPath+"?path=sub2", reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6165,7 +6165,7 @@ func TestWebUploadErrors(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) // we have no upload permissions within sub1 - req, err = http.NewRequest(http.MethodPost, userFilePath+"?path=sub1", reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath+"?path=sub1", reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6173,7 +6173,7 @@ func TestWebUploadErrors(t *testing.T) { checkResponseCode(t, http.StatusForbidden, rr) // create a dir and try to overwrite it with a file - req, err = http.NewRequest(http.MethodPost, userFolderPath+"?path=file.zip", nil) + req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path=file.zip", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6181,7 +6181,7 @@ func TestWebUploadErrors(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6191,14 +6191,14 @@ func TestWebUploadErrors(t *testing.T) { // try to upload to a missing parent directory _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath+"?path=missingdir", reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath+"?path=missingdir", reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - req, err = http.NewRequest(http.MethodDelete, userFolderPath+"?path=file.zip", nil) + req, err = http.NewRequest(http.MethodDelete, userDirsPath+"?path=file.zip", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6206,7 +6206,7 @@ func TestWebUploadErrors(t *testing.T) { // upload will work now _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6215,7 +6215,7 @@ func TestWebUploadErrors(t *testing.T) { // overwrite the file _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6226,7 +6226,7 @@ func TestWebUploadErrors(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6234,7 +6234,7 @@ func TestWebUploadErrors(t *testing.T) { checkResponseCode(t, http.StatusNotFound, rr) if runtime.GOOS != osWindows { - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=file.zip", reader) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.zip", reader) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6246,7 +6246,7 @@ func TestWebUploadErrors(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6268,7 +6268,7 @@ func TestWebUploadErrors(t *testing.T) { reader = bytes.NewReader(body.Bytes()) _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath+"?path=sub2", reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath+"?path=sub2", reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6315,7 +6315,7 @@ func TestWebAPIVFolder(t *testing.T) { assert.NoError(t, err) reader := bytes.NewReader(body.Bytes()) - req, err := http.NewRequest(http.MethodPost, userFilePath+"?path=vdir", reader) + req, err := http.NewRequest(http.MethodPost, userFilesPath+"?path=vdir", reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6332,7 +6332,7 @@ func TestWebAPIVFolder(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath+"?path=vdir", reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath+"?path=vdir", reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6375,56 +6375,56 @@ func TestWebAPIWritePermission(t *testing.T) { assert.NoError(t, err) reader := bytes.NewReader(body.Bytes()) - req, err := http.NewRequest(http.MethodPost, userFilePath, reader) + req, err := http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr := executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, err = http.NewRequest(http.MethodPatch, userFilePath+"?path=a&target=b", nil) + req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=a&target=b", nil) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=a", nil) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=a", nil) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, err = http.NewRequest(http.MethodGet, userFilePath+"?path=a.txt", nil) + req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path=a.txt", nil) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - req, err = http.NewRequest(http.MethodGet, userFolderPath, nil) + req, err = http.NewRequest(http.MethodGet, userDirsPath, nil) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - req, err = http.NewRequest(http.MethodPost, userFolderPath+"?path=dir", nil) + req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path=dir", nil) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, err = http.NewRequest(http.MethodPatch, userFolderPath+"?path=dir&target=dir1", nil) + req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path=dir&target=dir1", nil) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - req, err = http.NewRequest(http.MethodDelete, userFolderPath+"?path=dir", nil) + req, err = http.NewRequest(http.MethodDelete, userDirsPath+"?path=dir", nil) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6457,7 +6457,7 @@ func TestWebAPICryptFs(t *testing.T) { assert.NoError(t, err) reader := bytes.NewReader(body.Bytes()) - req, err := http.NewRequest(http.MethodPost, userFilePath, reader) + req, err := http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6466,7 +6466,7 @@ func TestWebAPICryptFs(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6503,7 +6503,7 @@ func TestWebUploadSFTP(t *testing.T) { assert.NoError(t, err) reader := bytes.NewReader(body.Bytes()) - req, err := http.NewRequest(http.MethodPost, userFilePath, reader) + req, err := http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6523,7 +6523,7 @@ func TestWebUploadSFTP(t *testing.T) { // we are now overquota on overwrite _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6531,7 +6531,7 @@ func TestWebUploadSFTP(t *testing.T) { checkResponseCode(t, http.StatusInternalServerError, rr) assert.Contains(t, rr.Body.String(), "denying write due to space limit") // delete the file - req, err = http.NewRequest(http.MethodDelete, userFilePath+"?path=file.txt", nil) + req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.txt", nil) assert.NoError(t, err) setBearerForReq(req, webAPIToken) rr = executeRequest(req) @@ -6539,7 +6539,7 @@ func TestWebUploadSFTP(t *testing.T) { _, err = reader.Seek(0, io.SeekStart) assert.NoError(t, err) - req, err = http.NewRequest(http.MethodPost, userFilePath, reader) + req, err = http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) @@ -6563,7 +6563,7 @@ func TestWebUploadMultipartFormReadError(t *testing.T) { webAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword) assert.NoError(t, err) - req, err := http.NewRequest(http.MethodPost, userFilePath, nil) + req, err := http.NewRequest(http.MethodPost, userFilesPath, nil) assert.NoError(t, err) mpartForm := &multipart.Form{ @@ -6735,7 +6735,7 @@ func TestClientUserClose(t *testing.T) { err = writer.Close() assert.NoError(t, err) reader := bytes.NewReader(body.Bytes()) - req, err := http.NewRequest(http.MethodPost, userFilePath, reader) + req, err := http.NewRequest(http.MethodPost, userFilesPath, reader) assert.NoError(t, err) req.Header.Add("Content-Type", writer.FormDataContentType()) setBearerForReq(req, webAPIToken) diff --git a/httpd/internal_test.go b/httpd/internal_test.go index f1c3d52e..bc959d63 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -1580,7 +1580,7 @@ func TestGetFilesInvalidClaims(t *testing.T) { assert.Contains(t, rr.Body.String(), "Invalid token claims") rr = httptest.NewRecorder() - req, _ = http.NewRequest(http.MethodGet, webClientDirContentsPath, nil) + req, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) handleClientGetDirContents(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index c0a5790a..7d892d56 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -1763,8 +1763,41 @@ paths: tags: - users API summary: Read folders contents - description: Returns the contents of the specified folder for the logged in user + description: Returns the contents of the specified folder for the logged in user. Please use '/user/dirs' instead operationId: get_user_folder_contents + deprecated: true + parameters: + - in: query + name: path + description: Path to the folder to read. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the root folder is assumed + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DirEntry' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' + /user/dirs: + get: + tags: + - users API + summary: Read directory contents + description: Returns the contents of the specified directory for the logged in user + operationId: get_user_dir_contents parameters: - in: query name: path @@ -1795,7 +1828,7 @@ paths: - users API summary: Create a directory description: Create a directory for the logged in user - operationId: create_user_folder + operationId: create_user_dir parameters: - in: query name: path @@ -1827,7 +1860,7 @@ paths: - users API summary: Rename a directory description: Rename a directory for the logged in user. The rename is allowed for empty directory or for non empty, local directories, with no virtual folders inside - operationId: rename_user_folder + operationId: rename_user_dir parameters: - in: query name: path @@ -1865,7 +1898,7 @@ paths: - users API summary: Delete a directory description: Delete a directory for the logged in user. Only empty directories can be deleted - operationId: delete_user_folder + operationId: delete_user_dir parameters: - in: query name: path @@ -1897,8 +1930,48 @@ paths: tags: - users API summary: Download a single file - description: Returns the file contents as response body + description: Returns the file contents as response body. Please use '/user/files' instead operationId: get_user_file + deprecated: true + parameters: + - in: query + name: path + required: true + description: Path to the file to download. It must be URL encoded, for example the path "my dir/àdir/file.txt" must be sent as "my%20dir%2F%C3%A0dir%2Ffile.txt" + schema: + type: string + responses: + '200': + description: successful operation + content: + '*/*': + schema: + type: string + format: binary + '206': + description: successful operation + content: + '*/*': + schema: + type: string + format: binary + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalServerError' + default: + $ref: '#/components/responses/DefaultResponse' + /user/files: + get: + tags: + - users API + summary: Download a single file + description: Returns the file contents as response body + operationId: download_user_file parameters: - in: query name: path diff --git a/httpd/server.go b/httpd/server.go index 68155683..d9975736 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -631,14 +631,18 @@ func (s *httpdServer) initializeRouter() { router.Put(userPwdPath, changeUserPassword) router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Get(userPublicKeysPath, getUserPublicKeys) router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).Put(userPublicKeysPath, setUserPublicKeys) - router.Get(userFolderPath, readUserFolder) - router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userFolderPath, createUserDir) - router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userFolderPath, renameUserDir) - router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Delete(userFolderPath, deleteUserDir) + // compatibility layer to remove in v2.3 + router.With(compressor.Handler).Get(userFolderPath, readUserFolder) router.Get(userFilePath, getUserFile) - router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userFilePath, uploadUserFiles) - router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userFilePath, renameUserFile) - router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Delete(userFilePath, deleteUserFile) + + router.With(compressor.Handler).Get(userDirsPath, readUserFolder) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userDirsPath, createUserDir) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userDirsPath, renameUserDir) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Delete(userDirsPath, deleteUserDir) + router.Get(userFilesPath, getUserFile) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userFilesPath, uploadUserFiles) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userFilesPath, renameUserFile) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Delete(userFilesPath, deleteUserFile) router.Post(userStreamZipPath, getUserFilesAsZipStream) }) @@ -677,7 +681,19 @@ func (s *httpdServer) initializeRouter() { router.Get(webClientLogoutPath, handleWebClientLogout) router.With(s.refreshCookie).Get(webClientFilesPath, handleClientGetFiles) - router.With(compressor.Handler, s.refreshCookie).Get(webClientDirContentsPath, handleClientGetDirContents) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). + Post(webClientFilesPath, uploadUserFiles) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). + Patch(webClientFilesPath, renameUserFile) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). + Delete(webClientFilesPath, deleteUserFile) + router.With(compressor.Handler, s.refreshCookie).Get(webClientDirsPath, handleClientGetDirContents) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). + Post(webClientDirsPath, createUserDir) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). + Patch(webClientDirsPath, renameUserDir) + router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader). + Delete(webClientDirsPath, deleteUserDir) router.With(s.refreshCookie).Get(webClientDownloadZipPath, handleWebClientDownloadZip) router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials) router.Post(webChangeClientPwdPath, handleWebClientChangePwdPost) diff --git a/httpd/webclient.go b/httpd/webclient.go index 92b82451..b6426b4f 100644 --- a/httpd/webclient.go +++ b/httpd/webclient.go @@ -73,11 +73,15 @@ type dirMapping struct { type filesPage struct { baseClientPage - CurrentDir string - ReadDirURL string - DownloadURL string - Error string - Paths []dirMapping + CurrentDir string + DirsURL string + DownloadURL string + CanAddFiles bool + CanCreateDirs bool + CanRename bool + CanDelete bool + Error string + Paths []dirMapping } type clientMessagePage struct { @@ -207,13 +211,17 @@ func renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "") } -func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string) { +func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User) { data := filesPage{ baseClientPage: getBaseClientPageData(pageClientFilesTitle, webClientFilesPath, r), Error: error, CurrentDir: url.QueryEscape(dirName), DownloadURL: webClientDownloadZipPath, - ReadDirURL: webClientDirContentsPath, + DirsURL: webClientDirsPath, + CanAddFiles: user.CanAddFilesFromWeb(dirName), + CanCreateDirs: user.CanAddDirsFromWeb(dirName), + CanRename: user.CanRenameFromWeb(dirName, dirName), + CanDelete: user.CanDeleteFromWeb(dirName), } paths := []dirMapping{} if dirName != "/" { @@ -359,6 +367,7 @@ func handleClientGetDirContents(w http.ResponseWriter, r *http.Request) { res["size"] = util.ByteCountIEC(info.Size()) } } + res["type_name"] = fmt.Sprintf("%v_%v", res["type"], info.Name()) res["name"] = info.Name() res["last_modified"] = getFileObjectModTime(info.ModTime()) res["url"] = getFileObjectURL(name, info.Name()) @@ -406,11 +415,11 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) { info, err = connection.Stat(name, 0) } if err != nil { - renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %#v: %v", name, err)) + renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %#v: %v", name, err), user) return } if info.IsDir() { - renderFilesPage(w, r, name, "") + renderFilesPage(w, r, name, "", user) return } if status, err := downloadFile(w, r, connection, name, info); err != nil && status != 0 { @@ -419,7 +428,7 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) { renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "") return } - renderFilesPage(w, r, path.Dir(name), err.Error()) + renderFilesPage(w, r, path.Dir(name), err.Error(), user) } } } diff --git a/templates/webadmin/admins.html b/templates/webadmin/admins.html index 039ce154..3aaf12a0 100644 --- a/templates/webadmin/admins.html +++ b/templates/webadmin/admins.html @@ -65,7 +65,7 @@ Confirmation required diff --git a/templates/webadmin/base.html b/templates/webadmin/base.html index 5bd51df5..38322c53 100644 --- a/templates/webadmin/base.html +++ b/templates/webadmin/base.html @@ -10,7 +10,7 @@ - SFTPGo - {{template "title" .}} + SFTPGo Admin - {{template "title" .}} @@ -227,7 +227,7 @@ diff --git a/templates/webadmin/connections.html b/templates/webadmin/connections.html index ce23d416..6765c04d 100644 --- a/templates/webadmin/connections.html +++ b/templates/webadmin/connections.html @@ -58,7 +58,7 @@ Confirmation required diff --git a/templates/webadmin/defender.html b/templates/webadmin/defender.html index 1739fbc2..e9a08933 100644 --- a/templates/webadmin/defender.html +++ b/templates/webadmin/defender.html @@ -45,7 +45,7 @@ Confirmation required diff --git a/templates/webadmin/folders.html b/templates/webadmin/folders.html index 7ed196d2..f579642b 100644 --- a/templates/webadmin/folders.html +++ b/templates/webadmin/folders.html @@ -63,7 +63,7 @@ Confirmation required diff --git a/templates/webadmin/login.html b/templates/webadmin/login.html index c10db000..1efe1ca3 100644 --- a/templates/webadmin/login.html +++ b/templates/webadmin/login.html @@ -88,7 +88,7 @@
-

SFTPGo - {{.Version}}

+

SFTPGo Admin - {{.Version}}

{{if .Error}}
diff --git a/templates/webadmin/users.html b/templates/webadmin/users.html index 5dc0d915..989b71dd 100644 --- a/templates/webadmin/users.html +++ b/templates/webadmin/users.html @@ -66,7 +66,7 @@ Confirmation required
diff --git a/templates/webclient/base.html b/templates/webclient/base.html index d19e0ba6..aaeb0bd2 100644 --- a/templates/webclient/base.html +++ b/templates/webclient/base.html @@ -178,7 +178,7 @@ @@ -209,6 +209,10 @@ return '%' + c.charCodeAt(0).toString(16); }); } + + function replaceSlash(str){ + return str.replace(/\//g,'\u2215'); + } diff --git a/templates/webclient/files.html b/templates/webclient/files.html index 07e2c31e..35193b65 100644 --- a/templates/webclient/files.html +++ b/templates/webclient/files.html @@ -48,6 +48,118 @@
{{end}} +{{define "dialog"}} + + + + + + + +{{end}} + {{define "extra_js"}} @@ -159,7 +271,180 @@ } } + function getNameFromTypeName(typeName) { + return typeName.split('_').slice(1).join('_'); + } + + function getTypeFromTypeName(typeName) { + return typeName.split('_')[0]; + } + + function deleteAction() { + var table = $('#dataTable').DataTable(); + table.button('delete:name').enable(false); + var selected = table.column(0).checkboxes.selected()[0]; + var itemType = getTypeFromTypeName(selected); + var itemName = getNameFromTypeName(selected); + var path; + if (itemType == "1"){ + path = '{{.DirsURL}}'; + } else { + path = '{{.FilesURL}}'; + } + path+='?path={{.CurrentDir}}'+fixedEncodeURIComponent("/"+itemName); + $('#deleteModal').modal('hide'); + $.ajax({ + url: path, + type: 'DELETE', + dataType: 'json', + headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' }, + timeout: 15000, + success: function (result) { + location.reload(); + }, + error: function ($xhr, textStatus, errorThrown) { + var txt = "Unable to delete the selected item"; + if ($xhr) { + var json = $xhr.responseJSON; + if (json) { + if (json.message) { + txt = json.message; + } + if (json.error) { + txt += ": " + json.error; + } + } + } + $('#errorTxt').text(txt); + $('#errorMsg').show(); + setTimeout(function () { + $('#errorMsg').hide(); + }, 5000); + } + }); + } + $(document).ready(function () { + $("#create_dir_form").submit(function (event) { + event.preventDefault(); + $('#createDirModal').modal('hide'); + var dirName = replaceSlash($("#directory_name").val()); + var path = '{{.DirsURL}}?path={{.CurrentDir}}' + fixedEncodeURIComponent("/"+dirName); + $.ajax({ + url: path, + type: 'POST', + dataType: 'json', + headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' }, + timeout: 15000, + success: function (result) { + location.reload(); + }, + error: function ($xhr, textStatus, errorThrown) { + var txt = "Unable to create the requested directory"; + if ($xhr) { + var json = $xhr.responseJSON; + if (json) { + if (json.message) { + txt = json.message; + } + if (json.error) { + txt += ": " + json.error; + } + } + } + $('#errorTxt').text(txt); + $('#errorMsg').show(); + setTimeout(function () { + $('#errorMsg').hide(); + }, 5000); + } + }); + }); + + $("#upload_files_form").submit(function (event){ + event.preventDefault(); + $('uploadFilesModal').modal('hide'); + var path = '{{.FilesURL}}?path={{.CurrentDir}}'; + $.ajax({ + url: path, + type: 'POST', + data: new FormData(this), + processData: false, + contentType: false, + headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' }, + timeout: 15000, + success: function (result) { + location.reload(); + }, + error: function ($xhr, textStatus, errorThrown) { + var txt = "Error uploading files"; + if ($xhr) { + var json = $xhr.responseJSON; + if (json) { + if (json.message) { + txt = json.message; + } + if (json.error) { + txt += ": " + json.error; + } + } + } + $('#errorTxt').text(txt); + $('#errorMsg').show(); + setTimeout(function () { + $('#errorMsg').hide(); + }, 5000); + } + }); + }); + + $("#rename_form").submit(function (event){ + event.preventDefault(); + var table = $('#dataTable').DataTable(); + table.button('rename:name').enable(false); + var selected = table.column(0).checkboxes.selected()[0]; + var itemType = getTypeFromTypeName(selected); + var itemName = getNameFromTypeName(selected); + var targetName = replaceSlash($("#rename_new_name").val()); + var path; + if (itemType == "1"){ + path = '{{.DirsURL}}'; + } else { + path = '{{.FilesURL}}'; + } + path+='?path={{.CurrentDir}}'+fixedEncodeURIComponent("/"+itemName)+'&target={{.CurrentDir}}'+fixedEncodeURIComponent("/"+targetName); + $('renameModal').modal('hide'); + $.ajax({ + url: path, + type: 'PATCH', + dataType: 'json', + headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' }, + timeout: 15000, + success: function (result) { + location.reload(); + }, + error: function ($xhr, textStatus, errorThrown) { + var txt = "Error renaming item"; + if ($xhr) { + var json = $xhr.responseJSON; + if (json) { + if (json.message) { + txt = json.message; + } + if (json.error) { + txt += ": " + json.error; + } + } + } + $('#errorTxt').text(txt); + $('#errorMsg').show(); + setTimeout(function () { + $('#errorMsg').hide(); + }, 5000); + } + }); + }); + $.fn.dataTable.ext.buttons.refresh = { text: '', name: 'refresh', @@ -177,7 +462,7 @@ var filesArray = []; var selected = dt.column(0).checkboxes.selected(); for (i = 0; i < selected.length; i++) { - filesArray.push(selected[i]); + filesArray.push(getNameFromTypeName(selected[i])); } var files = fixedEncodeURIComponent(JSON.stringify(filesArray)); var downloadURL = '{{.DownloadURL}}'; @@ -187,9 +472,54 @@ enabled: false }; + $.fn.dataTable.ext.buttons.addFiles = { + text: '', + name: 'addFiles', + titleAttr: "Upload files", + action: function (e, dt, node, config) { + $('#uploadFilesModal').modal('show'); + }, + enabled: true + }; + + $.fn.dataTable.ext.buttons.addDirectory = { + text: '', + name: 'addDirectory', + titleAttr: "Add directory", + action: function (e, dt, node, config) { + $("#directory_name").val(""); + $('#createDirModal').modal('show'); + }, + enabled: true + }; + + $.fn.dataTable.ext.buttons.rename = { + text: '', + name: 'rename', + titleAttr: "Rename", + action: function (e, dt, node, config) { + var selected = table.column(0).checkboxes.selected()[0]; + var itemName = getNameFromTypeName(selected); + $("#rename_old_name").val(itemName); + $("#rename_new_name").val(""); + $('#renameModal').modal('show'); + }, + enabled: false + }; + + $.fn.dataTable.ext.buttons.delete = { + text: '', + name: 'delete', + titleAttr: "Delete", + action: function (e, dt, node, config) { + $('#deleteModal').modal('show'); + }, + enabled: false + }; + var table = $('#dataTable').DataTable({ "ajax": { - "url": "{{.ReadDirURL}}?path={{.CurrentDir}}", + "url": "{{.DirsURL}}?path={{.CurrentDir}}", "dataSrc": "", "error": function ($xhr, textStatus, errorThrown) { $(".dataTables_processing").hide(); @@ -214,7 +544,7 @@ "deferRender": true, "processing": true, "columns": [ - { "data": "name" }, + { "data": "type_name" }, { "data": "type" }, { "data": "name", @@ -250,6 +580,12 @@ selectedText = `${selectedItems} items selected`; } table.button('download:name').enable(selectedItems > 0); + {{if .CanRename}} + table.button('rename:name').enable(selectedItems == 1); + {{end}} + {{if .CanDelete}} + table.button('delete:name').enable(selectedItems == 1); + {{end}} $('#dataTable_info').find('span').remove(); $("#dataTable_info").append('' + selectedText + ''); } @@ -279,6 +615,18 @@ table.button().add(0, 'refresh'); table.button().add(0, 'pageLength'); table.button().add(0, 'download'); + {{if .CanDelete}} + table.button().add(0, 'delete'); + {{end}} + {{if .CanRename}} + table.button().add(0, 'rename'); + {{end}} + {{if .CanCreateDirs}} + table.button().add(0, 'addDirectory'); + {{end}} + {{if .CanAddFiles}} + table.button().add(0, 'addFiles'); + {{end}} table.buttons().container().appendTo('#dataTable_wrapper .col-md-6:eq(0)'); }, "orderFixed": [1, 'asc'],