eventmanager: add support for pre-* actions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
6cebc037a0
commit
2611dd2c98
17 changed files with 361 additions and 59 deletions
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
2
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue