mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
web ui: allow to create folders from a template
This commit is contained in:
parent
17a42a0c11
commit
267d9f1831
13 changed files with 268 additions and 51 deletions
|
@ -677,7 +677,7 @@ func (p *BoltProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, erro
|
|||
}
|
||||
|
||||
func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
err := validateFolder(folder)
|
||||
err := ValidateFolder(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -696,7 +696,7 @@ func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
}
|
||||
|
||||
func (p *BoltProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
err := validateFolder(folder)
|
||||
err := ValidateFolder(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -997,7 +997,7 @@ func validateFolderQuotaLimits(folder vfs.VirtualFolder) error {
|
|||
}
|
||||
|
||||
func getVirtualFolderIfInvalid(folder *vfs.BaseVirtualFolder) *vfs.BaseVirtualFolder {
|
||||
if err := validateFolder(folder); err == nil {
|
||||
if err := ValidateFolder(folder); err == nil {
|
||||
return folder
|
||||
}
|
||||
// we try to get the folder from the data provider if only the Name is populated
|
||||
|
@ -1029,7 +1029,7 @@ func validateUserVirtualFolders(user *User) error {
|
|||
return err
|
||||
}
|
||||
folder := getVirtualFolderIfInvalid(&v.BaseVirtualFolder)
|
||||
if err := validateFolder(folder); err != nil {
|
||||
if err := ValidateFolder(folder); err != nil {
|
||||
return err
|
||||
}
|
||||
cleanedMPath := folder.MappedPath
|
||||
|
@ -1388,7 +1388,9 @@ func createUserPasswordHash(user *User) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
// ValidateFolder returns an error if the folder is not valid
|
||||
// FIXME: this should be defined as Folder struct method
|
||||
func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
if folder.Name == "" {
|
||||
return &ValidationError{err: "folder name is mandatory"}
|
||||
}
|
||||
|
|
|
@ -651,7 +651,7 @@ func (p *MemoryProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, er
|
|||
}
|
||||
|
||||
func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
err := validateFolder(folder)
|
||||
err := ValidateFolder(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -675,7 +675,7 @@ func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
}
|
||||
|
||||
func (p *MemoryProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
err := validateFolder(folder)
|
||||
err := ValidateFolder(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -669,7 +669,7 @@ func sqlCommonAddOrGetFolder(ctx context.Context, baseFolder vfs.BaseVirtualFold
|
|||
}
|
||||
|
||||
func sqlCommonAddFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) error {
|
||||
err := validateFolder(folder)
|
||||
err := ValidateFolder(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -688,7 +688,7 @@ func sqlCommonAddFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) erro
|
|||
}
|
||||
|
||||
func sqlCommonUpdateFolder(folder *vfs.BaseVirtualFolder, dbHandle *sql.DB) error {
|
||||
err := validateFolder(folder)
|
||||
err := ValidateFolder(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/jwtauth"
|
||||
"github.com/go-chi/render"
|
||||
|
@ -18,36 +17,9 @@ type adminPwd struct {
|
|||
}
|
||||
|
||||
func getAdmins(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 100
|
||||
offset := 0
|
||||
order := dataprovider.OrderASC
|
||||
var err error
|
||||
if _, ok := r.URL.Query()["limit"]; ok {
|
||||
limit, err = strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
err = errors.New("Invalid limit")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if limit > 500 {
|
||||
limit = 500
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["offset"]; ok {
|
||||
offset, err = strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
if err != nil {
|
||||
err = errors.New("Invalid offset")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["order"]; ok {
|
||||
order = r.URL.Query().Get("order")
|
||||
if order != dataprovider.OrderASC && order != dataprovider.OrderDESC {
|
||||
err = errors.New("Invalid order")
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
limit, offset, order, err := getSearchFilters(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
admins, err := dataprovider.GetAdmins(limit, offset, order)
|
||||
|
|
|
@ -65,6 +65,7 @@ const (
|
|||
webQuotaScanPath = "/web/quota-scans"
|
||||
webChangeAdminPwdPath = "/web/changepwd/admin"
|
||||
webTemplateUser = "/web/template/user"
|
||||
webTemplateFolder = "/web/template/folder"
|
||||
webStaticFilesPath = "/static"
|
||||
// MaxRestoreSize defines the max size for the loaddata input file
|
||||
MaxRestoreSize = 10485760 // 10 MB
|
||||
|
|
|
@ -81,6 +81,7 @@ const (
|
|||
webRestorePath = "/web/restore"
|
||||
webChangeAdminPwdPath = "/web/changepwd/admin"
|
||||
webTemplateUser = "/web/template/user"
|
||||
webTemplateFolder = "/web/template/folder"
|
||||
httpBaseURL = "http://127.0.0.1:8081"
|
||||
configDir = ".."
|
||||
httpsCert = `-----BEGIN CERTIFICATE-----
|
||||
|
@ -2325,6 +2326,11 @@ func TestProviderErrors(t *testing.T) {
|
|||
setJWTCookieForReq(req, testServerToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
req, err = http.NewRequest(http.MethodGet, webTemplateFolder+"?from=afolder", nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, testServerToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
err = config.LoadConfig(configDir, "")
|
||||
assert.NoError(t, err)
|
||||
providerConf := config.GetProviderConf()
|
||||
|
@ -4803,6 +4809,38 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
}
|
||||
|
||||
func TestRenderFolderTemplateMock(t *testing.T) {
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, webTemplateFolder, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
Name: "templatefolder",
|
||||
MappedPath: filepath.Join(os.TempDir(), "mapped"),
|
||||
}
|
||||
folder, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webTemplateFolder+fmt.Sprintf("?from=%v", folder.Name), nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webTemplateFolder+"?from=unknown-folder", nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRenderUserTemplateMock(t *testing.T) {
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
|
@ -4976,7 +5014,7 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
// test invalid s3_upload_part_size
|
||||
form.Set("s3_upload_part_size", "a")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ := http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
req, _ := http.NewRequest(http.MethodPost, webTemplateUser, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
|
@ -4985,7 +5023,7 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
form.Set("s3_upload_concurrency", strconv.Itoa(user.FsConfig.S3Config.UploadConcurrency))
|
||||
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -4993,7 +5031,7 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
|
||||
form.Set("users", "user1::password1::invalid-pkey")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -5002,7 +5040,7 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
|
||||
form.Set("users", "user1:password1")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -5011,7 +5049,7 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
|
||||
form.Set("users", "user1::password1\nuser2::password2::"+testPubKey+"\nuser3::::")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webTemplateUser), &b)
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateUser, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
|
@ -5021,6 +5059,8 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
err = json.Unmarshal(rr.Body.Bytes(), &dump)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dump.Users, 2)
|
||||
require.Len(t, dump.Admins, 0)
|
||||
require.Len(t, dump.Folders, 0)
|
||||
user1 := dump.Users[0]
|
||||
user2 := dump.Users[1]
|
||||
require.Equal(t, "user1", user1.Username)
|
||||
|
@ -5044,6 +5084,71 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
require.Equal(t, "password2", user2.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
}
|
||||
|
||||
func TestFolderTemplateMock(t *testing.T) {
|
||||
folderName := "vfolder-template"
|
||||
mappedPath := filepath.Join(os.TempDir(), "%name%mapped%name%path")
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
form := make(url.Values)
|
||||
form.Set("name", folderName)
|
||||
form.Set("mapped_path", mappedPath)
|
||||
form.Set("folders", "folder1\nfolder2\nfolder3\nfolder1\n\n\n")
|
||||
contentType := "application/x-www-form-urlencoded"
|
||||
req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to verify form token")
|
||||
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder+"?param=p%C3%AO%GG", bytes.NewBuffer([]byte(form.Encode())))
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Error parsing folders fields")
|
||||
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
var dump dataprovider.BackupData
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &dump)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dump.Users, 0)
|
||||
require.Len(t, dump.Admins, 0)
|
||||
require.Len(t, dump.Folders, 3)
|
||||
require.Equal(t, "folder1", dump.Folders[0].Name)
|
||||
require.True(t, strings.HasSuffix(dump.Folders[0].MappedPath, "folder1mappedfolder1path"))
|
||||
require.Equal(t, "folder2", dump.Folders[1].Name)
|
||||
require.True(t, strings.HasSuffix(dump.Folders[1].MappedPath, "folder2mappedfolder2path"))
|
||||
require.Equal(t, "folder3", dump.Folders[2].Name)
|
||||
require.True(t, strings.HasSuffix(dump.Folders[2].MappedPath, "folder3mappedfolder3path"))
|
||||
|
||||
form.Set("folders", "\n\n\n")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "No folders to export")
|
||||
|
||||
form.Set("folders", "name")
|
||||
form.Set("mapped_path", "relative-path")
|
||||
req, _ = http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Error validating folder")
|
||||
}
|
||||
|
||||
func TestWebUserS3Mock(t *testing.T) {
|
||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -392,6 +392,9 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
|
||||
Get(webTemplateUser, handleWebTemplateUserGet)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, handleWebTemplateUserPost)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
|
||||
Get(webTemplateFolder, handleWebTemplateFolderGet)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, handleWebTemplateFolderPost)
|
||||
})
|
||||
|
||||
router.Group(func(router chi.Router) {
|
||||
|
|
85
httpd/web.go
85
httpd/web.go
|
@ -36,6 +36,7 @@ type folderPageMode int
|
|||
const (
|
||||
folderPageModeAdd folderPageMode = iota + 1
|
||||
folderPageModeUpdate
|
||||
folderPageModeTemplate
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -88,6 +89,7 @@ type basePage struct {
|
|||
ConnectionsURL string
|
||||
FoldersURL string
|
||||
FolderURL string
|
||||
FolderTemplateURL string
|
||||
LogoutURL string
|
||||
ChangeAdminPwdURL string
|
||||
FolderQuotaScanURL string
|
||||
|
@ -277,6 +279,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
|
|||
AdminURL: webAdminPath,
|
||||
FoldersURL: webFoldersPath,
|
||||
FolderURL: webFolderPath,
|
||||
FolderTemplateURL: webTemplateFolder,
|
||||
LogoutURL: webLogoutPath,
|
||||
ChangeAdminPwdURL: webChangeAdminPwdPath,
|
||||
QuotaScanURL: webQuotaScanPath,
|
||||
|
@ -409,6 +412,9 @@ func renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVir
|
|||
case folderPageModeUpdate:
|
||||
title = "Update folder"
|
||||
currentURL = fmt.Sprintf("%v/%v", webFolderPath, url.PathEscape(folder.Name))
|
||||
case folderPageModeTemplate:
|
||||
title = "Folder template"
|
||||
currentURL = webTemplateFolder
|
||||
}
|
||||
data := folderPage{
|
||||
basePage: getBasePageData(title, currentURL, r),
|
||||
|
@ -419,6 +425,20 @@ func renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVir
|
|||
renderTemplate(w, templateFolder, data)
|
||||
}
|
||||
|
||||
func getFoldersForTemplate(r *http.Request) []string {
|
||||
var res []string
|
||||
formValue := r.Form.Get("folders")
|
||||
folders := make(map[string]bool)
|
||||
for _, name := range getSliceFromDelimitedValues(formValue, "\n") {
|
||||
if _, ok := folders[name]; ok {
|
||||
continue
|
||||
}
|
||||
folders[name] = true
|
||||
res = append(res, name)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func getUsersForTemplate(r *http.Request) []userTemplateFields {
|
||||
var res []userTemplateFields
|
||||
formValue := r.Form.Get("users")
|
||||
|
@ -789,6 +809,16 @@ func replacePlaceholders(field string, replacements map[string]string) string {
|
|||
return field
|
||||
}
|
||||
|
||||
func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVirtualFolder {
|
||||
folder.Name = name
|
||||
replacements := make(map[string]string)
|
||||
replacements["%name%"] = folder.Name
|
||||
|
||||
folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements)
|
||||
|
||||
return folder
|
||||
}
|
||||
|
||||
func getCryptFsFromTemplate(fsConfig vfs.CryptFsConfig, replacements map[string]string) vfs.CryptFsConfig {
|
||||
if fsConfig.Passphrase != nil {
|
||||
if fsConfig.Passphrase.IsPlain() {
|
||||
|
@ -1185,6 +1215,61 @@ func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
|||
renderTemplate(w, templateUsers, data)
|
||||
}
|
||||
|
||||
func handleWebTemplateFolderGet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("from") != "" {
|
||||
name := r.URL.Query().Get("from")
|
||||
folder, err := dataprovider.GetFolderByName(name)
|
||||
if err == nil {
|
||||
renderFolderPage(w, r, folder, folderPageModeTemplate, "")
|
||||
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
}
|
||||
} else {
|
||||
folder := vfs.BaseVirtualFolder{}
|
||||
renderFolderPage(w, r, folder, folderPageModeTemplate, "")
|
||||
}
|
||||
}
|
||||
|
||||
func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
templateFolder := vfs.BaseVirtualFolder{}
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
renderMessagePage(w, r, "Error parsing folders fields", "", http.StatusBadRequest, err, "")
|
||||
return
|
||||
}
|
||||
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
templateFolder.MappedPath = r.Form.Get("mapped_path")
|
||||
|
||||
var dump dataprovider.BackupData
|
||||
dump.Version = dataprovider.DumpVersion
|
||||
|
||||
foldersFields := getFoldersForTemplate(r)
|
||||
for _, tmpl := range foldersFields {
|
||||
f := getFolderFromTemplate(templateFolder, tmpl)
|
||||
if err := dataprovider.ValidateFolder(&f); err != nil {
|
||||
renderMessagePage(w, r, fmt.Sprintf("Error validating folder %#v", f.Name), "", http.StatusBadRequest, err, "")
|
||||
return
|
||||
}
|
||||
dump.Folders = append(dump.Folders, f)
|
||||
}
|
||||
|
||||
if len(dump.Folders) == 0 {
|
||||
renderMessagePage(w, r, "No folders to export", "No valid folders found, export is not possible", http.StatusBadRequest, nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"sftpgo-%v-folders-from-template.json\"", len(dump.Folders)))
|
||||
render.JSON(w, r, dump)
|
||||
}
|
||||
|
||||
func handleWebTemplateUserGet(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("from") != "" {
|
||||
username := r.URL.Query().Get("from")
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{{define "page_body"}}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Add a new folder</h6>
|
||||
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Error}}
|
||||
|
@ -13,7 +13,34 @@
|
|||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="folder_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||
{{if eq .Mode 3}}
|
||||
<div class="card mb-4 border-left-info">
|
||||
<div class="card-body">
|
||||
Generate a data provider independent JSON file to create new folders or update existing ones.
|
||||
<br>
|
||||
The following placeholder is supported:
|
||||
<br><br>
|
||||
<ul>
|
||||
<li><span class="text-success">%name%</span> will be replaced with the specified folder name</li>
|
||||
</ul>
|
||||
The generated folders file can be imported from the "Maintenance" section.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="folder_form" action="{{.CurrentURL}}" method="POST" autocomplete="off" {{if eq .Mode 3}}target="_blank"{{end}}>
|
||||
{{if eq .Mode 3}}
|
||||
<div class="form-group row">
|
||||
<label for="idFolders" class="col-sm-2 col-form-label">Folders</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idFolders" name="folders" rows="5" required
|
||||
aria-describedby="foldersHelpBlock"></textarea>
|
||||
<small id="foldersHelpBlock" class="form-text text-muted">
|
||||
Specify the folder names, one for line.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="name" id="idFolderName" value="{{.Folder.Name}}">
|
||||
{{else}}
|
||||
<div class="form-group row">
|
||||
<label for="idFolderName" class="col-sm-2 col-form-label">Name</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -21,6 +48,7 @@
|
|||
value="{{.Folder.Name}}" maxlength="255" autocomplete="nope" required {{if ge .Mode 2}}readonly{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-group row">
|
||||
<label for="idMappedPath" class="col-sm-2 col-form-label">Absolute Path</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -30,7 +58,7 @@
|
|||
</div>
|
||||
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">{{if eq .Mode 3}}Generate and export folders{{else}}Submit{{end}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -141,6 +141,21 @@ function deleteAction() {
|
|||
enabled: false
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.template = {
|
||||
text: 'Template',
|
||||
name: 'template',
|
||||
action: function (e, dt, node, config) {
|
||||
var selectedRows = table.rows({ selected: true }).count();
|
||||
if (selectedRows == 1){
|
||||
var folderName = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.FolderTemplateURL}}' + "?from=" + encodeURIComponent(folderName);
|
||||
window.location.href = path;
|
||||
} else {
|
||||
window.location.href = '{{.FolderTemplateURL}}';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.delete = {
|
||||
text: 'Delete',
|
||||
name: 'delete',
|
||||
|
@ -213,6 +228,10 @@ function deleteAction() {
|
|||
table.button().add(0,'quota_scan');
|
||||
{{end}}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "manage_system"}}
|
||||
table.button().add(0,'template');
|
||||
{{end}}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "del_users"}}
|
||||
table.button().add(0,'delete');
|
||||
{{end}}
|
||||
|
|
|
@ -37,8 +37,7 @@ type webDavServer struct {
|
|||
binding Binding
|
||||
}
|
||||
|
||||
func (s *webDavServer) listenAndServe() error {
|
||||
compressor := middleware.NewCompressor(5, "text/*")
|
||||
func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
|
||||
handler := compressor.Handler(s)
|
||||
httpServer := &http.Server{
|
||||
Addr: s.binding.GetAddress(),
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
|
@ -157,6 +159,7 @@ func (c *Configuration) Initialize(configDir string) error {
|
|||
}
|
||||
certMgr = mgr
|
||||
}
|
||||
compressor := middleware.NewCompressor(5, "text/*")
|
||||
|
||||
serviceStatus = ServiceStatus{
|
||||
Bindings: nil,
|
||||
|
@ -174,7 +177,7 @@ func (c *Configuration) Initialize(configDir string) error {
|
|||
config: c,
|
||||
binding: binding,
|
||||
}
|
||||
exitChannel <- server.listenAndServe()
|
||||
exitChannel <- server.listenAndServe(compressor)
|
||||
}(binding)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue