mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 07:30:25 +00:00
WIP new WebAdmin: groups page
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
e6c8b0c86b
commit
5c8214e121
17 changed files with 590 additions and 313 deletions
2
go.mod
2
go.mod
|
@ -165,7 +165,7 @@ require (
|
|||
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.21.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -430,8 +430,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
|||
gocloud.dev v0.36.0 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus=
|
||||
gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
|
|
|
@ -3131,7 +3131,7 @@ func validateBaseParams(user *User) error {
|
|||
}
|
||||
err := user.FsConfig.Validate(user.GetEncryptionAdditionalData())
|
||||
if err != nil {
|
||||
return util.NewI18nError(err, util.I18nErrorFsValidation)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -3173,17 +3173,22 @@ func createUserPasswordHash(user *User) error {
|
|||
func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
|
||||
folder.FsConfig.SetEmptySecretsIfNil()
|
||||
if folder.Name == "" {
|
||||
return util.NewValidationError("folder name is mandatory")
|
||||
return util.NewI18nError(util.NewValidationError("folder name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(folder.Name) {
|
||||
return util.NewValidationError(fmt.Sprintf("folder name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
|
||||
folder.Name))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("folder name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", folder.Name)),
|
||||
util.I18nErrorInvalidName,
|
||||
)
|
||||
}
|
||||
if folder.FsConfig.Provider == sdk.LocalFilesystemProvider || folder.FsConfig.Provider == sdk.CryptedFilesystemProvider ||
|
||||
folder.MappedPath != "" {
|
||||
cleanedMPath := filepath.Clean(folder.MappedPath)
|
||||
if !filepath.IsAbs(cleanedMPath) {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid folder mapped path %q", folder.MappedPath))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("invalid folder mapped path %q", folder.MappedPath)),
|
||||
util.I18nErrorInvalidHomeDir,
|
||||
)
|
||||
}
|
||||
folder.MappedPath = cleanedMPath
|
||||
}
|
||||
|
|
|
@ -135,13 +135,16 @@ func (g *Group) hasRedactedSecret() bool {
|
|||
func (g *Group) validate() error {
|
||||
g.SetEmptySecretsIfNil()
|
||||
if g.Name == "" {
|
||||
return util.NewValidationError("name is mandatory")
|
||||
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(g.Name) {
|
||||
return util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", g.Name))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", g.Name)),
|
||||
util.I18nErrorInvalidName,
|
||||
)
|
||||
}
|
||||
if g.hasRedactedSecret() {
|
||||
return util.NewValidationError("cannot save a user with a redacted secret")
|
||||
return util.NewValidationError("cannot save a group with a redacted secret")
|
||||
}
|
||||
vfolders, err := validateAssociatedVirtualFolders(g.VirtualFolders)
|
||||
if err != nil {
|
||||
|
@ -155,8 +158,10 @@ func (g *Group) validateUserSettings() error {
|
|||
if g.UserSettings.HomeDir != "" {
|
||||
g.UserSettings.HomeDir = filepath.Clean(g.UserSettings.HomeDir)
|
||||
if !filepath.IsAbs(g.UserSettings.HomeDir) {
|
||||
return util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v",
|
||||
g.UserSettings.HomeDir))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", g.UserSettings.HomeDir)),
|
||||
util.I18nErrorInvalidHomeDir,
|
||||
)
|
||||
}
|
||||
}
|
||||
if err := g.UserSettings.FsConfig.Validate(g.GetEncryptionAdditionalData()); err != nil {
|
||||
|
@ -170,7 +175,7 @@ func (g *Group) validateUserSettings() error {
|
|||
if len(g.UserSettings.Permissions) > 0 {
|
||||
permissions, err := validateUserPermissions(g.UserSettings.Permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
return util.NewI18nError(err, util.I18nErrorGenericPermission)
|
||||
}
|
||||
g.UserSettings.Permissions = permissions
|
||||
}
|
||||
|
|
|
@ -1693,6 +1693,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
s.handleWebUpdateUserPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
|
||||
Get(webGroupsPath, s.handleWebGetGroups)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
|
||||
Get(webGroupsPath+"/json", getAllGroups)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
|
||||
Get(webGroupPath, s.handleWebAddGroupGet)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath, s.handleWebAddGroupPost)
|
||||
|
|
|
@ -180,11 +180,6 @@ type foldersPage struct {
|
|||
Folders []vfs.BaseVirtualFolder
|
||||
}
|
||||
|
||||
type groupsPage struct {
|
||||
basePage
|
||||
Groups []dataprovider.Group
|
||||
}
|
||||
|
||||
type rolesPage struct {
|
||||
basePage
|
||||
Roles []dataprovider.Role
|
||||
|
@ -447,7 +442,7 @@ func loadAdminTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateAdminDir, templateFolder),
|
||||
}
|
||||
groupsPaths := []string{
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateGroups),
|
||||
}
|
||||
|
@ -2982,7 +2977,7 @@ func getAllUsers(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, nil, util.I18nErrorDirList403, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
users := make([]dataprovider.User, 0, defaultQueryLimit)
|
||||
users := make([]dataprovider.User, 0, 100)
|
||||
for {
|
||||
u, err := dataprovider.GetUsers(defaultQueryLimit, len(users), dataprovider.OrderASC, claims.Role)
|
||||
if err != nil {
|
||||
|
@ -3500,7 +3495,7 @@ func (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
func (s *httpdServer) getWebGroups(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Group, error) {
|
||||
groups := make([]dataprovider.Group, 0, limit)
|
||||
groups := make([]dataprovider.Group, 0, 50)
|
||||
for {
|
||||
f, err := dataprovider.GetGroups(limit, len(groups), dataprovider.OrderASC, minimal)
|
||||
if err != nil {
|
||||
|
@ -3515,25 +3510,27 @@ func (s *httpdServer) getWebGroups(w http.ResponseWriter, r *http.Request, limit
|
|||
return groups, nil
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
func getAllGroups(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
var err error
|
||||
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
|
||||
groups := make([]dataprovider.Group, 0, 50)
|
||||
for {
|
||||
f, err := dataprovider.GetGroups(defaultQueryLimit, len(groups), dataprovider.OrderASC, false)
|
||||
if err != nil {
|
||||
limit = defaultQueryLimit
|
||||
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
groups = append(groups, f...)
|
||||
if len(f) < defaultQueryLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
groups, err := s.getWebGroups(w, r, limit, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
render.JSON(w, r, groups)
|
||||
}
|
||||
|
||||
data := groupsPage{
|
||||
basePage: s.getBasePageData(pageGroupsTitle, webGroupsPath, r),
|
||||
Groups: groups,
|
||||
}
|
||||
func (s *httpdServer) handleWebGetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := s.getBasePageData(pageGroupsTitle, webGroupsPath, r)
|
||||
renderAdminTemplate(w, templateGroups, data)
|
||||
}
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@ const (
|
|||
I18nErrorReservedUsername = "user.username_reserved"
|
||||
I18nErrorInvalidEmail = "general.email_invalid"
|
||||
I18nErrorInvalidUser = "user.username_invalid"
|
||||
I18nErrorInvalidName = "user.name_invalid"
|
||||
I18nErrorHomeRequired = "user.home_required"
|
||||
I18nErrorHomeInvalid = "user.home_invalid"
|
||||
I18nErrorPubKeyInvalid = "user.pub_key_invalid"
|
||||
|
@ -161,6 +162,24 @@ const (
|
|||
I18nStorageHTTP = "storage.http"
|
||||
I18nErrorInvalidQuotaSize = "user.invalid_quota_size"
|
||||
I18nErrorInvalidMaxFilesize = "filters.max_upload_size_invalid"
|
||||
I18nErrorInvalidHomeDir = "storage.home_dir_invalid"
|
||||
I18nErrorBucketRequired = "storage.bucket_required"
|
||||
I18nErrorRegionRequired = "storage.region_required"
|
||||
I18nErrorKeyPrefixInvalid = "storage.key_prefix_invalid"
|
||||
I18nErrorULPartSizeInvalid = "storage.ul_part_size_invalid"
|
||||
I18nErrorDLPartSizeInvalid = "storage.dl_part_size_invalid"
|
||||
I18nErrorULConcurrencyInvalid = "storage.ul_concurrency_invalid"
|
||||
I18nErrorDLConcurrencyInvalid = "storage.dl_concurrency_invalid"
|
||||
I18nErrorAccessKeyRequired = "storage.access_key_required"
|
||||
I18nErrorAccessSecretRequired = "storage.access_secret_required"
|
||||
I18nErrorFsCredentialsRequired = "storage.credentials_required"
|
||||
I18nErrorContainerRequired = "storage.container_required"
|
||||
I18nErrorAccountNameRequired = "storage.account_name_required"
|
||||
I18nErrorSASURLInvalid = "storage.sas_url_invalid"
|
||||
I18nErrorPassphraseRequired = "storage.passphrase_required"
|
||||
I18nErrorEndpointInvalid = "storage.endpoint_invalid"
|
||||
I18nErrorEndpointRequired = "storage.endpoint_required"
|
||||
I18nErrorFsUsernameRequired = "storage.username_required"
|
||||
)
|
||||
|
||||
// NewI18nError returns a I18nError wrappring the provided error
|
||||
|
|
|
@ -122,20 +122,26 @@ func (c *HTTPFsConfig) isSameResource(other HTTPFsConfig) bool {
|
|||
func (c *HTTPFsConfig) validate() error {
|
||||
c.setEmptyCredentialsIfNil()
|
||||
if c.Endpoint == "" {
|
||||
return errors.New("httpfs: endpoint cannot be empty")
|
||||
return util.NewI18nError(errors.New("httpfs: endpoint cannot be empty"), util.I18nErrorEndpointRequired)
|
||||
}
|
||||
c.Endpoint = strings.TrimRight(c.Endpoint, "/")
|
||||
endpointURL, err := url.Parse(c.Endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpfs: invalid endpoint: %w", err)
|
||||
return util.NewI18nError(fmt.Errorf("httpfs: invalid endpoint: %w", err), util.I18nErrorEndpointInvalid)
|
||||
}
|
||||
if !util.IsStringPrefixInSlice(c.Endpoint, supportedEndpointSchema) {
|
||||
return errors.New("httpfs: invalid endpoint schema: http and https are supported")
|
||||
return util.NewI18nError(
|
||||
errors.New("httpfs: invalid endpoint schema: http and https are supported"),
|
||||
util.I18nErrorEndpointInvalid,
|
||||
)
|
||||
}
|
||||
if endpointURL.Host == "unix" {
|
||||
socketPath := endpointURL.Query().Get("socket_path")
|
||||
if !filepath.IsAbs(socketPath) {
|
||||
return fmt.Errorf("httpfs: invalid unix domain socket path: %q", socketPath)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("httpfs: invalid unix domain socket path: %q", socketPath),
|
||||
util.I18nErrorEndpointInvalid,
|
||||
)
|
||||
}
|
||||
}
|
||||
if !isEqualityCheckModeValid(c.EqualityCheckMode) {
|
||||
|
@ -159,18 +165,29 @@ func (c *HTTPFsConfig) validate() error {
|
|||
// ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text
|
||||
func (c *HTTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not validate HTTP fs config: %v", err))
|
||||
var errI18n *util.I18nError
|
||||
errValidation := util.NewValidationError(fmt.Sprintf("could not validate HTTP fs config: %v", err))
|
||||
if errors.As(err, &errI18n) {
|
||||
return util.NewI18nError(errValidation, errI18n.Message)
|
||||
}
|
||||
return util.NewI18nError(errValidation, util.I18nErrorFsValidation)
|
||||
}
|
||||
if c.Password.IsPlain() {
|
||||
c.Password.SetAdditionalData(additionalData)
|
||||
if err := c.Password.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs password: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs password: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
if c.APIKey.IsPlain() {
|
||||
c.APIKey.SetAdditionalData(additionalData)
|
||||
if err := c.APIKey.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs API key: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs API key: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -153,17 +153,17 @@ func (c *SFTPFsConfig) isSameResource(other SFTPFsConfig) bool {
|
|||
func (c *SFTPFsConfig) validate() error {
|
||||
c.setEmptyCredentialsIfNil()
|
||||
if c.Endpoint == "" {
|
||||
return errors.New("endpoint cannot be empty")
|
||||
return util.NewI18nError(errors.New("endpoint cannot be empty"), util.I18nErrorEndpointRequired)
|
||||
}
|
||||
if !strings.Contains(c.Endpoint, ":") {
|
||||
c.Endpoint += ":22"
|
||||
}
|
||||
_, _, err := net.SplitHostPort(c.Endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid endpoint: %v", err)
|
||||
return util.NewI18nError(fmt.Errorf("invalid endpoint: %v", err), util.I18nErrorEndpointInvalid)
|
||||
}
|
||||
if c.Username == "" {
|
||||
return errors.New("username cannot be empty")
|
||||
return util.NewI18nError(errors.New("username cannot be empty"), util.I18nErrorFsUsernameRequired)
|
||||
}
|
||||
if c.BufferSize < 0 || c.BufferSize > 16 {
|
||||
return errors.New("invalid buffer_size, valid range is 0-16")
|
||||
|
@ -184,7 +184,7 @@ func (c *SFTPFsConfig) validate() error {
|
|||
|
||||
func (c *SFTPFsConfig) validateCredentials() error {
|
||||
if c.Password.IsEmpty() && c.PrivateKey.IsEmpty() {
|
||||
return errors.New("credentials cannot be empty")
|
||||
return util.NewI18nError(errors.New("credentials cannot be empty"), util.I18nErrorFsCredentialsRequired)
|
||||
}
|
||||
if c.Password.IsEncrypted() && !c.Password.IsValid() {
|
||||
return errors.New("invalid encrypted password")
|
||||
|
@ -210,24 +210,38 @@ func (c *SFTPFsConfig) validateCredentials() error {
|
|||
// ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text
|
||||
func (c *SFTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not validate SFTP fs config: %v", err))
|
||||
var errI18n *util.I18nError
|
||||
errValidation := util.NewValidationError(fmt.Sprintf("could not validate SFTP fs config: %v", err))
|
||||
if errors.As(err, &errI18n) {
|
||||
return util.NewI18nError(errValidation, errI18n.Message)
|
||||
}
|
||||
return util.NewI18nError(errValidation, util.I18nErrorFsValidation)
|
||||
}
|
||||
if c.Password.IsPlain() {
|
||||
c.Password.SetAdditionalData(additionalData)
|
||||
if err := c.Password.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs password: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs password: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
if c.PrivateKey.IsPlain() {
|
||||
c.PrivateKey.SetAdditionalData(additionalData)
|
||||
if err := c.PrivateKey.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs private key: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs private key: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
if c.KeyPassphrase.IsPlain() {
|
||||
c.KeyPassphrase.SetAdditionalData(additionalData)
|
||||
if err := c.KeyPassphrase.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs private key passphrase: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt SFTP fs private key passphrase: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -305,10 +305,16 @@ func (c *S3FsConfig) isSecretEqual(other S3FsConfig) bool {
|
|||
|
||||
func (c *S3FsConfig) checkCredentials() error {
|
||||
if c.AccessKey == "" && !c.AccessSecret.IsEmpty() {
|
||||
return errors.New("access_key cannot be empty with access_secret not empty")
|
||||
return util.NewI18nError(
|
||||
errors.New("access_key cannot be empty with access_secret not empty"),
|
||||
util.I18nErrorAccessKeyRequired,
|
||||
)
|
||||
}
|
||||
if c.AccessSecret.IsEmpty() && c.AccessKey != "" {
|
||||
return errors.New("access_secret cannot be empty with access_key not empty")
|
||||
return util.NewI18nError(
|
||||
errors.New("access_secret cannot be empty with access_key not empty"),
|
||||
util.I18nErrorAccessSecretRequired,
|
||||
)
|
||||
}
|
||||
if c.AccessSecret.IsEncrypted() && !c.AccessSecret.IsValid() {
|
||||
return errors.New("invalid encrypted access_secret")
|
||||
|
@ -322,13 +328,21 @@ func (c *S3FsConfig) checkCredentials() error {
|
|||
// ValidateAndEncryptCredentials validates the configuration and encrypts access secret if it is in plain text
|
||||
func (c *S3FsConfig) ValidateAndEncryptCredentials(additionalData string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not validate s3config: %v", err))
|
||||
var errI18n *util.I18nError
|
||||
errValidation := util.NewValidationError(fmt.Sprintf("could not validate s3config: %v", err))
|
||||
if errors.As(err, &errI18n) {
|
||||
return util.NewI18nError(errValidation, errI18n.Message)
|
||||
}
|
||||
return util.NewI18nError(errValidation, util.I18nErrorFsValidation)
|
||||
}
|
||||
if c.AccessSecret.IsPlain() {
|
||||
c.AccessSecret.SetAdditionalData(additionalData)
|
||||
err := c.AccessSecret.Encrypt()
|
||||
if err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt s3 access secret: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt s3 access secret: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -336,16 +350,28 @@ func (c *S3FsConfig) ValidateAndEncryptCredentials(additionalData string) error
|
|||
|
||||
func (c *S3FsConfig) checkPartSizeAndConcurrency() error {
|
||||
if c.UploadPartSize != 0 && (c.UploadPartSize < 5 || c.UploadPartSize > 5000) {
|
||||
return errors.New("upload_part_size cannot be != 0, lower than 5 (MB) or greater than 5000 (MB)")
|
||||
return util.NewI18nError(
|
||||
errors.New("upload_part_size cannot be != 0, lower than 5 (MB) or greater than 5000 (MB)"),
|
||||
util.I18nErrorULPartSizeInvalid,
|
||||
)
|
||||
}
|
||||
if c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {
|
||||
return fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency),
|
||||
util.I18nErrorULConcurrencyInvalid,
|
||||
)
|
||||
}
|
||||
if c.DownloadPartSize != 0 && (c.DownloadPartSize < 5 || c.DownloadPartSize > 5000) {
|
||||
return errors.New("download_part_size cannot be != 0, lower than 5 (MB) or greater than 5000 (MB)")
|
||||
return util.NewI18nError(
|
||||
errors.New("download_part_size cannot be != 0, lower than 5 (MB) or greater than 5000 (MB)"),
|
||||
util.I18nErrorDLPartSizeInvalid,
|
||||
)
|
||||
}
|
||||
if c.DownloadConcurrency < 0 || c.DownloadConcurrency > 64 {
|
||||
return fmt.Errorf("invalid download concurrency: %v", c.DownloadConcurrency)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("invalid download concurrency: %v", c.DownloadConcurrency),
|
||||
util.I18nErrorDLConcurrencyInvalid,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -366,19 +392,19 @@ func (c *S3FsConfig) validate() error {
|
|||
c.AccessSecret = kms.NewEmptySecret()
|
||||
}
|
||||
if c.Bucket == "" {
|
||||
return errors.New("bucket cannot be empty")
|
||||
return util.NewI18nError(errors.New("bucket cannot be empty"), util.I18nErrorBucketRequired)
|
||||
}
|
||||
// the region may be embedded within the endpoint for some S3 compatible
|
||||
// object storage, for example B2
|
||||
if c.Endpoint == "" && c.Region == "" {
|
||||
return errors.New("region cannot be empty")
|
||||
return util.NewI18nError(errors.New("region cannot be empty"), util.I18nErrorRegionRequired)
|
||||
}
|
||||
if err := c.checkCredentials(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.KeyPrefix != "" {
|
||||
if strings.HasPrefix(c.KeyPrefix, "/") {
|
||||
return errors.New("key_prefix cannot start with /")
|
||||
return util.NewI18nError(errors.New("key_prefix cannot start with /"), util.I18nErrorKeyPrefixInvalid)
|
||||
}
|
||||
c.KeyPrefix = path.Clean(c.KeyPrefix)
|
||||
if !strings.HasSuffix(c.KeyPrefix, "/") {
|
||||
|
@ -406,13 +432,21 @@ func (c *GCSFsConfig) HideConfidentialData() {
|
|||
// ValidateAndEncryptCredentials validates the configuration and encrypts credentials if they are in plain text
|
||||
func (c *GCSFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not validate GCS config: %v", err))
|
||||
var errI18n *util.I18nError
|
||||
errValidation := util.NewValidationError(fmt.Sprintf("could not validate GCS config: %v", err))
|
||||
if errors.As(err, &errI18n) {
|
||||
return util.NewI18nError(errValidation, errI18n.Message)
|
||||
}
|
||||
return util.NewI18nError(errValidation, util.I18nErrorFsValidation)
|
||||
}
|
||||
if c.Credentials.IsPlain() {
|
||||
c.Credentials.SetAdditionalData(additionalData)
|
||||
err := c.Credentials.Encrypt()
|
||||
if err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt GCS credentials: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt GCS credentials: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -459,11 +493,11 @@ func (c *GCSFsConfig) validate() error {
|
|||
c.Credentials = kms.NewEmptySecret()
|
||||
}
|
||||
if c.Bucket == "" {
|
||||
return errors.New("bucket cannot be empty")
|
||||
return util.NewI18nError(errors.New("bucket cannot be empty"), util.I18nErrorBucketRequired)
|
||||
}
|
||||
if c.KeyPrefix != "" {
|
||||
if strings.HasPrefix(c.KeyPrefix, "/") {
|
||||
return errors.New("key_prefix cannot start with /")
|
||||
return util.NewI18nError(errors.New("key_prefix cannot start with /"), util.I18nErrorKeyPrefixInvalid)
|
||||
}
|
||||
c.KeyPrefix = path.Clean(c.KeyPrefix)
|
||||
if !strings.HasSuffix(c.KeyPrefix, "/") {
|
||||
|
@ -474,7 +508,7 @@ func (c *GCSFsConfig) validate() error {
|
|||
return errors.New("invalid encrypted credentials")
|
||||
}
|
||||
if c.AutomaticCredentials == 0 && !c.Credentials.IsValidInput() {
|
||||
return errors.New("invalid credentials")
|
||||
return util.NewI18nError(errors.New("invalid credentials"), util.I18nErrorFsCredentialsRequired)
|
||||
}
|
||||
c.StorageClass = strings.TrimSpace(c.StorageClass)
|
||||
c.ACL = strings.TrimSpace(c.ACL)
|
||||
|
@ -563,18 +597,29 @@ func (c *AzBlobFsConfig) isSecretEqual(other AzBlobFsConfig) bool {
|
|||
// ValidateAndEncryptCredentials validates the configuration and encrypts access secret if it is in plain text
|
||||
func (c *AzBlobFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not validate Azure Blob config: %v", err))
|
||||
var errI18n *util.I18nError
|
||||
errValidation := util.NewValidationError(fmt.Sprintf("could not validate Azure Blob config: %v", err))
|
||||
if errors.As(err, &errI18n) {
|
||||
return util.NewI18nError(errValidation, errI18n.Message)
|
||||
}
|
||||
return util.NewI18nError(errValidation, util.I18nErrorFsValidation)
|
||||
}
|
||||
if c.AccountKey.IsPlain() {
|
||||
c.AccountKey.SetAdditionalData(additionalData)
|
||||
if err := c.AccountKey.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt Azure blob account key: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt Azure blob account key: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
if c.SASURL.IsPlain() {
|
||||
c.SASURL.SetAdditionalData(additionalData)
|
||||
if err := c.SASURL.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt Azure blob SAS URL: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt Azure blob SAS URL: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -583,7 +628,7 @@ func (c *AzBlobFsConfig) ValidateAndEncryptCredentials(additionalData string) er
|
|||
func (c *AzBlobFsConfig) checkCredentials() error {
|
||||
if c.SASURL.IsPlain() {
|
||||
_, err := url.Parse(c.SASURL.GetPayload())
|
||||
return err
|
||||
return util.NewI18nError(err, util.I18nErrorSASURLInvalid)
|
||||
}
|
||||
if c.SASURL.IsEncrypted() && !c.SASURL.IsValid() {
|
||||
return errors.New("invalid encrypted sas_url")
|
||||
|
@ -592,7 +637,7 @@ func (c *AzBlobFsConfig) checkCredentials() error {
|
|||
return nil
|
||||
}
|
||||
if c.AccountName == "" || !c.AccountKey.IsValidInput() {
|
||||
return errors.New("credentials cannot be empty or invalid")
|
||||
return util.NewI18nError(errors.New("credentials cannot be empty or invalid"), util.I18nErrorAccountNameRequired)
|
||||
}
|
||||
if c.AccountKey.IsEncrypted() && !c.AccountKey.IsValid() {
|
||||
return errors.New("invalid encrypted account_key")
|
||||
|
@ -602,16 +647,28 @@ func (c *AzBlobFsConfig) checkCredentials() error {
|
|||
|
||||
func (c *AzBlobFsConfig) checkPartSizeAndConcurrency() error {
|
||||
if c.UploadPartSize < 0 || c.UploadPartSize > 100 {
|
||||
return fmt.Errorf("invalid upload part size: %v", c.UploadPartSize)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("invalid upload part size: %v", c.UploadPartSize),
|
||||
util.I18nErrorULPartSizeInvalid,
|
||||
)
|
||||
}
|
||||
if c.UploadConcurrency < 0 || c.UploadConcurrency > 64 {
|
||||
return fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("invalid upload concurrency: %v", c.UploadConcurrency),
|
||||
util.I18nErrorULConcurrencyInvalid,
|
||||
)
|
||||
}
|
||||
if c.DownloadPartSize < 0 || c.DownloadPartSize > 100 {
|
||||
return fmt.Errorf("invalid download part size: %v", c.DownloadPartSize)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("invalid download part size: %v", c.DownloadPartSize),
|
||||
util.I18nErrorDLPartSizeInvalid,
|
||||
)
|
||||
}
|
||||
if c.DownloadConcurrency < 0 || c.DownloadConcurrency > 64 {
|
||||
return fmt.Errorf("invalid upload concurrency: %v", c.DownloadConcurrency)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("invalid upload concurrency: %v", c.DownloadConcurrency),
|
||||
util.I18nErrorDLConcurrencyInvalid,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -646,14 +703,14 @@ func (c *AzBlobFsConfig) validate() error {
|
|||
}
|
||||
// container could be embedded within SAS URL we check this at runtime
|
||||
if c.SASURL.IsEmpty() && c.Container == "" {
|
||||
return errors.New("container cannot be empty")
|
||||
return util.NewI18nError(errors.New("container cannot be empty"), util.I18nErrorContainerRequired)
|
||||
}
|
||||
if err := c.checkCredentials(); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.KeyPrefix != "" {
|
||||
if strings.HasPrefix(c.KeyPrefix, "/") {
|
||||
return errors.New("key_prefix cannot start with /")
|
||||
return util.NewI18nError(errors.New("key_prefix cannot start with /"), util.I18nErrorKeyPrefixInvalid)
|
||||
}
|
||||
c.KeyPrefix = path.Clean(c.KeyPrefix)
|
||||
if !strings.HasSuffix(c.KeyPrefix, "/") {
|
||||
|
@ -695,12 +752,20 @@ func (c *CryptFsConfig) isEqual(other CryptFsConfig) bool {
|
|||
// ValidateAndEncryptCredentials validates the configuration and encrypts the passphrase if it is in plain text
|
||||
func (c *CryptFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not validate Crypt fs config: %v", err))
|
||||
var errI18n *util.I18nError
|
||||
errValidation := util.NewValidationError(fmt.Sprintf("could not validate crypt fs config: %v", err))
|
||||
if errors.As(err, &errI18n) {
|
||||
return util.NewI18nError(errValidation, errI18n.Message)
|
||||
}
|
||||
return util.NewI18nError(errValidation, util.I18nErrorFsValidation)
|
||||
}
|
||||
if c.Passphrase.IsPlain() {
|
||||
c.Passphrase.SetAdditionalData(additionalData)
|
||||
if err := c.Passphrase.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt Crypt fs passphrase: %v", err))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("could not encrypt Crypt fs passphrase: %v", err)),
|
||||
util.I18nErrorFsValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -713,10 +778,10 @@ func (c *CryptFsConfig) isSameResource(other CryptFsConfig) bool {
|
|||
// validate returns an error if the configuration is not valid
|
||||
func (c *CryptFsConfig) validate() error {
|
||||
if c.Passphrase == nil || c.Passphrase.IsEmpty() {
|
||||
return errors.New("invalid passphrase")
|
||||
return util.NewI18nError(errors.New("invalid passphrase"), util.I18nErrorPassphraseRequired)
|
||||
}
|
||||
if !c.Passphrase.IsValidInput() {
|
||||
return errors.New("passphrase cannot be empty or invalid")
|
||||
return util.NewI18nError(errors.New("passphrase cannot be empty or invalid"), util.I18nErrorPassphraseRequired)
|
||||
}
|
||||
if c.Passphrase.IsEncrypted() && !c.Passphrase.IsValid() {
|
||||
return errors.New("invalid encrypted passphrase")
|
||||
|
|
|
@ -203,7 +203,8 @@
|
|||
"denied": "Denied",
|
||||
"zero_no_limit_help": "0 means no limit",
|
||||
"global_settings": "Global settings",
|
||||
"mandatory_encryption": "Mandatory encryption"
|
||||
"mandatory_encryption": "Mandatory encryption",
|
||||
"name_invalid": "The specified username is not valid, the following characters are allowed: a-zA-Z0-9-_.~"
|
||||
},
|
||||
"fs": {
|
||||
"view_file": "View file \"{{- path}}\"",
|
||||
|
@ -311,7 +312,8 @@
|
|||
"info": "Showing _START_ to _END_ of _TOTAL_ records",
|
||||
"info_empty": "Showing no record",
|
||||
"info_filtered": "(filtered from _MAX_ total records)",
|
||||
"processing": "Processing..."
|
||||
"processing": "Processing...",
|
||||
"no_records": "No records found"
|
||||
},
|
||||
"editor": {
|
||||
"keybinding": "Editor keybindings",
|
||||
|
@ -462,6 +464,11 @@
|
|||
"submit_export": "Generate and export users",
|
||||
"invalid_quota_size": "Invalid quota size"
|
||||
},
|
||||
"group": {
|
||||
"view_manage": "View and manage groups",
|
||||
"members": "Members",
|
||||
"members_summary": "Users: {{users}}. Admins: {{admins}}"
|
||||
},
|
||||
"virtual_folders": {
|
||||
"mount_path": "mount path, i.e. /vfolder",
|
||||
"quota_size": "Quota size",
|
||||
|
@ -483,6 +490,7 @@
|
|||
"home_dir_help1": "Leave blank for an appropriate default",
|
||||
"home_dir_help2": "Leave blank and storage to \"Local disk\" to not override the root directory",
|
||||
"home_dir_help3": "Required for local disk storage providers. For other storage providers this folder will be used for temporary files, you can leave it blank for an appropriate default",
|
||||
"home_dir_invalid": "Invalid root directory, make sure it is an absolute path",
|
||||
"sftp_home_dir": "SFTP root directory",
|
||||
"sftp_home_help": "Restrict access to this SFTP path. Example: \"/somedir/subdir\"",
|
||||
"os_read_buffer": "Download buffer (MB)",
|
||||
|
@ -535,7 +543,25 @@
|
|||
"sftp_concurrent_reads": "Disable concurrent reads",
|
||||
"relaxed_equality_check": "Relaxed equality check",
|
||||
"relaxed_equality_check_help": "Enable to consider only the endpoint to determine if different configurations point to the same server. By default, both the endpoint and username must match",
|
||||
"api_key": "API key"
|
||||
"api_key": "API key",
|
||||
"fs_error": "Filesystem configuration error",
|
||||
"bucket_required": "$t(storage.fs_error): bucket is required",
|
||||
"region_required": "$t(storage.fs_error): region is required",
|
||||
"key_prefix_invalid": "$t(storage.fs_error): invalid key prefix, cannot start with \"/\"",
|
||||
"ul_part_size_invalid": "$t(storage.fs_error): invalid upload part size",
|
||||
"ul_concurrency_invalid": "$t(storage.fs_error): invalid upload concurrency",
|
||||
"dl_part_size_invalid": "$t(storage.fs_error): invalid download part size",
|
||||
"dl_concurrency_invalid": "$t(storage.fs_error): invalid download concurrency",
|
||||
"access_key_required": "$t(storage.fs_error): access Key is required",
|
||||
"access_secret_required": "$t(storage.fs_error): access Secret is required",
|
||||
"credentials_required": "$t(storage.fs_error): credentials are required",
|
||||
"container_required": "$t(storage.fs_error): container is required",
|
||||
"account_name_required": "$t(storage.fs_error): account name is required",
|
||||
"sas_url_invalid": "$t(storage.fs_error): invalid SAS URL",
|
||||
"passphrase_required": "$t(storage.fs_error): passphrase is required",
|
||||
"endpoint_invalid": "$t(storage.fs_error): endpoint is invalid",
|
||||
"endpoint_required": "$t(storage.fs_error): endpoint is required",
|
||||
"username_required": "$t(storage.fs_error): username is required"
|
||||
},
|
||||
"oidc": {
|
||||
"token_expired": "Your OpenID token has expired, please log in again",
|
||||
|
|
|
@ -203,7 +203,8 @@
|
|||
"denied": "Negato",
|
||||
"zero_no_limit_help": "0 significa nessun limite",
|
||||
"global_settings": "Impostazioni globali",
|
||||
"mandatory_encryption": "Crittografia obbligatoria"
|
||||
"mandatory_encryption": "Crittografia obbligatoria",
|
||||
"name_invalid": "Il nome specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~"
|
||||
},
|
||||
"fs": {
|
||||
"view_file": "Visualizza file \"{{- path}}\"",
|
||||
|
@ -309,9 +310,10 @@
|
|||
},
|
||||
"datatable": {
|
||||
"info": "Risultati da _START_ a _END_ di _TOTAL_ elementi",
|
||||
"info_empty": "Nessun risultato",
|
||||
"info_empty": "Nulla da mostrare",
|
||||
"info_filtered": "(filtrati da _MAX_ elementi totali)",
|
||||
"processing": "Elaborazione..."
|
||||
"processing": "Elaborazione...",
|
||||
"no_records": "Nessun risultato"
|
||||
},
|
||||
"editor": {
|
||||
"keybinding": "Combinazioni di tasti dell'editor",
|
||||
|
@ -462,6 +464,11 @@
|
|||
"submit_export": "Genera ed esporta utenti",
|
||||
"invalid_quota_size": "Quota (dimensione) non valida"
|
||||
},
|
||||
"group": {
|
||||
"view_manage": "Visualizza e gestisci gruppi",
|
||||
"members": "Membri",
|
||||
"members_summary": "Utenti: {{users}}. Amministratori: {{admins}}"
|
||||
},
|
||||
"virtual_folders": {
|
||||
"mount_path": "percorso, es. /vfolder",
|
||||
"quota_size": "Quota (dimensione)",
|
||||
|
@ -483,6 +490,7 @@
|
|||
"home_dir_help1": "Lasciare vuoto per un valore predefinito appropriato",
|
||||
"home_dir_help2": "Lascia vuoto e archiviazione su \"Disco locale\" per non sovrascrivere la directory principale",
|
||||
"home_dir_help3": "Obbligatorio per i provider di archiviazione su disco locale. Per gli altri provider di archiviazione questa cartella sarà usata per i file temporanei, puoi lasciare vuoto per un valore predefinito appropriato",
|
||||
"home_dir_invalid": "Cartella principale non valida, assicurati che sia un path assoluto",
|
||||
"sftp_home_dir": "Cartella principale SFTP",
|
||||
"sftp_home_help": "Limitare l'accesso a questo percorso SFTP. Esempio: \"/somedir/subdir\"",
|
||||
"os_read_buffer": "Buffer download (MB)",
|
||||
|
@ -495,12 +503,12 @@
|
|||
"endpoint": "Endpoint",
|
||||
"endpoint_help": "Per AWS S3, lasciare vuoto per utilizzare l'endpoint predefinito per la regione specificata",
|
||||
"sftp_endpoint_help": "Endpoint come host:porta. La porta è sempre richiesta",
|
||||
"ul_part_size": "Dimensioni parte per upload (MB)",
|
||||
"ul_part_size": "Dimensione parte per upload (MB)",
|
||||
"part_size_help": "0 significa il default (5 MB). Il minimo è 5",
|
||||
"gcs_part_size_help": "0 significa il default (16 MB)",
|
||||
"ul_concurrency": "Concorrenza upload",
|
||||
"ul_concurrency_help": "Numero di parti caricate in parallelo. 0 significa il default (5)",
|
||||
"dl_part_size": "Dimensioni parte per download (MB)",
|
||||
"dl_part_size": "Dimensione parte per download (MB)",
|
||||
"dl_concurrency": "Concorrenza download",
|
||||
"dl_concurrency_help": "Numero di parti scaricate in parallelo. 0 significa il default (5)",
|
||||
"ul_part_timeout": "Timeout per upload parte",
|
||||
|
@ -535,7 +543,25 @@
|
|||
"sftp_concurrent_reads": "Disabilitare letture concorrenti",
|
||||
"relaxed_equality_check": "Controllo di uguaglianza non rigoroso",
|
||||
"relaxed_equality_check_help": "Abilitare per considerare solo l'endpoint per determinare se diverse configurazioni puntano allo stesso server. Per impostazione predefinita, sia l'endpoint che il nome utente devono corrispondere",
|
||||
"api_key": "Chiave API"
|
||||
"api_key": "Chiave API",
|
||||
"fs_error": "Errore configurazione filesystem",
|
||||
"bucket_required": "$t(storage.fs_error): il bucket è obbligatorio",
|
||||
"region_required": "$t(storage.fs_error): la regione è obbligatoria",
|
||||
"key_prefix_invalid": "$t(storage.fs_error): prefisso chiave non valido, non può iniziare con \"/\"",
|
||||
"ul_part_size_invalid": "$t(storage.fs_error): dimensione parte per upload non valida",
|
||||
"ul_concurrency_invalid": "$t(storage.fs_error): concorrenza upload non valida",
|
||||
"dl_part_size_invalid": "$t(storage.fs_error): dimensione parte per download non valida",
|
||||
"dl_concurrency_invalid": "$t(storage.fs_error): concorrenza download non valida",
|
||||
"access_key_required": "$t(storage.fs_error): la chiave di accesso è obbligatoria",
|
||||
"access_secret_required": "$t(storage.fs_error): la chiave di accesso segreta è obbligatoria",
|
||||
"credentials_required": "$t(storage.fs_error): le credenziali per il filesystem sono obbligatorie",
|
||||
"container_required": "$t(storage.fs_error): il contenitore è obbligatorio",
|
||||
"account_name_required": "$t(storage.fs_error): il nome account è obbligatorio",
|
||||
"sas_url_invalid": "$t(storage.fs_error): SAS URL non valido",
|
||||
"passphrase_required": "$t(storage.fs_error): la passphrase è obbligatoria",
|
||||
"endpoint_invalid": "$t(storage.fs_error): endpoint non valido",
|
||||
"endpoint_required": "$t(storage.fs_error): endpoint è obbligatorio",
|
||||
"username_required": "$t(storage.fs_error): nome utente è obbligatorio"
|
||||
},
|
||||
"oidc": {
|
||||
"token_expired": "Il tuo token OpenID è scaduto, effettua nuovamente l'accesso",
|
||||
|
|
|
@ -130,13 +130,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div class="form-group row mt-10 fsconfig fsconfig-s3fs">
|
||||
<label for="idS3PartSize" data-i18n="storage.ul_part_size" class="col-md-3 col-form-label">Upload Part Size (MB)</label>
|
||||
<div class="col-md-3">
|
||||
<input id="idS3PartSize" type="number" min="0" class="form-control" name="s3_upload_part_size" value="{{.S3Config.UploadPartSize}}" aria-describedby="idS3PartSizeHelp" />
|
||||
<input id="idS3PartSize" type="number" min="0" max="5000" class="form-control" name="s3_upload_part_size" value="{{.S3Config.UploadPartSize}}" aria-describedby="idS3PartSizeHelp" />
|
||||
<div id="idS3PartSizeHelp" class="form-text" data-i18n="storage.part_size_help"></div>
|
||||
</div>
|
||||
<div class="col-md-1"></div>
|
||||
<label for="idS3UploadConcurrency" data-i18n="storage.ul_concurrency" class="col-md-2 col-form-label">Upload concurrency</label>
|
||||
<div class="col-md-3">
|
||||
<input id="idS3UploadConcurrency" type="number" min="0" class="form-control" name="s3_upload_concurrency" value="{{.S3Config.UploadConcurrency}}" aria-describedby="idS3UploadConcurrencyHelp" />
|
||||
<input id="idS3UploadConcurrency" type="number" min="0" max="64" class="form-control" name="s3_upload_concurrency" value="{{.S3Config.UploadConcurrency}}" aria-describedby="idS3UploadConcurrencyHelp" />
|
||||
<div id="idS3UploadConcurrencyHelp" class="form-text" data-i18n="storage.ul_concurrency_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -144,13 +144,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div class="form-group row mt-10 fsconfig fsconfig-s3fs">
|
||||
<label for="idS3DLPartSize" data-i18n="storage.dl_part_size" class="col-md-3 col-form-label">Download Part Size (MB)</label>
|
||||
<div class="col-md-3">
|
||||
<input id="idS3DLPartSize" type="number" min="0" class="form-control" name="s3_download_part_size" value="{{.S3Config.DownloadPartSize}}" aria-describedby="idS3DLPartSizeHelp" />
|
||||
<input id="idS3DLPartSize" type="number" min="0" max="5000" class="form-control" name="s3_download_part_size" value="{{.S3Config.DownloadPartSize}}" aria-describedby="idS3DLPartSizeHelp" />
|
||||
<div id="idS3DLPartSizeHelp" class="form-text" data-i18n="storage.part_size_help"></div>
|
||||
</div>
|
||||
<div class="col-md-1"></div>
|
||||
<label for="idS3DownloadConcurrency" data-i18n="storage.dl_concurrency" class="col-md-2 col-form-label">Download concurrency</label>
|
||||
<div class="col-md-3">
|
||||
<input id="idS3DownloadConcurrency" type="number" min="0" class="form-control" name="s3_download_concurrency" value="{{.S3Config.DownloadConcurrency}}" aria-describedby="idS3DownloadConcurrencyHelp" />
|
||||
<input id="idS3DownloadConcurrency" type="number" min="0" max="64" class="form-control" name="s3_download_concurrency" value="{{.S3Config.DownloadConcurrency}}" aria-describedby="idS3DownloadConcurrencyHelp" />
|
||||
<div id="idS3DownloadConcurrencyHelp" class="form-text" data-i18n="storage.dl_concurrency_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -306,13 +306,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div class="form-group row mt-10 fsconfig fsconfig-azblobfs">
|
||||
<label for="idAzUploadPartSize" data-i18n="storage.ul_part_size" class="col-md-3 col-form-label">Upload Part Size (MB)</label>
|
||||
<div class="col-md-3">
|
||||
<input id="idAzUploadPartSize" type="number" min="0" class="form-control" name="az_upload_part_size" value="{{.AzBlobConfig.UploadPartSize}}" aria-describedby="idAzUploadPartSizeHelp" />
|
||||
<input id="idAzUploadPartSize" type="number" min="0" max="100" class="form-control" name="az_upload_part_size" value="{{.AzBlobConfig.UploadPartSize}}" aria-describedby="idAzUploadPartSizeHelp" />
|
||||
<div id="idAzUploadPartSizeHelp" class="form-text" data-i18n="storage.part_size_help"></div>
|
||||
</div>
|
||||
<div class="col-md-1"></div>
|
||||
<label for="idAzUploadConcurrency" data-i18n="storage.ul_concurrency" class="col-md-2 col-form-label">Upload concurrency</label>
|
||||
<div class="col-md-3">
|
||||
<input id="idAzUploadConcurrency" type="number" min="0" class="form-control" name="az_upload_concurrency" value="{{.AzBlobConfig.UploadConcurrency}}" aria-describedby="idAzUploadConcurrencyHelp" />
|
||||
<input id="idAzUploadConcurrency" type="number" min="0" max="64" class="form-control" name="az_upload_concurrency" value="{{.AzBlobConfig.UploadConcurrency}}" aria-describedby="idAzUploadConcurrencyHelp" />
|
||||
<div id="idAzUploadConcurrencyHelp" class="form-text" data-i18n="storage.ul_concurrency_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -320,13 +320,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div class="form-group row mt-10 fsconfig fsconfig-azblobfs">
|
||||
<label for="idAzDownloadPartSize" data-i18n="storage.dl_part_size" class="col-md-3 col-form-label">Download Part Size (MB)</label>
|
||||
<div class="col-md-3">
|
||||
<input id="idAzDownloadPartSize" type="number" min="0" class="form-control" name="az_download_part_size" value="{{.AzBlobConfig.DownloadPartSize}}" aria-describedby="idAzDownloadPartSizeHelp" />
|
||||
<input id="idAzDownloadPartSize" type="number" min="0" max="100" class="form-control" name="az_download_part_size" value="{{.AzBlobConfig.DownloadPartSize}}" aria-describedby="idAzDownloadPartSizeHelp" />
|
||||
<div id="idAzDownloadPartSizeHelp" class="form-text" data-i18n="storage.part_size_help"></div>
|
||||
</div>
|
||||
<div class="col-md-1"></div>
|
||||
<label for="idAzDownloadConcurrency" data-i18n="storage.dl_concurrency" class="col-md-2 col-form-label">Download concurrency</label>
|
||||
<div class="col-md-3">
|
||||
<input id="idAzDownloadConcurrency" type="number" min="0" class="form-control" name="az_download_concurrency" value="{{.AzBlobConfig.DownloadConcurrency}}" aria-describedby="idAzDownloadConcurrencyHelp"/>
|
||||
<input id="idAzDownloadConcurrency" type="number" min="0" max="64" class="form-control" name="az_download_concurrency" value="{{.AzBlobConfig.DownloadConcurrency}}" aria-describedby="idAzDownloadConcurrencyHelp"/>
|
||||
<div id="idAzDownloadConcurrencyHelp" class="form-text" data-i18n="storage.dl_concurrency_help"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,229 +1,339 @@
|
|||
<!--
|
||||
Copyright (C) 2019 Nicola Murino
|
||||
Copyright (C) 2024 Nicola Murino
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3.
|
||||
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
https://keenthemes.com/products/templates-mega-bundle
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
KeenThemes HTML/CSS/JS components are allowed for use only within the
|
||||
SFTPGo product and restricted to be used in a resealable HTML template
|
||||
that can compete with KeenThemes products anyhow.
|
||||
|
||||
This WebUI is allowed for use only within the SFTPGo product and
|
||||
therefore cannot be used in derivative works/products without an
|
||||
explicit grant from the SFTPGo Team (support@sftpgo.com).
|
||||
-->
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
{{- define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
|
||||
{{- end}}
|
||||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||
<span id="errorTxt"></span>
|
||||
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
function dismissErrorMsg(){
|
||||
$('#errorMsg').hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">View and manage groups</h6>
|
||||
{{- define "page_body"}}
|
||||
{{- template "errmsg" ""}}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="group.view_manage" class="card-title section-title">View and manage groups</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
|
||||
<div id="card_body" class="card-body">
|
||||
<div id="loader" class="align-items-center text-center my-10">
|
||||
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
|
||||
<span data-i18n="general.loading" class="text-gray-700">Loading...</span>
|
||||
</div>
|
||||
<div id="card_content" class="d-none">
|
||||
<div class="d-flex flex-stack flex-wrap mb-5">
|
||||
<div class="d-flex align-items-center position-relative my-2">
|
||||
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
|
||||
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
|
||||
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end my-2" data-table-toolbar="base">
|
||||
<button type="button" class="btn btn-light-primary rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom" data-kt-menu-permanent="true">
|
||||
<span data-i18n="general.colvis">Column visibility</span>
|
||||
<i class="ki-duotone ki-down fs-3 rotate-180 ms-3 me-0"></i>
|
||||
</button>
|
||||
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4" data-kt-menu="true">
|
||||
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
|
||||
<input type="checkbox" class="form-check-input" value="" id="checkColMembers" />
|
||||
<label class="form-check-label" for="checkColMembers">
|
||||
<span data-i18n="group.members" class="text-gray-800 fs-6">Members</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
|
||||
<input type="checkbox" class="form-check-input" value="" id="checkColDesc" />
|
||||
<label class="form-check-label" for="checkColDesc">
|
||||
<span data-i18n="general.description" class="text-gray-800 fs-6">Description</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{- if .LoggedUser.HasPermission "manage_groups"}}
|
||||
<a href="{{.GroupURL}}" class="btn btn-primary ms-5">
|
||||
<i class="ki-duotone ki-plus fs-2"></i>
|
||||
<span data-i18n="general.add">Add</span>
|
||||
</a>
|
||||
{{- end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Members</th>
|
||||
<tr class="text-start text-muted fw-bold fs-6 gs-0">
|
||||
<th data-i18n="general.name">Name</th>
|
||||
<th data-i18n="group.members">Members</th>
|
||||
<th data-i18n="general.description">Description</th>
|
||||
<th class="min-w-100px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Groups}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.Description}}</td>
|
||||
<td>{{.GetMembersAsString}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{- end}}
|
||||
{{- define "extra_js"}}
|
||||
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
|
||||
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
|
||||
function deleteAction(username) {
|
||||
ModalAlert.fire({
|
||||
text: $.t('general.delete_confirm_generic'),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('general.delete_confirm_btn'),
|
||||
cancelButtonText: $.t('general.cancel'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-danger",
|
||||
cancelButton: 'btn btn-secondary'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed){
|
||||
$('#loading_message').text("");
|
||||
KTApp.showPageLoading();
|
||||
let path = '{{.GroupURL}}' + "/" + encodeURIComponent(username);
|
||||
|
||||
{{define "dialog"}}
|
||||
<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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">Do you want to delete the selected group? A referenced group cannot be removed</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>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function deleteAction() {
|
||||
let table = $('#dataTable').DataTable();
|
||||
table.button('delete:name').enable(false);
|
||||
let groupName = table.row({ selected: true }).data()[0];
|
||||
let path = '{{.GroupURL}}' + "/" + fixedEncodeURIComponent(groupName);
|
||||
$('#deleteModal').modal('hide');
|
||||
$('#errorMsg').hide();
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
window.location.href = '{{.GroupsURL}}';
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
var txt = "Unable to delete the selected group";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt += ": " + json.message;
|
||||
} else {
|
||||
txt += ": " + json.error;
|
||||
axios.delete(path, {
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
validateStatus: function (status) {
|
||||
return status == 200;
|
||||
}
|
||||
}).then(function(response){
|
||||
location.reload();
|
||||
}).catch(function(error){
|
||||
KTApp.hidePageLoading();
|
||||
let errorMessage;
|
||||
if (error && error.response) {
|
||||
switch (error.response.status) {
|
||||
case 403:
|
||||
errorMessage = "general.delete_error_403";
|
||||
break;
|
||||
case 404:
|
||||
errorMessage = "general.delete_error_404";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorMsg').show();
|
||||
if (!errorMessage){
|
||||
errorMessage = "general.delete_error_generic";
|
||||
}
|
||||
showToast(2, errorMessage);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$.fn.dataTable.ext.buttons.add = {
|
||||
text: '<i class="fas fa-plus"></i>',
|
||||
name: 'add',
|
||||
titleAttr: "Add",
|
||||
action: function (e, dt, node, config) {
|
||||
window.location.href = '{{.GroupURL}}';
|
||||
}
|
||||
};
|
||||
var datatable = function(){
|
||||
var dt;
|
||||
|
||||
$.fn.dataTable.ext.buttons.edit = {
|
||||
text: '<i class="fas fa-pen"></i>',
|
||||
name: 'edit',
|
||||
titleAttr: "Edit",
|
||||
action: function (e, dt, node, config) {
|
||||
var groupName = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.GroupURL}}' + "/" + fixedEncodeURIComponent(groupName);
|
||||
window.location.href = path;
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
var initDatatable = function () {
|
||||
$('#errorMsg').addClass("d-none");
|
||||
dt = $('#dataTable').DataTable({
|
||||
ajax: {
|
||||
url: "{{.GroupsURL}}/json",
|
||||
dataSrc: "",
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
$(".dataTables_processing").hide();
|
||||
let txt = "";
|
||||
if ($xhr) {
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt = json.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!txt){
|
||||
txt = "general.error500";
|
||||
}
|
||||
setI18NData($('#errorTxt'), txt);
|
||||
$('#errorMsg').removeClass("d-none");
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
data: "name",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "users",
|
||||
defaultContent: "",
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
let users = 0;
|
||||
if (row.users){
|
||||
users = row.users.length;
|
||||
}
|
||||
let admins = 0;
|
||||
if (row.admins){
|
||||
admins = row.admins.length;
|
||||
}
|
||||
return $.t('group.members_summary', {users: users, admins: admins});
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "description",
|
||||
visible: false,
|
||||
defaultContent: "",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
if (data){
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "id",
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
className: 'text-end',
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
let numActions = 0;
|
||||
let actions = `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
|
||||
<span data-i18n="general.actions" class="fs-6">Actions</span>
|
||||
<i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
|
||||
</button>
|
||||
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">`;
|
||||
|
||||
$.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({
|
||||
"select": {
|
||||
"style": "single",
|
||||
"blurable": true
|
||||
},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0,
|
||||
"buttons": [
|
||||
{
|
||||
"text": "Column visibility",
|
||||
"extend": "colvis",
|
||||
"columns": ":not(.noVis)"
|
||||
//{{- if .LoggedUser.HasPermission "manage_groups"}}
|
||||
numActions++;
|
||||
actions+=`<div class="menu-item px-3">
|
||||
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a>
|
||||
</div>`
|
||||
numActions++;
|
||||
actions+=`<div class="menu-item px-3">
|
||||
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a>
|
||||
</div>`
|
||||
//{{- end}}
|
||||
if (numActions > 0){
|
||||
actions+=`</div>`;
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
],
|
||||
deferRender: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
colReorder: {
|
||||
enable: true,
|
||||
fixedColumnsLeft: 1
|
||||
},
|
||||
stateLoadParams: function (settings, data) {
|
||||
if (data.search.search){
|
||||
const filterSearch = document.querySelector('[data-table-filter="search"]');
|
||||
filterSearch.value = data.search.search;
|
||||
}
|
||||
},
|
||||
language: {
|
||||
info: $.t('datatable.info'),
|
||||
infoEmpty: $.t('datatable.info_empty'),
|
||||
infoFiltered: $.t('datatable.info_filtered'),
|
||||
loadingRecords: "",
|
||||
processing: $.t('datatable.processing'),
|
||||
zeroRecords: "",
|
||||
emptyTable: $.t('datatable.no_records')
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
initComplete: function(settings, json) {
|
||||
$('#loader').addClass("d-none");
|
||||
$('#card_content').removeClass("d-none");
|
||||
let api = $.fn.dataTable.Api(settings);
|
||||
api.columns.adjust().draw("page");
|
||||
drawAction();
|
||||
}
|
||||
],
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
"className": "noVis"
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"render": $.fn.dataTable.render.ellipsis(100, true)
|
||||
},
|
||||
],
|
||||
"scrollX": false,
|
||||
"scrollY": false,
|
||||
"responsive": true,
|
||||
"language": {
|
||||
"emptyTable": "No group defined"
|
||||
},
|
||||
"order": [[0, 'asc']]
|
||||
});
|
||||
});
|
||||
|
||||
new $.fn.dataTable.FixedHeader( table );
|
||||
dt.on('draw', drawAction);
|
||||
dt.on('column-reorder', function(e, settings, details){
|
||||
drawAction();
|
||||
});
|
||||
}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "manage_groups"}}
|
||||
table.button().add(0,'delete');
|
||||
table.button().add(0,'edit');
|
||||
table.button().add(0,'add');
|
||||
function drawAction() {
|
||||
KTMenu.createInstances();
|
||||
handleRowActions();
|
||||
$('#table_body').localize();
|
||||
}
|
||||
|
||||
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
|
||||
function handleColVisibilityCheckbox(el, index) {
|
||||
el.off("change");
|
||||
el.prop('checked', dt.column(index).visible());
|
||||
el.on("change", function(e){
|
||||
dt.column(index).visible($(this).is(':checked'));
|
||||
dt.draw('page');
|
||||
});
|
||||
}
|
||||
|
||||
table.on('select deselect', function () {
|
||||
var selectedRows = table.rows({ selected: true }).count();
|
||||
table.button('delete:name').enable(selectedRows == 1);
|
||||
table.button('edit:name').enable(selectedRows == 1);
|
||||
});
|
||||
{{end}}
|
||||
var handleDatatableActions = function () {
|
||||
const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
|
||||
filterSearch.off("keyup");
|
||||
filterSearch.on('keyup', function (e) {
|
||||
dt.rows().deselect();
|
||||
dt.search(e.target.value, true, false).draw();
|
||||
});
|
||||
handleColVisibilityCheckbox($('#checkColMembers'), 1);
|
||||
handleColVisibilityCheckbox($('#checkColDesc'), 2);
|
||||
}
|
||||
|
||||
function handleRowActions() {
|
||||
const editButtons = document.querySelectorAll('[data-share-table-action="edit_row"]');
|
||||
editButtons.forEach(d => {
|
||||
let el = $(d);
|
||||
el.off("click");
|
||||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
let rowData = dt.row(e.target.closest('tr')).data();
|
||||
window.location.replace('{{.GroupURL}}' + "/" + encodeURIComponent(rowData['name']));
|
||||
});
|
||||
});
|
||||
|
||||
const deleteButtons = document.querySelectorAll('[data-share-table-action="delete_row"]');
|
||||
deleteButtons.forEach(d => {
|
||||
let el = $(d);
|
||||
el.off("click");
|
||||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
const parent = e.target.closest('tr');
|
||||
deleteAction(dt.row(parent).data()['name']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: function () {
|
||||
initDatatable();
|
||||
handleDatatableActions();
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
$(document).on("i18nshow", function(){
|
||||
datatable.init();
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
{{- end}}
|
|
@ -33,10 +33,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div id="card_content" class="d-none">
|
||||
<div class="d-flex flex-stack flex-wrap mb-5">
|
||||
<div class="d-flex align-items-center position-relative my-2">
|
||||
<i class="ki-duotone ki-magnifier fs-1 position-absolute ms-6">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
</i>
|
||||
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
|
||||
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
|
||||
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
|
||||
</div>
|
||||
|
@ -48,8 +45,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</button>
|
||||
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4" data-kt-menu="true">
|
||||
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
|
||||
<input type="checkbox" class="form-check-input" value="" id="checkColStatus" />
|
||||
<label class="form-check-label" for="checkColStatus">
|
||||
<input type="checkbox" class="form-check-input" value="" id="checkColMembers" />
|
||||
<label class="form-check-label" for="checkColMembers">
|
||||
<span data-i18n="user.status" class="text-gray-800 fs-6">Status</span>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -510,7 +507,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
loadingRecords: "",
|
||||
processing: $.t('datatable.processing'),
|
||||
zeroRecords: "",
|
||||
emptyTable: $.t('share.no_share')
|
||||
emptyTable: $.t('datatable.no_records')
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
initComplete: function(settings, json) {
|
||||
|
|
|
@ -21,10 +21,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div class="card-header pt-8">
|
||||
<div class="card-title">
|
||||
<div class="d-flex align-items-center position-relative my-1">
|
||||
<i class="ki-duotone ki-magnifier fs-1 position-absolute ms-6">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
</i>
|
||||
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
|
||||
<input name="search" data-i18n="[placeholder]general.search" type="text" data-kt-filemanager-table-filter="search" class="form-control rounded-1 w-250px ps-15" placeholder="Search Files & Folders" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -32,10 +32,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<div id="card_content" class="d-none">
|
||||
<div class="d-flex flex-stack flex-wrap mb-5">
|
||||
<div class="d-flex align-items-center position-relative my-2">
|
||||
<i class="ki-duotone ki-magnifier fs-1 position-absolute ms-6">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
</i>
|
||||
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
|
||||
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
|
||||
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
|
||||
</div>
|
||||
|
@ -77,7 +74,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</div>
|
||||
<div class="modal-body fs-5">
|
||||
<div id="readShare">
|
||||
<div class="mb-5">
|
||||
<div class="mb-3">
|
||||
<h4 data-i18n="share.link_single_title">Single zip file</h4>
|
||||
<p data-i18n="share.link_single_desc">You can download shared content as a single zip file</p>
|
||||
<div class="d-flex">
|
||||
|
@ -98,7 +95,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mb-5">
|
||||
<div class="mb-3 mt-10">
|
||||
<h4 data-i18n="share.link_dir_title">Single directory</h4>
|
||||
<p data-i18n="share.link_dir_desc">If the share consists of a single directory you can browse and download files</p>
|
||||
<button id="readBrowseLinkCopy" data-clipboard-target="#readBrowseLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
|
||||
|
@ -117,7 +114,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</a>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<div class="mt-10">
|
||||
<h4 data-i18n="share.link_uncompressed_title">Uncompressed file</h4>
|
||||
<p data-i18n="share.link_uncompressed_desc">If the share consists of a single file you can download it uncompressed</p>
|
||||
<button id="readUncompressedLinkCopy" data-clipboard-target="#readUncompressedLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
|
||||
|
|
Loading…
Reference in a new issue