web UI: add support for upload, create dirs, rename, delete

This commit is contained in:
Nicola Murino 2021-07-26 20:55:49 +02:00
parent 45a0473fec
commit 3a22aae34f
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
16 changed files with 625 additions and 131 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -65,7 +65,7 @@
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected admin?</div>

View file

@ -10,7 +10,7 @@
<meta name="description" content="">
<meta name="author" content="">
<title>SFTPGo - {{template "title" .}}</title>
<title>SFTPGo Admin - {{template "title" .}}</title>
<link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
@ -227,7 +227,7 @@
<div class="modal-header">
<h5 class="modal-title" id="modalLabel">Ready to Leave?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>

View file

@ -58,7 +58,7 @@
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to close the selected connection?</div>

View file

@ -45,7 +45,7 @@
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to remoce the selected blocklist entry?</div>

View file

@ -63,7 +63,7 @@
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected virtual folder and any users mapping?</div>

View file

@ -88,7 +88,7 @@
<div class="col-lg-12">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">SFTPGo - {{.Version}}</h1>
<h1 class="h4 text-gray-900 mb-4">SFTPGo Admin - {{.Version}}</h1>
</div>
{{if .Error}}
<div class="card mb-4 border-left-warning">

View file

@ -66,7 +66,7 @@
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected user?</div>

View file

@ -178,7 +178,7 @@
<div class="modal-header">
<h5 class="modal-title" id="modalLabel">Ready to Leave?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
@ -209,6 +209,10 @@
return '%' + c.charCodeAt(0).toString(16);
});
}
function replaceSlash(str){
return str.replace(/\//g,'\u2215');
}
</script>
<!-- Page level plugins -->

View file

@ -48,6 +48,118 @@
</div>
{{end}}
{{define "dialog"}}
<div class="modal fade" id="createDirModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Create a new directory
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="create_dir_form" action="" method="POST">
<div class="modal-body">
<div class="form-group">
<label for="directory_name" class="col-form-label">Name</label>
<input type="text" class="form-control" id="directory_name" required>
</div>
</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="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="" method="POST" enctype="multipart/form-data">
<div class="modal-body">
<input type="file" class="form-control-file" id="files_name" name="filename" 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="renameModal" tabindex="-1" role="dialog" aria-labelledby="renameModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameModalLabel">
Rename the selected item
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="rename_form" action="" method="POST">
<div class="modal-body">
<div class="form-group">
<label for="rename_old_name" class="col-form-label">Old name</label>
<input type="text" class="form-control" id="rename_old_name" readonly>
</div>
<div class="form-group">
<label for="rename_new_name" class="col-form-label">New name</label>
<input type="text" class="form-control" id="rename_new_name" required>
</div>
</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="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected item?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
@ -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: '<i class="fas fa-sync-alt"></i>',
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: '<i class="fas fa-file-upload"></i>',
name: 'addFiles',
titleAttr: "Upload files",
action: function (e, dt, node, config) {
$('#uploadFilesModal').modal('show');
},
enabled: true
};
$.fn.dataTable.ext.buttons.addDirectory = {
text: '<i class="fas fa-folder-plus"></i>',
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: '<i class="fas fa-edit"></i>',
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: '<i class="fas fa-trash"></i>',
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('<span class="selected-info"><span class="selected-item">' + selectedText + '</span></span>');
}
@ -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'],