eventmanager: add support for pre-* actions

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-01-01 17:59:41 +01:00
parent 6cebc037a0
commit 2611dd2c98
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
17 changed files with 361 additions and 59 deletions

View file

@ -28,9 +28,9 @@ For cloud backends directories are virtual, they are created implicitly when you
The notification will indicate if an error is detected and so, for example, a partial file is uploaded.
The `pre-delete` action, if defined, will be called just before files deletion. If the external command completes with a zero exit status or the HTTP notification response code is `200` then SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute the hook defined for the `delete` action.
The `pre-delete` action, if defined, will be called just before files deletion. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute the hook defined for the `delete` action.
The `pre-download` and `pre-upload` actions, will be called before downloads and uploads. If the external command completes with a zero exit status or the HTTP notification response code is `200` then SFTPGo allows the operation, otherwise the client will get a permission denied error.
The `pre-download` and `pre-upload` actions, will be called before downloads and uploads. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will allow the operation, otherwise the client will get a permission denied error.
If the `hook` defines a path to an external program, then this program can read the following environment variables:

View file

@ -63,7 +63,7 @@ Actions are executed in a sequential order except for sync actions that are exec
- `Stop on failure`, the next action will not be executed if the current one fails.
- `Failure action`, this action will be executed only if at least another one fails. :warning: Please note that a failure action isn't executed if the event fails, for example if a download fails the main action is executed. The failure action is executed only if one of the non-failure actions associated to a rule fails.
- `Execute sync`, for upload events, you can execute the action synchronously. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your action have completed its execution. If your acion takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection.
- `Execute sync`, for upload events, you can execute the action(s) synchronously. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your action have completed its execution. If your acion takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection. For pre-* events at least a sync action is required. If pre-delete sync action(s) completes successfully, SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute any defined `delete` actions. If pre-upload/download action(s) completes successfully, SFTPGo will allow the operation, otherwise the client will get a permission denied error.
If you are running multiple SFTPGo instances connected to the same data provider, you can choose whether to allow simultaneous execution for scheduled actions.

2
go.mod
View file

@ -52,7 +52,7 @@ require (
github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.28.0
github.com/sftpgo/sdk v0.1.3-0.20221217110036-383c1bb50fa0
github.com/shirou/gopsutil/v3 v3.22.11
github.com/shirou/gopsutil/v3 v3.22.12
github.com/spf13/afero v1.9.3
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.14.0

4
go.sum
View file

@ -1453,8 +1453,8 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/sftpgo/sdk v0.1.3-0.20221217110036-383c1bb50fa0 h1:e1OQroqX8SWV06Z270CxG2/v//Wx1026iXKTDRn5J1E=
github.com/sftpgo/sdk v0.1.3-0.20221217110036-383c1bb50fa0/go.mod h1:3GpW3Qy8IHH6kex0ny+Y6ayeYb9OJxz8Pxh3IZgAs2E=
github.com/shirou/gopsutil/v3 v3.22.11 h1:kxsPKS+Eeo+VnEQ2XCaGJepeP6KY53QoRTETx3+1ndM=
github.com/shirou/gopsutil/v3 v3.22.11/go.mod h1:xl0EeL4vXJ+hQMAGN8B9VFpxukEMA0XdevQOe5MZ1oY=
github.com/shirou/gopsutil/v3 v3.22.12 h1:oG0ns6poeUSxf78JtOsfygNWuEHYYz8hnnNg7P04TJs=
github.com/shirou/gopsutil/v3 v3.22.12/go.mod h1:Xd7P1kwZcp5VW52+9XsirIKd/BROzbb2wdX3Kqlz9uI=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=

View file

@ -95,7 +95,8 @@ func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath str
var event *notifier.FsEvent
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
if !hasHook && !hasNotifiersPlugin {
hasRules := eventManager.hasFsRules()
if !hasHook && !hasNotifiersPlugin && !hasRules {
return handleUnconfiguredPreAction(operation)
}
event = newActionNotification(&conn.User, operation, filePath, virtualPath, "", "", "",
@ -103,6 +104,29 @@ func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath str
if hasNotifiersPlugin {
plugin.Handler.NotifyFsEvent(event)
}
if hasRules {
params := EventParams{
Name: event.Username,
Groups: conn.User.Groups,
Event: event.Action,
Status: event.Status,
VirtualPath: event.VirtualPath,
FsPath: event.Path,
VirtualTargetPath: event.VirtualTargetPath,
FsTargetPath: event.TargetPath,
ObjectName: path.Base(event.VirtualPath),
FileSize: event.FileSize,
Protocol: event.Protocol,
IP: event.IP,
Role: event.Role,
Timestamp: event.Timestamp,
Object: nil,
}
executedSync, err := eventManager.handleFsEvent(params)
if executedSync {
return err
}
}
if !hasHook {
return handleUnconfiguredPreAction(operation)
}
@ -124,7 +148,6 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
if hasNotifiersPlugin {
plugin.Handler.NotifyFsEvent(notification)
}
var errRes error
if hasRules {
params := EventParams{
Name: notification.Username,
@ -146,23 +169,23 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
if err != nil {
params.AddError(fmt.Errorf("%q failed: %w", params.Event, err))
}
errRes = eventManager.handleFsEvent(params)
executedSync, err := eventManager.handleFsEvent(params)
if executedSync {
return err
}
}
if hasHook {
if util.Contains(Config.Actions.ExecuteSync, operation) {
if errHook := actionHandler.Handle(notification); errHook != nil {
errRes = errHook
}
} else {
go func() {
startNewHook()
defer hookEnded()
actionHandler.Handle(notification) //nolint:errcheck
}()
return actionHandler.Handle(notification)
}
go func() {
startNewHook()
defer hookEnded()
actionHandler.Handle(notification) //nolint:errcheck
}()
}
return errRes
return nil
}
// ActionHandler handles a notification for a Protocol Action.

View file

@ -315,10 +315,11 @@ func (r *eventRulesContainer) hasFsRules() bool {
return len(r.FsEvents) > 0
}
// handleFsEvent executes the rules actions defined for the specified event
func (r *eventRulesContainer) handleFsEvent(params EventParams) error {
// handleFsEvent executes the rules actions defined for the specified event.
// The boolean parameter indicates whether a sync action was executed
func (r *eventRulesContainer) handleFsEvent(params EventParams) (bool, error) {
if params.Protocol == protocolEventAction {
return nil
return false, nil
}
r.RLock()
@ -353,9 +354,9 @@ func (r *eventRulesContainer) handleFsEvent(params EventParams) error {
}
if len(rulesWithSyncActions) > 0 {
return executeSyncRulesActions(rulesWithSyncActions, params)
return true, executeSyncRulesActions(rulesWithSyncActions, params)
}
return nil
return false, nil
}
// username is populated for user objects
@ -1312,10 +1313,11 @@ func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
}
func replacePathsPlaceholders(paths []string, replacer *strings.Replacer) []string {
for idx := range paths {
paths[idx] = util.CleanPath(replaceWithReplacer(paths[idx], replacer))
results := make([]string, 0, len(paths))
for _, p := range paths {
results = append(results, util.CleanPath(replaceWithReplacer(p, replacer)))
}
return util.RemoveDuplicates(paths, false)
return util.RemoveDuplicates(results, false)
}
func executeDeleteFileFsAction(conn *BaseConnection, item string, info os.FileInfo) error {

View file

@ -4195,6 +4195,213 @@ func TestEventRuleFsActions(t *testing.T) {
assert.NoError(t, err)
}
func TestEventRulePreDelete(t *testing.T) {
movePath := "recycle bin"
a1 := dataprovider.BaseEventAction{
Name: "a1",
Type: dataprovider.ActionTypeFilesystem,
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionMkdirs,
MkDirs: []string{fmt.Sprintf("/%s/{{VirtualDirPath}}", movePath)},
},
},
}
action1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)
assert.NoError(t, err, string(resp))
a2 := dataprovider.BaseEventAction{
Name: "a2",
Type: dataprovider.ActionTypeFilesystem,
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
{
Key: "/{{VirtualPath}}",
Value: fmt.Sprintf("/%s/{{VirtualPath}}", movePath),
},
},
},
},
}
action2, resp, err := httpdtest.AddEventAction(a2, http.StatusCreated)
assert.NoError(t, err, string(resp))
r1 := dataprovider.EventRule{
Name: "rule1",
Trigger: dataprovider.EventTriggerFsEvent,
Conditions: dataprovider.EventConditions{
FsEvents: []string{"pre-delete"},
},
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action1.Name,
},
Order: 1,
Options: dataprovider.EventActionOptions{
ExecuteSync: true,
},
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 2,
Options: dataprovider.EventActionOptions{
ExecuteSync: true,
},
},
},
}
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
assert.NoError(t, err)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
testDir := "sub dir"
err = client.MkdirAll(testDir)
assert.NoError(t, err)
err = writeSFTPFile(testFileName, 100, client)
assert.NoError(t, err)
err = writeSFTPFile(path.Join(testDir, testFileName), 100, client)
assert.NoError(t, err)
err = client.Remove(testFileName)
assert.NoError(t, err)
err = client.Remove(path.Join(testDir, testFileName))
assert.NoError(t, err)
// check
_, err = client.Stat(testFileName)
assert.ErrorIs(t, err, os.ErrNotExist)
_, err = client.Stat(path.Join(testDir, testFileName))
assert.ErrorIs(t, err, os.ErrNotExist)
_, err = client.Stat(path.Join("/", movePath, testFileName))
assert.NoError(t, err)
_, err = client.Stat(path.Join("/", movePath, testDir, testFileName))
assert.NoError(t, err)
}
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestEventRulePreDownloadUpload(t *testing.T) {
testDir := "/d"
a1 := dataprovider.BaseEventAction{
Name: "a1",
Type: dataprovider.ActionTypeFilesystem,
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionMkdirs,
MkDirs: []string{testDir},
},
},
}
action1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated)
assert.NoError(t, err, string(resp))
a2 := dataprovider.BaseEventAction{
Name: "a2",
Type: dataprovider.ActionTypeFilesystem,
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
{
Key: "/missing source",
Value: "/missing target",
},
},
},
},
}
action2, resp, err := httpdtest.AddEventAction(a2, http.StatusCreated)
assert.NoError(t, err, string(resp))
r1 := dataprovider.EventRule{
Name: "rule1",
Trigger: dataprovider.EventTriggerFsEvent,
Conditions: dataprovider.EventConditions{
FsEvents: []string{"pre-download", "pre-upload"},
},
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action1.Name,
},
Order: 1,
Options: dataprovider.EventActionOptions{
ExecuteSync: true,
},
},
},
}
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
assert.NoError(t, err)
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
// the rule will always succeed, so uploads/downloads will work
err = writeSFTPFile(testFileName, 100, client)
assert.NoError(t, err)
_, err = client.Stat(testDir)
assert.NoError(t, err)
err = client.RemoveDirectory(testDir)
assert.NoError(t, err)
f, err := client.Open(testFileName)
assert.NoError(t, err)
contents := make([]byte, 100)
n, err := io.ReadFull(f, contents)
assert.NoError(t, err)
assert.Equal(t, int(100), n)
err = f.Close()
assert.NoError(t, err)
// now update the rule so that it will always fail
rule1.Actions = []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 1,
Options: dataprovider.EventActionOptions{
ExecuteSync: true,
},
},
}
_, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)
assert.NoError(t, err)
_, err = client.Open(testFileName)
assert.ErrorIs(t, err, os.ErrPermission)
err = client.Remove(testFileName)
assert.NoError(t, err)
err = writeSFTPFile(testFileName, 100, client)
assert.ErrorIs(t, err, os.ErrPermission)
}
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestFsActionCopy(t *testing.T) {
a1 := dataprovider.BaseEventAction{
Name: "a1",

View file

@ -164,8 +164,8 @@ func getFsActionTypeAsString(value int) string {
// TODO: replace the copied strings with shared constants
var (
// SupportedFsEvents defines the supported filesystem events
SupportedFsEvents = []string{"upload", "first-upload", "download", "first-download", "delete", "rename",
"mkdir", "rmdir", "copy", "ssh_cmd"}
SupportedFsEvents = []string{"upload", "pre-upload", "first-upload", "download", "pre-download",
"first-download", "delete", "pre-delete", "rename", "mkdir", "rmdir", "copy", "ssh_cmd"}
// SupportedProviderEvents defines the supported provider events
SupportedProviderEvents = []string{operationAdd, operationUpdate, operationDelete}
// SupportedRuleConditionProtocols defines the supported protcols for rule conditions
@ -176,6 +176,8 @@ var (
actionObjectAdmin, actionObjectAPIKey, actionObjectShare, actionObjectEventRule, actionObjectEventAction}
// SupportedHTTPActionMethods defines the supported methods for HTTP actions
SupportedHTTPActionMethods = []string{http.MethodPost, http.MethodGet, http.MethodPut}
allowedSyncFsEvents = []string{"upload", "pre-upload", "pre-download", "pre-delete"}
mandatorySyncFsEvents = []string{"pre-upload", "pre-download", "pre-delete"}
)
// enum mappings
@ -1076,9 +1078,14 @@ func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error
return util.NewValidationError("sync execution is not supported for failure actions")
}
}
if trigger != EventTriggerFsEvent || !util.Contains(fsEvents, "upload") {
if a.Options.ExecuteSync {
return util.NewValidationError("sync execution is only supported for upload event")
if a.Options.ExecuteSync {
if trigger != EventTriggerFsEvent {
return util.NewValidationError("sync execution is only supported for some filesystem events")
}
for _, ev := range fsEvents {
if !util.Contains(allowedSyncFsEvents, ev) {
return util.NewValidationError("sync execution is only supported for upload and pre-* events")
}
}
}
return nil
@ -1380,6 +1387,7 @@ func (r *EventRule) validate() error {
actionNames := make(map[string]bool)
actionOrders := make(map[int]bool)
failureActions := 0
hasSyncAction := false
for idx := range r.Actions {
if r.Actions[idx].Name == "" {
return util.NewValidationError(fmt.Sprintf("invalid action at position %d, name not specified", idx))
@ -1397,12 +1405,30 @@ func (r *EventRule) validate() error {
if r.Actions[idx].Options.IsFailureAction {
failureActions++
}
if r.Actions[idx].Options.ExecuteSync {
hasSyncAction = true
}
actionNames[r.Actions[idx].Name] = true
actionOrders[r.Actions[idx].Order] = true
}
if len(r.Actions) == failureActions {
return util.NewValidationError("at least a non-failure action is required")
}
if !hasSyncAction {
return r.validateMandatorySyncActions()
}
return nil
}
func (r *EventRule) validateMandatorySyncActions() error {
if r.Trigger != EventTriggerFsEvent {
return nil
}
for _, ev := range r.Conditions.FsEvents {
if util.Contains(mandatorySyncFsEvents, ev) {
return util.NewValidationError(fmt.Sprintf("event %s requires at least a sync action", ev))
}
}
return nil
}

View file

@ -2024,6 +2024,51 @@ func TestEventRuleValidation(t *testing.T) {
_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "at least a non-failure action is required")
rule.Conditions.FsEvents = []string{"upload", "download"}
rule.Actions = []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: "action111",
},
Order: 1,
Options: dataprovider.EventActionOptions{
ExecuteSync: true,
},
},
}
_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "sync execution is only supported for upload and pre-* events")
rule.Conditions.FsEvents = []string{"pre-upload", "download"}
rule.Actions = []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: "action",
},
Order: 1,
Options: dataprovider.EventActionOptions{
ExecuteSync: false,
},
},
}
_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "event pre-upload requires at least a sync action")
rule.Actions = []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: "action",
},
Order: 1,
Options: dataprovider.EventActionOptions{
ExecuteSync: true,
},
},
}
_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "sync execution is only supported for upload and pre-* events")
rule.Trigger = dataprovider.EventTriggerProviderEvent
rule.Actions = []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
@ -2035,7 +2080,6 @@ func TestEventRuleValidation(t *testing.T) {
},
},
}
rule.Trigger = dataprovider.EventTriggerProviderEvent
_, resp, err = httpdtest.AddEventRule(rule, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "at least one provider event is required")

View file

@ -259,7 +259,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
$("body").on("click", ".add_new_group_field_btn", function () {
var index = $(".form_field_groups_outer").find("form_field_groups_outer_row").length;
let index = $(".form_field_groups_outer").find(".form_field_groups_outer_row").length;
while (document.getElementById("idGroup"+index) != null){
index++;
}

View file

@ -788,7 +788,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
$("body").on("click", ".add_new_header_field_btn", function () {
var index = $(".form_field_http_headers_outer").find(".form_field_http_headers_outer_row").length;
let index = $(".form_field_http_headers_outer").find(".form_field_http_headers_outer_row").length;
while (document.getElementById("idHTTPHeaderKey"+index) != null){
index++;
}
@ -815,7 +815,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_query_field_btn", function () {
var index = $(".form_field_http_query_outer").find(".form_field_http_query_outer_row").length;
let index = $(".form_field_http_query_outer").find(".form_field_http_query_outer_row").length;
while (document.getElementById("idHTTPQueryKey"+index) != null){
index++;
}
@ -842,7 +842,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_cmd_env_field_btn", function () {
var index = $(".form_field_cmd_env_outer").find(".form_field_cmd_env_outer_row").length;
let index = $(".form_field_cmd_env_outer").find(".form_field_cmd_env_outer_row").length;
while (document.getElementById("idCMDEnvKey"+index) != null){
index++;
}
@ -869,7 +869,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_data_retention_field_btn", function () {
var index = $(".form_field_data_retention_outer").find(".form_field_data_retention_outer_row").length;
let index = $(".form_field_data_retention_outer").find(".form_field_data_retention_outer_row").length;
while (document.getElementById("idFolderRetentionPath"+index) != null){
index++;
}
@ -903,7 +903,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_fs_rename_field_btn", function () {
var index = $(".form_field_fs_rename_outer").find(".form_field_fs_rename_outer_row").length;
let index = $(".form_field_fs_rename_outer").find(".form_field_fs_rename_outer_row").length;
while (document.getElementById("idFsRenameSource"+index) != null){
index++;
}
@ -930,7 +930,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_fs_copy_field_btn", function () {
var index = $(".form_field_fs_copy_outer").find(".form_field_fs_copy_outer_row").length;
let index = $(".form_field_fs_copy_outer").find(".form_field_fs_copy_outer_row").length;
while (document.getElementById("idFsCopySource"+index) != null){
index++;
}
@ -957,7 +957,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_http_part_field_btn", function () {
var index = $(".form_field_http_part_outer").find(".form_field_http_part_outer_row").length;
let index = $(".form_field_http_part_outer").find(".form_field_http_part_outer_row").length;
while (document.getElementById("idHTTPPartName"+index) != null){
index++;
}

View file

@ -425,7 +425,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<b>Actions</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">One or more actions to execute. The "Execute sync" options is only supported for upload events</h6>
<h6 class="card-title mb-4">One or more actions to execute. The "Execute sync" options is supported for upload events and required for pre-* events</h6>
<div class="form-group row">
<div class="col-md-12 form_field_action_outer">
{{range $idx, $val := .Rule.Actions}}
@ -505,7 +505,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
$("body").on("click", ".add_new_schedule_field_btn", function () {
var index = $(".form_field_schedules_outer").find(".form_field_schedules_outer_row").length;
let index = $(".form_field_schedules_outer").find(".form_field_schedules_outer_row").length;
while (document.getElementById("idScheduleHour"+index) != null){
index++;
}
@ -537,7 +537,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_name_pattern_field_btn", function () {
var index = $(".form_field_names_outer").find(".form_field_names_outer_row").length;
let index = $(".form_field_names_outer").find(".form_field_names_outer_row").length;
while (document.getElementById("idNamePattern"+index) != null){
index++;
}
@ -567,7 +567,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_group_name_pattern_field_btn", function () {
var index = $(".form_field_group_names_outer").find(".form_field_group_names_outer_row").length;
let index = $(".form_field_group_names_outer").find(".form_field_group_names_outer_row").length;
while (document.getElementById("idGroupNamePattern"+index) != null){
index++;
}
@ -597,7 +597,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_role_name_pattern_field_btn", function () {
var index = $(".form_field_role_names_outer").find(".form_field_role_names_outer_row").length;
let index = $(".form_field_role_names_outer").find(".form_field_role_names_outer_row").length;
while (document.getElementById("idRoleNamePattern"+index) != null){
index++;
}
@ -627,7 +627,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_fs_path_pattern_field_btn", function () {
var index = $(".form_field_fs_paths_outer").find("form_field_fs_paths_outer_row").length;
let index = $(".form_field_fs_paths_outer").find(".form_field_fs_paths_outer_row").length;
while (document.getElementById("idFsPathPattern"+index) != null){
index++;
}
@ -657,7 +657,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_action_field_btn", function () {
var index = $(".form_field_action_outer").find("form_field_action_outer_row").length;
let index = $(".form_field_action_outer").find(".form_field_action_outer_row").length;
while (document.getElementById("idActionName"+index) != null){
index++;
}

View file

@ -117,7 +117,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
onFilesystemChanged('{{.Folder.FsConfig.Provider.Name}}');
$("body").on("click", ".add_new_tpl_folder_field_btn", function () {
var index = $(".form_field_tpl_folders_outer").find(".form_field_tpl_folder_outer_row").length;
let index = $(".form_field_tpl_folders_outer").find(".form_field_tpl_folder_outer_row").length;
while (document.getElementById("idTplFolder"+index) != null){
index++;
}

View file

@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{define "shared_user_group"}}
<script type="text/javascript">
$("body").on("click", ".add_new_dirperms_field_btn", function () {
var index = $(".form_field_dirperms_outer").find(".form_field_dirperms_outer_row").length;
let index = $(".form_field_dirperms_outer").find(".form_field_dirperms_outer_row").length;
while (document.getElementById("idSubDirPermsPath"+index) != null){
index++;
}
@ -48,7 +48,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_vfolder_field_btn", function () {
var index = $(".form_field_vfolders_outer").find(".form_field_vfolder_outer_row").length;
let index = $(".form_field_vfolders_outer").find(".form_field_vfolder_outer_row").length;
while (document.getElementById("idVolderPath" + index) != null) {
index++;
}
@ -95,7 +95,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_bwlimit_field_btn", function () {
var index = $(".form_field_bwlimits_outer").find(".form_field_bwlimits_outer_row").length;
let index = $(".form_field_bwlimits_outer").find(".form_field_bwlimits_outer_row").length;
while (document.getElementById("idBandwidthLimitSources"+index) != null){
index++;
}
@ -138,7 +138,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_dtlimit_field_btn", function () {
var index = $(".form_field_dtlimits_outer").find(".form_field_dtlimits_outer_row").length;
let index = $(".form_field_dtlimits_outer").find(".form_field_dtlimits_outer_row").length;
while (document.getElementById("idDataTransferLimitSources"+index) != null){
index++;
}
@ -190,7 +190,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_pattern_field_btn", function () {
var index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
let index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
while (document.getElementById("idPatternPath"+index) != null){
index++;
}

View file

@ -1129,7 +1129,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_pk_field_btn", function () {
var index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
let index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
while (document.getElementById("idPublicKey"+index) != null){
index++;
}
@ -1153,7 +1153,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_tpl_user_field_btn", function () {
var index = $(".form_field_tpl_users_outer").find(".form_field_tpl_user_outer_row").length;
let index = $(".form_field_tpl_users_outer").find(".form_field_tpl_user_outer_row").length;
while (document.getElementById("idTplUsername"+index) != null){
index++;
}

View file

@ -115,7 +115,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script type="text/javascript">
$(document).ready(function () {
$("body").on("click", ".add_new_pk_field_btn", function () {
var index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
let index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
while (document.getElementById("idPublicKey"+index) != null){
index++;
}

View file

@ -199,7 +199,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
});
$("body").on("click", ".add_new_path_field_btn", function () {
var index = $(".form_field_path_outer").find(".form_field_path_outer_row").length;
let index = $(".form_field_path_outer").find(".form_field_path_outer_row").length;
while (document.getElementById("idPath"+index) != null){
index++;
}