From 56bf51277c59b0a07890ffbcb14a088f11942707 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 29 Aug 2022 19:03:31 +0200 Subject: [PATCH] eventmanager placeholders: add StatusString and ErrorString Signed-off-by: Nicola Murino --- docs/eventmanager.md | 4 +- go.mod | 4 +- go.sum | 9 +- internal/acme/acme.go | 4 +- internal/common/actions.go | 8 +- internal/common/eventmanager.go | 184 +++++++++++++++++---------- internal/common/eventmanager_test.go | 118 +++++++++++------ internal/common/protocol_test.go | 98 +++++++++----- templates/webadmin/eventaction.html | 6 + 9 files changed, 285 insertions(+), 150 deletions(-) diff --git a/docs/eventmanager.md b/docs/eventmanager.md index 3f86439f..8ab6458e 100644 --- a/docs/eventmanager.md +++ b/docs/eventmanager.md @@ -23,6 +23,8 @@ The following placeholders are supported: - `{{Name}}`. Username, folder name or admin username for provider actions. - `{{Event}}`. Event name, for example `upload`, `download` for filesystem events or `add`, `update` for provider events. - `{{Status}}`. Status for `upload`, `download` and `ssh_cmd` events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error. +- `{{StatusString}}`. Status as string. Possible values "OK", "KO". +- `{{ErrorString}}`. Error details. Replaced with an empty string if no errors occur. - `{{VirtualPath}}`. Path seen by SFTPGo users, for example `/adir/afile.txt`. - `{{FsPath}}`. Full filesystem path, for example `/user/homedir/adir/afile.txt` or `C:/data/user/homedir/adir/afile.txt` on Windows. - `{{ObjectName}}`. File/directory name, for example `afile.txt` or provider object name. @@ -51,7 +53,7 @@ Actions such as user quota reset, transfer quota reset, data retention check, fo Actions are executed in a sequential order except for sync actions that are executed before the others. For each action associated to a rule you can define the following settings: - `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. +- `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. If you are running multiple SFTPGo instances connected to the same data provider, you can choose whether to allow simultaneous execution for scheduled actions. diff --git a/go.mod b/go.mod index d571be84..63c06e22 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5 github.com/rs/xid v1.4.0 - github.com/rs/zerolog v1.27.0 + github.com/rs/zerolog v1.28.0 github.com/sftpgo/sdk v0.1.2-0.20220828084006-f9e2fffac657 github.com/shirou/gopsutil/v3 v3.22.7 github.com/spf13/afero v1.9.2 @@ -155,7 +155,7 @@ require ( golang.org/x/tools v0.1.12 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect + google.golang.org/genproto v0.0.0-20220829144015-23454907ede3 // indirect google.golang.org/grpc v1.49.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index a259ef97..0d3d5a3d 100644 --- a/go.sum +++ b/go.sum @@ -701,13 +701,12 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5 h1:7PcjxKTsfGXpTMiTNNa1VllbsYSZJN5nhvVEWQMdX8Y= github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= -github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= +github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -1221,8 +1220,8 @@ google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3 h1:4wwmycAWg7WUIFWgzxP6Wumy2GBLxmATgkhgpFnJl2U= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/internal/acme/acme.go b/internal/acme/acme.go index c17c3d08..54176b3a 100644 --- a/internal/acme/acme.go +++ b/internal/acme/acme.go @@ -568,14 +568,14 @@ func (c *Configuration) notifyCertificateRenewal(domain string, err error) { } params := common.EventParams{ Name: domain, + Event: "Certificate renewal", Timestamp: time.Now().UnixNano(), } if err != nil { params.Status = 2 - params.Event = "Certificate renewal failed" + params.AddError(err) } else { params.Status = 1 - params.Event = "Successful certificate renewal" } common.HandleCertificateEvent(params) } diff --git a/internal/common/actions.go b/internal/common/actions.go index e7b0e811..449cbd48 100644 --- a/internal/common/actions.go +++ b/internal/common/actions.go @@ -122,7 +122,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua } var errRes error if hasRules { - errRes = eventManager.handleFsEvent(EventParams{ + params := EventParams{ Name: notification.Username, Event: notification.Action, Status: notification.Status, @@ -136,7 +136,11 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua IP: notification.IP, Timestamp: notification.Timestamp, Object: nil, - }) + } + if err != nil { + params.AddError(fmt.Errorf("%q failed: %w", params.Event, err)) + } + errRes = eventManager.handleFsEvent(params) } if hasHook { if util.Contains(Config.Actions.ExecuteSync, operation) { diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index ab736c40..7e1cb2ce 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -406,21 +406,50 @@ func (r *eventRulesContainer) handleCertificateEvent(params EventParams) { // EventParams defines the supported event parameters type EventParams struct { - Name string - Event string - Status int - VirtualPath string - FsPath string - VirtualTargetPath string - FsTargetPath string - ObjectName string - ObjectType string - FileSize int64 - Protocol string - IP string - Timestamp int64 - Object plugin.Renderer - sender string + Name string + Event string + Status int + VirtualPath string + FsPath string + VirtualTargetPath string + FsTargetPath string + ObjectName string + ObjectType string + FileSize int64 + Protocol string + IP string + Timestamp int64 + Object plugin.Renderer + sender string + updateStatusFromError bool + errors []string +} + +func (p *EventParams) getACopy() *EventParams { + params := *p + params.errors = make([]string, len(p.errors)) + copy(params.errors, p.errors) + return ¶ms +} + +// AddError adds a new error to the event params and update the status if needed +func (p *EventParams) AddError(err error) { + if err == nil { + return + } + if p.updateStatusFromError && p.Status == 1 { + p.Status = 2 + } + p.errors = append(p.errors, err.Error()) +} + +func (p *EventParams) getStatusString() string { + switch p.Status { + case 1: + return "OK" + default: + return "KO" + } } // getUsers returns users with group settings not applied @@ -469,11 +498,18 @@ func (p *EventParams) getStringReplacements(addObjectData bool) []string { "{{Protocol}}", p.Protocol, "{{IP}}", p.IP, "{{Timestamp}}", fmt.Sprintf("%d", p.Timestamp), + "{{StatusString}}", p.getStatusString(), } + if len(p.errors) > 0 { + replacements = append(replacements, "{{ErrorString}}", strings.Join(p.errors, ", ")) + } else { + replacements = append(replacements, "{{ErrorString}}", "") + } + replacements = append(replacements, "{{ObjectData}}", "") if addObjectData { data, err := p.Object.RenderAsJSON(p.Event != operationDelete) if err == nil { - replacements = append(replacements, "{{ObjectData}}", string(data)) + replacements[len(replacements)-1] = string(data) } } return replacements @@ -516,7 +552,7 @@ func getMailAttachments(user dataprovider.User, attachments []string, replacer * err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return nil, err + return nil, fmt.Errorf("error getting email attachments, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) totalSize := int64(0) @@ -596,7 +632,7 @@ func getHTTPRuleActionEndpoint(c dataprovider.EventActionHTTPConfig, replacer *s return c.Endpoint, nil } -func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params EventParams) error { +func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventParams) error { if !c.Password.IsEmpty() { if err := c.Password.TryDecrypt(); err != nil { return fmt.Errorf("unable to decrypt password: %w", err) @@ -653,7 +689,7 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params EventPar return nil } -func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params EventParams) error { +func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *EventParams) error { envVars := make([]string, 0, len(c.EnvVars)) addObjectData := false if params.Object != nil { @@ -686,7 +722,7 @@ func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params Ev return err } -func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params EventParams) error { +func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *EventParams) error { addObjectData := false if params.Object != nil { if strings.Contains(c.Body, "{{ObjectData}}") { @@ -748,7 +784,7 @@ func executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return err + return fmt.Errorf("delete error, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range deletes { @@ -775,7 +811,7 @@ func executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, } func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer, - conditions dataprovider.ConditionOptions, params EventParams, + conditions dataprovider.ConditionOptions, params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -792,6 +828,7 @@ func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer, } executed++ if err = executeDeleteFsActionForUser(deletes, replacer, user); err != nil { + params.AddError(err) failures = append(failures, user.Username) continue } @@ -815,7 +852,7 @@ func executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, use err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return err + return fmt.Errorf("mkdir error, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range dirs { @@ -832,7 +869,7 @@ func executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, use } func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer, - conditions dataprovider.ConditionOptions, params EventParams, + conditions dataprovider.ConditionOptions, params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -874,7 +911,7 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return err + return fmt.Errorf("rename error, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range renames { @@ -899,7 +936,7 @@ func executeExistFsActionForUser(exist []string, replacer *strings.Replacer, err = user.CheckFsRoot(connectionID) defer user.CloseFs() //nolint:errcheck if err != nil { - return err + return fmt.Errorf("existence check error, unable to check root fs for user %q: %w", user.Username, err) } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range exist { @@ -913,7 +950,7 @@ func executeExistFsActionForUser(exist []string, replacer *strings.Replacer, } func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer, - conditions dataprovider.ConditionOptions, params EventParams, + conditions dataprovider.ConditionOptions, params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -931,6 +968,7 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string executed++ if err = executeRenameFsActionForUser(renames, replacer, user); err != nil { failures = append(failures, user.Username) + params.AddError(err) continue } } @@ -945,7 +983,7 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string } func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, conditions dataprovider.ConditionOptions, - params EventParams, + params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -963,6 +1001,7 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit executed++ if err = executeExistFsActionForUser(exist, replacer, user); err != nil { failures = append(failures, user.Username) + params.AddError(err) continue } } @@ -977,7 +1016,7 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit } func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions dataprovider.ConditionOptions, - params EventParams, + params *EventParams, ) error { addObjectData := false replacements := params.getStringReplacements(addObjectData) @@ -1003,25 +1042,25 @@ func executeQuotaResetForUser(user dataprovider.User) error { return err } if !QuotaScans.AddUserQuotaScan(user.Username) { - eventManagerLog(logger.LevelError, "another quota scan is already in progress for user %s", user.Username) - return fmt.Errorf("another quota scan is in progress for user %s", user.Username) + eventManagerLog(logger.LevelError, "another quota scan is already in progress for user %q", user.Username) + return fmt.Errorf("another quota scan is in progress for user %q", user.Username) } defer QuotaScans.RemoveUserQuotaScan(user.Username) numFiles, size, err := user.ScanQuota() if err != nil { - eventManagerLog(logger.LevelError, "error scanning quota for user %s: %v", user.Username, err) - return err + eventManagerLog(logger.LevelError, "error scanning quota for user %q: %v", user.Username, err) + return fmt.Errorf("error scanning quota for user %q: %w", user.Username, err) } err = dataprovider.UpdateUserQuota(&user, numFiles, size, true) if err != nil { - eventManagerLog(logger.LevelError, "error updating quota for user %s: %v", user.Username, err) - return err + eventManagerLog(logger.LevelError, "error updating quota for user %q: %v", user.Username, err) + return fmt.Errorf("error updating quota for user %q: %w", user.Username, err) } return nil } -func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error { +func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error { users, err := params.getUsers() if err != nil { return fmt.Errorf("unable to get users: %w", err) @@ -1031,12 +1070,13 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, for _, user := range users { // if sender is set, the conditions have already been evaluated if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) { - eventManagerLog(logger.LevelDebug, "skipping scheduled quota reset for user %s, name conditions don't match", + eventManagerLog(logger.LevelDebug, "skipping quota reset for user %q, name conditions don't match", user.Username) continue } executed++ if err = executeQuotaResetForUser(user); err != nil { + params.AddError(err) failedResets = append(failedResets, user.Username) continue } @@ -1051,7 +1091,7 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, return nil } -func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error { +func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error { folders, err := params.getFolders() if err != nil { return fmt.Errorf("unable to get folders: %w", err) @@ -1066,7 +1106,8 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions continue } if !QuotaScans.AddVFolderQuotaScan(folder.Name) { - eventManagerLog(logger.LevelError, "another quota scan is already in progress for folder %s", folder.Name) + eventManagerLog(logger.LevelError, "another quota scan is already in progress for folder %q", folder.Name) + params.AddError(fmt.Errorf("another quota scan is already in progress for folder %q", folder.Name)) failedResets = append(failedResets, folder.Name) continue } @@ -1078,13 +1119,15 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions numFiles, size, err := f.ScanQuota() QuotaScans.RemoveVFolderQuotaScan(folder.Name) if err != nil { - eventManagerLog(logger.LevelError, "error scanning quota for folder %s: %v", folder.Name, err) + eventManagerLog(logger.LevelError, "error scanning quota for folder %q: %v", folder.Name, err) + params.AddError(fmt.Errorf("error scanning quota for folder %q: %w", folder.Name, err)) failedResets = append(failedResets, folder.Name) continue } err = dataprovider.UpdateVirtualFolderQuota(&folder, numFiles, size, true) if err != nil { - eventManagerLog(logger.LevelError, "error updating quota for folder %s: %v", folder.Name, err) + eventManagerLog(logger.LevelError, "error updating quota for folder %q: %v", folder.Name, err) + params.AddError(fmt.Errorf("error updating quota for folder %q: %w", folder.Name, err)) failedResets = append(failedResets, folder.Name) } } @@ -1098,7 +1141,7 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions return nil } -func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params EventParams) error { +func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOptions, params *EventParams) error { users, err := params.getUsers() if err != nil { return fmt.Errorf("unable to get users: %w", err) @@ -1115,7 +1158,8 @@ func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOption executed++ err = dataprovider.UpdateUserTransferQuota(&user, 0, 0, true) if err != nil { - eventManagerLog(logger.LevelError, "error updating transfer quota for user %s: %v", user.Username, err) + eventManagerLog(logger.LevelError, "error updating transfer quota for user %q: %v", user.Username, err) + params.AddError(fmt.Errorf("error updating transfer quota for user %q: %w", user.Username, err)) failedResets = append(failedResets, user.Username) } } @@ -1140,18 +1184,18 @@ func executeDataRetentionCheckForUser(user dataprovider.User, folders []dataprov } c := RetentionChecks.Add(check, &user) if c == nil { - eventManagerLog(logger.LevelError, "another retention check is already in progress for user %s", user.Username) - return fmt.Errorf("another retention check is in progress for user %s", user.Username) + eventManagerLog(logger.LevelError, "another retention check is already in progress for user %q", user.Username) + return fmt.Errorf("another retention check is in progress for user %q", user.Username) } if err := c.Start(); err != nil { - eventManagerLog(logger.LevelError, "error checking retention for user %s: %v", user.Username, err) - return err + eventManagerLog(logger.LevelError, "error checking retention for user %q: %v", user.Username, err) + return fmt.Errorf("error checking retention for user %q: %w", user.Username, err) } return nil } func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRetentionConfig, - conditions dataprovider.ConditionOptions, params EventParams, + conditions dataprovider.ConditionOptions, params *EventParams, ) error { users, err := params.getUsers() if err != nil { @@ -1169,6 +1213,7 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete executed++ if err = executeDataRetentionCheckForUser(user, config.Folders); err != nil { failedChecks = append(failedChecks, user.Username) + params.AddError(err) continue } } @@ -1182,29 +1227,37 @@ func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRete return nil } -func executeRuleAction(action dataprovider.BaseEventAction, params EventParams, conditions dataprovider.ConditionOptions) error { +func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, conditions dataprovider.ConditionOptions) error { + var err error + switch action.Type { case dataprovider.ActionTypeHTTP: - return executeHTTPRuleAction(action.Options.HTTPConfig, params) + err = executeHTTPRuleAction(action.Options.HTTPConfig, params) case dataprovider.ActionTypeCommand: - return executeCommandRuleAction(action.Options.CmdConfig, params) + err = executeCommandRuleAction(action.Options.CmdConfig, params) case dataprovider.ActionTypeEmail: - return executeEmailRuleAction(action.Options.EmailConfig, params) + err = executeEmailRuleAction(action.Options.EmailConfig, params) case dataprovider.ActionTypeBackup: - return dataprovider.ExecuteBackup() + err = dataprovider.ExecuteBackup() case dataprovider.ActionTypeUserQuotaReset: - return executeUsersQuotaResetRuleAction(conditions, params) + err = executeUsersQuotaResetRuleAction(conditions, params) case dataprovider.ActionTypeFolderQuotaReset: - return executeFoldersQuotaResetRuleAction(conditions, params) + err = executeFoldersQuotaResetRuleAction(conditions, params) case dataprovider.ActionTypeTransferQuotaReset: - return executeTransferQuotaResetRuleAction(conditions, params) + err = executeTransferQuotaResetRuleAction(conditions, params) case dataprovider.ActionTypeDataRetentionCheck: - return executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions, params) + err = executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions, params) case dataprovider.ActionTypeFilesystem: - return executeFsRuleAction(action.Options.FsConfig, conditions, params) + err = executeFsRuleAction(action.Options.FsConfig, conditions, params) default: - return fmt.Errorf("unsupported action type: %d", action.Type) + err = fmt.Errorf("unsupported action type: %d", action.Type) } + + if err != nil { + err = fmt.Errorf("action %q failed: %w", action.Name, err) + } + params.AddError(err) + return err } func executeSyncRulesActions(rules []dataprovider.EventRule, params EventParams) error { @@ -1212,10 +1265,11 @@ func executeSyncRulesActions(rules []dataprovider.EventRule, params EventParams) for _, rule := range rules { var failedActions []string + paramsCopy := params.getACopy() for _, action := range rule.Actions { if !action.Options.IsFailureAction && action.Options.ExecuteSync { startTime := time.Now() - if err := executeRuleAction(action.BaseEventAction, params, rule.Conditions.Options); err != nil { + if err := executeRuleAction(action.BaseEventAction, paramsCopy, rule.Conditions.Options); err != nil { eventManagerLog(logger.LevelError, "unable to execute sync action %q for rule %q, elapsed %s, err: %v", action.Name, rule.Name, time.Since(startTime), err) failedActions = append(failedActions, action.Name) @@ -1231,7 +1285,7 @@ func executeSyncRulesActions(rules []dataprovider.EventRule, params EventParams) } } // execute async actions if any, including failure actions - go executeRuleAsyncActions(rule, params, failedActions) + go executeRuleAsyncActions(rule, paramsCopy, failedActions) } return errRes @@ -1242,11 +1296,11 @@ func executeAsyncRulesActions(rules []dataprovider.EventRule, params EventParams defer eventManager.removeAsyncTask() for _, rule := range rules { - executeRuleAsyncActions(rule, params, nil) + executeRuleAsyncActions(rule, params.getACopy(), nil) } } -func executeRuleAsyncActions(rule dataprovider.EventRule, params EventParams, failedActions []string) { +func executeRuleAsyncActions(rule dataprovider.EventRule, params *EventParams, failedActions []string) { for _, action := range rule.Actions { if !action.Options.IsFailureAction && !action.Options.ExecuteSync { startTime := time.Now() @@ -1361,9 +1415,9 @@ func (j *eventCronJob) Run() { } }(task.Name) - executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{}) + executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true}) } else { - executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{}) + executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true}) } eventManagerLog(logger.LevelDebug, "execution for scheduled rule %q finished", j.ruleName) } diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go index 4e7c019f..f065f384 100644 --- a/internal/common/eventmanager_test.go +++ b/internal/common/eventmanager_test.go @@ -270,19 +270,19 @@ func TestEventManagerErrors(t *testing.T) { _, err = params.getFolders() assert.Error(t, err) - err = executeUsersQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{}) + err = executeUsersQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeFoldersQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{}) + err = executeFoldersQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{}, EventParams{}) + err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + err = executeDeleteFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + err = executeMkdirFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeRenameFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + err = executeRenameFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) - err = executeExistFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + err = executeExistFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{}) assert.Error(t, err) groupName := "agroup" @@ -362,7 +362,7 @@ func TestEventManagerErrors(t *testing.T) { }, }, } - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "username1", @@ -421,10 +421,10 @@ func TestEventRuleActions(t *testing.T) { Name: actionName, Type: dataprovider.ActionTypeBackup, } - err := executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{}) + err := executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{}) assert.NoError(t, err) action.Type = -1 - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{}) + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{}) assert.Error(t, err) action = dataprovider.BaseEventAction{ @@ -454,12 +454,12 @@ func TestEventRuleActions(t *testing.T) { }, } action.Options.SetEmptySecretsIfNil() - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{}) + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "invalid endpoint") } action.Options.HTTPConfig.Endpoint = fmt.Sprintf("http://%v", httpAddr) - params := EventParams{ + params := &EventParams{ Name: "a", Object: &dataprovider.User{ BaseUser: sdk.BaseUser{ @@ -472,7 +472,7 @@ func TestEventRuleActions(t *testing.T) { action.Options.HTTPConfig.Endpoint = fmt.Sprintf("http://%v/404", httpAddr) err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) if assert.Error(t, err) { - assert.Equal(t, err.Error(), "unexpected status code: 404") + assert.Contains(t, err.Error(), "unexpected status code: 404") } action.Options.HTTPConfig.Endpoint = "http://invalid:1234" err = executeRuleAction(action, params, dataprovider.ConditionOptions{}) @@ -517,7 +517,7 @@ func TestEventRuleActions(t *testing.T) { action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeUserQuotaReset, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -530,7 +530,7 @@ func TestEventRuleActions(t *testing.T) { assert.NoError(t, err) err = os.WriteFile(filepath.Join(user1.GetHomeDir(), "file.txt"), []byte("user"), 0666) assert.NoError(t, err) - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -544,7 +544,7 @@ func TestEventRuleActions(t *testing.T) { assert.Equal(t, int64(4), userGet.UsedQuotaSize) // simulate another quota scan in progress assert.True(t, QuotaScans.AddUserQuotaScan(username1)) - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -554,7 +554,7 @@ func TestEventRuleActions(t *testing.T) { assert.Error(t, err) assert.True(t, QuotaScans.RemoveUserQuotaScan(username1)) // non matching pattern - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "don't match", @@ -578,7 +578,7 @@ func TestEventRuleActions(t *testing.T) { }, }, } - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -622,7 +622,7 @@ func TestEventRuleActions(t *testing.T) { err = os.Chtimes(file4, timeBeforeRetention, timeBeforeRetention) assert.NoError(t, err) - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -637,7 +637,7 @@ func TestEventRuleActions(t *testing.T) { // simulate another check in progress c := RetentionChecks.Add(RetentionCheck{}, &user1) assert.NotNil(t, c) - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -647,7 +647,7 @@ func TestEventRuleActions(t *testing.T) { assert.Error(t, err) RetentionChecks.remove(user1.Username) - err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(dataRetentionAction, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -667,7 +667,7 @@ func TestEventRuleActions(t *testing.T) { }, }, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -677,7 +677,7 @@ func TestEventRuleActions(t *testing.T) { if assert.Error(t, err) { assert.Contains(t, err.Error(), "no existence check executed") } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -686,7 +686,7 @@ func TestEventRuleActions(t *testing.T) { }) assert.NoError(t, err) action.Options.FsConfig.Exist = []string{"/file1.txt", path.Join("/", retentionDir, "file2.txt")} - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -702,7 +702,7 @@ func TestEventRuleActions(t *testing.T) { assert.NoError(t, err) action.Type = dataprovider.ActionTypeTransferQuotaReset - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username1, @@ -715,7 +715,7 @@ func TestEventRuleActions(t *testing.T) { assert.Equal(t, int64(0), userGet.UsedDownloadDataTransfer) assert.Equal(t, int64(0), userGet.UsedUploadDataTransfer) - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -737,7 +737,7 @@ func TestEventRuleActions(t *testing.T) { }, }, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -753,7 +753,7 @@ func TestEventRuleActions(t *testing.T) { Deletes: []string{"/dir1"}, }, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -769,7 +769,7 @@ func TestEventRuleActions(t *testing.T) { Deletes: []string{"/dir1"}, }, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no match", @@ -802,7 +802,7 @@ func TestEventRuleActions(t *testing.T) { action = dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeFolderQuotaReset, } - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, @@ -814,7 +814,7 @@ func TestEventRuleActions(t *testing.T) { assert.NoError(t, err) err = os.WriteFile(filepath.Join(folder1.MappedPath, "file.txt"), []byte("folder"), 0666) assert.NoError(t, err) - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, @@ -828,7 +828,7 @@ func TestEventRuleActions(t *testing.T) { assert.Equal(t, int64(6), folderGet.UsedQuotaSize) // simulate another quota scan in progress assert.True(t, QuotaScans.AddVFolderQuotaScan(foldername1)) - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername1, @@ -838,7 +838,7 @@ func TestEventRuleActions(t *testing.T) { assert.Error(t, err) assert.True(t, QuotaScans.RemoveVFolderQuotaScan(foldername1)) - err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: "no folder match", @@ -920,7 +920,7 @@ func TestGetFileContent(t *testing.T) { } func TestFilesystemActionErrors(t *testing.T) { - err := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, dataprovider.ConditionOptions{}, EventParams{}) + err := executeFsRuleAction(dataprovider.EventActionFilesystemConfig{}, dataprovider.ConditionOptions{}, &EventParams{}) if assert.Error(t, err) { assert.Contains(t, err.Error(), "unsupported filesystem action") } @@ -950,7 +950,7 @@ func TestFilesystemActionErrors(t *testing.T) { Subject: "subject", Body: "body", Attachments: []string{"/file.txt"}, - }, EventParams{ + }, &EventParams{ sender: username, }) assert.Error(t, err) @@ -973,7 +973,7 @@ func TestFilesystemActionErrors(t *testing.T) { Subject: "subject", Body: "body", Attachments: []string{"/file1.txt"}, - }, EventParams{ + }, &EventParams{ sender: username, }) assert.Error(t, err) @@ -1008,7 +1008,7 @@ func TestFilesystemActionErrors(t *testing.T) { }, }, }, - }, EventParams{}, dataprovider.ConditionOptions{ + }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1045,7 +1045,7 @@ func TestFilesystemActionErrors(t *testing.T) { Deletes: []string{"/adir/sub/f.dat"}, }, }, - }, EventParams{}, dataprovider.ConditionOptions{ + }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1071,7 +1071,7 @@ func TestFilesystemActionErrors(t *testing.T) { MkDirs: []string{"/adir/sub/sub1"}, }, }, - }, EventParams{}, dataprovider.ConditionOptions{ + }, &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1119,7 +1119,7 @@ func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) { err = os.MkdirAll(user.GetHomeDir(), os.ModePerm) assert.NoError(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeUserQuotaReset}, - EventParams{}, dataprovider.ConditionOptions{ + &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1129,7 +1129,7 @@ func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) { assert.Error(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeTransferQuotaReset}, - EventParams{}, dataprovider.ConditionOptions{ + &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: username, @@ -1154,7 +1154,7 @@ func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) { assert.NoError(t, err) err = executeRuleAction(dataprovider.BaseEventAction{Type: dataprovider.ActionTypeFolderQuotaReset}, - EventParams{}, dataprovider.ConditionOptions{ + &EventParams{}, dataprovider.ConditionOptions{ Names: []dataprovider.ConditionPattern{ { Pattern: foldername, @@ -1242,3 +1242,37 @@ func TestScheduledActions(t *testing.T) { assert.NoError(t, err) stopEventScheduler() } + +func TestEventParamsCopy(t *testing.T) { + params := EventParams{ + Name: "name", + Event: "event", + Status: 1, + errors: []string{"error1"}, + } + paramsCopy := params.getACopy() + assert.Equal(t, params, *paramsCopy) + params.Name = "name mod" + paramsCopy.Event = "event mod" + paramsCopy.Status = 2 + params.errors = append(params.errors, "error2") + paramsCopy.errors = append(paramsCopy.errors, "error3") + assert.Equal(t, []string{"error1", "error3"}, paramsCopy.errors) + assert.Equal(t, []string{"error1", "error2"}, params.errors) + assert.Equal(t, "name mod", params.Name) + assert.Equal(t, "name", paramsCopy.Name) + assert.Equal(t, "event", params.Event) + assert.Equal(t, "event mod", paramsCopy.Event) + assert.Equal(t, 1, params.Status) + assert.Equal(t, 2, paramsCopy.Status) +} + +func TestEventParamsStatusFromError(t *testing.T) { + params := EventParams{Status: 1} + params.AddError(os.ErrNotExist) + assert.Equal(t, 1, params.Status) + + params = EventParams{Status: 1, updateStatusFromError: true} + params.AddError(os.ErrNotExist) + assert.Equal(t, 2, params.Status) +} diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index 268dbdc8..414ba54f 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -19,6 +19,7 @@ import ( "bytes" "crypto/rand" "encoding/json" + "errors" "fmt" "io" "math" @@ -3064,8 +3065,8 @@ func TestEventRule(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test1@example.com", "test2@example.com"}, - Subject: `New "{{Event}}" from "{{Name}}"`, - Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} Data: {{ObjectData}}", + Subject: `New "{{Event}}" from "{{Name}}" status {{StatusString}}`, + Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}} Data: {{ObjectData}} {{ErrorString}}", }, }, } @@ -3076,7 +3077,7 @@ func TestEventRule(t *testing.T) { EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"failure@example.com"}, Subject: `Failed "{{Event}}" from "{{Name}}"`, - Body: "Fs path {{FsPath}}, protocol: {{Protocol}}, IP: {{IP}}", + Body: "Fs path {{FsPath}}, protocol: {{Protocol}}, IP: {{IP}} {{ErrorString}}", }, }, } @@ -3187,6 +3188,7 @@ func TestEventRule(t *testing.T) { uploadScriptPath := filepath.Join(os.TempDir(), "upload.sh") u := getTestUser() + u.DownloadDataTransfer = 1 user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) movedFileName := "moved.dat" @@ -3230,6 +3232,8 @@ func TestEventRule(t *testing.T) { dirName := "subdir" err = client.Mkdir(dirName) assert.NoError(t, err) + err = client.Mkdir("subdir1") + assert.NoError(t, err) // rule conditions match lastReceivedEmail.reset() err = writeSFTPFileNoCheck(path.Join(dirName, testFileName), size, client) @@ -3247,7 +3251,32 @@ func TestEventRule(t *testing.T) { assert.Len(t, email.To, 2) assert.True(t, util.Contains(email.To, "test1@example.com")) assert.True(t, util.Contains(email.To, "test2@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: New "upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "upload" from "%s" status OK`, user.Username)) + // test the failure action, we download a file that exceeds the transfer quota limit + err = writeSFTPFileNoCheck(path.Join("subdir1", testFileName), 1*1024*1024+65535, client) + assert.NoError(t, err) + lastReceivedEmail.reset() + f, err := client.Open(path.Join("subdir1", testFileName)) + assert.NoError(t, err) + _, err = io.ReadAll(f) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error()) + } + err = f.Close() + assert.Error(t, err) + assert.Eventually(t, func() bool { + return lastReceivedEmail.get().From != "" + }, 3000*time.Millisecond, 100*time.Millisecond) + email = lastReceivedEmail.get() + assert.Len(t, email.To, 2) + assert.True(t, util.Contains(email.To, "test1@example.com")) + assert.True(t, util.Contains(email.To, "test2@example.com")) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s" status KO`, user.Username)) + assert.Contains(t, email.Data, `"download" failed`) + assert.Contains(t, email.Data, common.ErrReadQuotaExceeded.Error()) + _, err = httpdtest.UpdateTransferQuotaUsage(user, "", http.StatusOK) + assert.NoError(t, err) + // remove the upload script to test the failure action err = os.Remove(uploadScriptPath) assert.NoError(t, err) @@ -3260,10 +3289,11 @@ func TestEventRule(t *testing.T) { email = lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "failure@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`action %q failed`, action1.Name)) // now test the download rule lastReceivedEmail.reset() - f, err := client.Open(movedFileName) + f, err = client.Open(movedFileName) assert.NoError(t, err) contents, err := io.ReadAll(f) assert.NoError(t, err) @@ -3277,7 +3307,7 @@ func TestEventRule(t *testing.T) { assert.Len(t, email.To, 2) assert.True(t, util.Contains(email.To, "test1@example.com")) assert.True(t, util.Contains(email.To, "test2@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username)) } _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) @@ -3291,7 +3321,7 @@ func TestEventRule(t *testing.T) { assert.Len(t, email.To, 2) assert.True(t, util.Contains(email.To, "test1@example.com")) assert.True(t, util.Contains(email.To, "test2@example.com")) - assert.Contains(t, string(email.Data), `Subject: New "delete" from "admin"`) + assert.Contains(t, email.Data, `Subject: New "delete" from "admin"`) _, err = httpdtest.RemoveEventRule(rule3, http.StatusOK) assert.NoError(t, err) _, err = httpdtest.RemoveEventAction(action1, http.StatusOK) @@ -3443,7 +3473,7 @@ func TestEventRuleProviderEvents(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test3@example.com")) - assert.Contains(t, string(email.Data), `Subject: New "update" from "admin"`) + assert.Contains(t, email.Data, `Subject: New "update" from "admin"`) } // now delete the script to generate an error lastReceivedEmail.reset() @@ -3458,8 +3488,8 @@ func TestEventRuleProviderEvents(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "failure@example.com")) - assert.Contains(t, string(email.Data), `Subject: Failed "update" from "admin"`) - assert.Contains(t, string(email.Data), fmt.Sprintf("Object name: %s object type: folder", folder.Name)) + assert.Contains(t, email.Data, `Subject: Failed "update" from "admin"`) + assert.Contains(t, email.Data, fmt.Sprintf("Object name: %s object type: folder", folder.Name)) lastReceivedEmail.reset() // generate an error for the failure action smtpCfg = smtp.Config{} @@ -3830,8 +3860,8 @@ func TestEventActionEmailAttachments(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: "upload" from "%s"`, user.Username)) - assert.Contains(t, string(email.Data), "Content-Disposition: attachment") + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, "Content-Disposition: attachment") } } @@ -3929,7 +3959,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username)) lastReceivedEmail.reset() // a new upload will not produce a new notification err = writeSFTPFileNoCheck(testFileName+"_1", 32768, client) @@ -3952,7 +3982,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) { email = lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username)) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username)) // download again lastReceivedEmail.reset() f, err = client.Open(testFileName) @@ -4001,8 +4031,8 @@ func TestEventRuleCertificate(t *testing.T) { Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ Recipients: []string{"test@example.com"}, - Subject: `"{{Event}}"`, - Body: "Domain: {{Name}} Timestamp: {{Timestamp}}", + Subject: `"{{Event}} {{StatusString}}"`, + Body: "Domain: {{Name}} Timestamp: {{Timestamp}} {{ErrorString}}", }, }, } @@ -4051,11 +4081,13 @@ func TestEventRuleCertificate(t *testing.T) { rule2, _, err := httpdtest.AddEventRule(r2, http.StatusCreated) assert.NoError(t, err) + renewalEvent := "Certificate renewal" + common.HandleCertificateEvent(common.EventParams{ Name: "example.com", Timestamp: time.Now().UnixNano(), Status: 1, - Event: "Successful certificate renewal", + Event: renewalEvent, }) assert.Eventually(t, func() bool { return lastReceivedEmail.get().From != "" @@ -4063,24 +4095,28 @@ func TestEventRuleCertificate(t *testing.T) { email := lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), `Subject: "Successful certificate renewal"`) - assert.Contains(t, string(email.Data), `Domain: example.com Timestamp`) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent)) + assert.Contains(t, email.Data, `Domain: example.com Timestamp`) lastReceivedEmail.reset() - common.HandleCertificateEvent(common.EventParams{ + params := common.EventParams{ Name: "example.com", Timestamp: time.Now().UnixNano(), Status: 2, - Event: "Certificate renewal failed", - }) + Event: renewalEvent, + } + errRenew := errors.New("generic renew error") + params.AddError(errRenew) + common.HandleCertificateEvent(params) assert.Eventually(t, func() bool { return lastReceivedEmail.get().From != "" }, 3000*time.Millisecond, 100*time.Millisecond) email = lastReceivedEmail.get() assert.Len(t, email.To, 1) assert.True(t, util.Contains(email.To, "test@example.com")) - assert.Contains(t, string(email.Data), `Subject: "Certificate renewal failed"`) - assert.Contains(t, string(email.Data), `Domain: example.com Timestamp`) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s KO"`, renewalEvent)) + assert.Contains(t, email.Data, `Domain: example.com Timestamp`) + assert.Contains(t, email.Data, errRenew.Error()) _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) assert.NoError(t, err) @@ -4095,7 +4131,7 @@ func TestEventRuleCertificate(t *testing.T) { Name: "example.com", Timestamp: time.Now().UnixNano(), Status: 1, - Event: "Successful certificate renewal", + Event: renewalEvent, }) smtpCfg = smtp.Config{} @@ -4184,7 +4220,7 @@ func TestEventRuleIPBlocked(t *testing.T) { assert.NoError(t, err) lastReceivedEmail.reset() time.Sleep(300 * time.Millisecond) - assert.Empty(t, lastReceivedEmail.get().From, string(lastReceivedEmail.get().Data)) + assert.Empty(t, lastReceivedEmail.get().From, lastReceivedEmail.get().Data) for i := 0; i < 3; i++ { user.Password = "wrong_pwd" @@ -4203,7 +4239,7 @@ func TestEventRuleIPBlocked(t *testing.T) { assert.Len(t, email.To, 2) assert.True(t, util.Contains(email.To, "test3@example.com")) assert.True(t, util.Contains(email.To, "test4@example.com")) - assert.Contains(t, string(email.Data), `Subject: New "IP Blocked"`) + assert.Contains(t, email.Data, `Subject: New "IP Blocked"`) err = dataprovider.DeleteEventRule(rule1.Name, "", "") assert.NoError(t, err) @@ -5305,7 +5341,7 @@ type receivedEmail struct { sync.RWMutex From string To []string - Data []byte + Data string } func (e *receivedEmail) set(from string, to []string, data []byte) { @@ -5314,7 +5350,7 @@ func (e *receivedEmail) set(from string, to []string, data []byte) { e.From = from e.To = to - e.Data = data + e.Data = strings.ReplaceAll(string(data), "=\r\n", "") } func (e *receivedEmail) reset() { @@ -5323,7 +5359,7 @@ func (e *receivedEmail) reset() { e.From = "" e.To = nil - e.Data = nil + e.Data = "" } func (e *receivedEmail) get() receivedEmail { diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html index a92c796b..d2f99dde 100644 --- a/templates/webadmin/eventaction.html +++ b/templates/webadmin/eventaction.html @@ -541,6 +541,12 @@ along with this program. If not, see .

{{`{{Status}}`}} => Status for "upload", "download" and "ssh_cmd" events. 1 means no error, 2 means a generic error occurred, 3 means quota exceeded error.

+

+ {{`{{StatusString}}`}} => Status as string. Possible values "OK", "KO". +

+

+ {{`{{ErrorString}}`}} => Error details. Replaced with an empty string if no errors occur. +

{{`{{VirtualPath}}`}} => Path seen by SFTPGo users, for example "/adir/afile.txt".