|
@@ -34,8 +34,29 @@ type ProtocolActions struct {
|
|
|
Hook string `json:"hook" mapstructure:"hook"`
|
|
|
}
|
|
|
|
|
|
-// actionNotification defines a notification for a Protocol Action
|
|
|
-type actionNotification struct {
|
|
|
+var actionHandler ActionHandler = defaultActionHandler{}
|
|
|
+
|
|
|
+// InitializeActionHandler lets the user choose an action handler implementation.
|
|
|
+//
|
|
|
+// Do NOT call this function after application initialization.
|
|
|
+func InitializeActionHandler(handler ActionHandler) {
|
|
|
+ actionHandler = handler
|
|
|
+}
|
|
|
+
|
|
|
+// SSHCommandActionNotification executes the defined action for the specified SSH command.
|
|
|
+func SSHCommandActionNotification(user *dataprovider.User, filePath, target, sshCmd string, err error) {
|
|
|
+ notification := newActionNotification(user, operationSSHCmd, filePath, target, sshCmd, ProtocolSSH, 0, err)
|
|
|
+
|
|
|
+ go actionHandler.Handle(notification) // nolint:errcheck
|
|
|
+}
|
|
|
+
|
|
|
+// ActionHandler handles a notification for a Protocol Action.
|
|
|
+type ActionHandler interface {
|
|
|
+ Handle(notification ActionNotification) error
|
|
|
+}
|
|
|
+
|
|
|
+// ActionNotification defines a notification for a Protocol Action.
|
|
|
+type ActionNotification struct {
|
|
|
Action string `json:"action"`
|
|
|
Username string `json:"username"`
|
|
|
Path string `json:"path"`
|
|
@@ -49,29 +70,29 @@ type actionNotification struct {
|
|
|
Protocol string `json:"protocol"`
|
|
|
}
|
|
|
|
|
|
-// SSHCommandActionNotification executes the defined action for the specified SSH command
|
|
|
-func SSHCommandActionNotification(user *dataprovider.User, filePath, target, sshCmd string, err error) {
|
|
|
- action := newActionNotification(user, operationSSHCmd, filePath, target, sshCmd, ProtocolSSH, 0, err)
|
|
|
- go action.execute() //nolint:errcheck
|
|
|
-}
|
|
|
-
|
|
|
-func newActionNotification(user *dataprovider.User, operation, filePath, target, sshCmd, protocol string, fileSize int64,
|
|
|
- err error) actionNotification {
|
|
|
- bucket := ""
|
|
|
- endpoint := ""
|
|
|
+func newActionNotification(
|
|
|
+ user *dataprovider.User,
|
|
|
+ operation, filePath, target, sshCmd, protocol string,
|
|
|
+ fileSize int64,
|
|
|
+ err error,
|
|
|
+) ActionNotification {
|
|
|
+ var bucket, endpoint string
|
|
|
status := 1
|
|
|
+
|
|
|
if user.FsConfig.Provider == dataprovider.S3FilesystemProvider {
|
|
|
bucket = user.FsConfig.S3Config.Bucket
|
|
|
endpoint = user.FsConfig.S3Config.Endpoint
|
|
|
} else if user.FsConfig.Provider == dataprovider.GCSFilesystemProvider {
|
|
|
bucket = user.FsConfig.GCSConfig.Bucket
|
|
|
}
|
|
|
+
|
|
|
if err == ErrQuotaExceeded {
|
|
|
status = 2
|
|
|
} else if err != nil {
|
|
|
status = 0
|
|
|
}
|
|
|
- return actionNotification{
|
|
|
+
|
|
|
+ return ActionNotification{
|
|
|
Action: operation,
|
|
|
Username: user.Username,
|
|
|
Path: filePath,
|
|
@@ -86,72 +107,92 @@ func newActionNotification(user *dataprovider.User, operation, filePath, target,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (a *actionNotification) asJSON() []byte {
|
|
|
- res, _ := json.Marshal(a)
|
|
|
- return res
|
|
|
+type defaultActionHandler struct{}
|
|
|
+
|
|
|
+func (h defaultActionHandler) Handle(notification ActionNotification) error {
|
|
|
+ if !utils.IsStringInSlice(notification.Action, Config.Actions.ExecuteOn) {
|
|
|
+ return errUnconfiguredAction
|
|
|
+ }
|
|
|
+
|
|
|
+ if Config.Actions.Hook == "" {
|
|
|
+ logger.Warn(notification.Protocol, "", "Unable to send notification, no hook is defined")
|
|
|
+
|
|
|
+ return errNoHook
|
|
|
+ }
|
|
|
+
|
|
|
+ if strings.HasPrefix(Config.Actions.Hook, "http") {
|
|
|
+ return h.handleHTTP(notification)
|
|
|
+ }
|
|
|
+
|
|
|
+ return h.handleCommand(notification)
|
|
|
}
|
|
|
|
|
|
-func (a *actionNotification) asEnvVars() []string {
|
|
|
- return []string{fmt.Sprintf("SFTPGO_ACTION=%v", a.Action),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", a.Username),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_PATH=%v", a.Path),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", a.TargetPath),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", a.SSHCmd),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", a.FileSize),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", a.FsProvider),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", a.Bucket),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", a.Endpoint),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", a.Status),
|
|
|
- fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", a.Protocol),
|
|
|
+func (h defaultActionHandler) handleHTTP(notification ActionNotification) error {
|
|
|
+ u, err := url.Parse(Config.Actions.Hook)
|
|
|
+ if err != nil {
|
|
|
+ logger.Warn(notification.Protocol, "", "Invalid hook %#v for operation %#v: %v", Config.Actions.Hook, notification.Action, err)
|
|
|
+
|
|
|
+ return err
|
|
|
}
|
|
|
+
|
|
|
+ startTime := time.Now()
|
|
|
+ respCode := 0
|
|
|
+
|
|
|
+ httpClient := httpclient.GetHTTPClient()
|
|
|
+
|
|
|
+ var b bytes.Buffer
|
|
|
+ _ = json.NewEncoder(&b).Encode(notification)
|
|
|
+
|
|
|
+ resp, err := httpClient.Post(u.String(), "application/json", &b)
|
|
|
+ if err == nil {
|
|
|
+ respCode = resp.StatusCode
|
|
|
+ resp.Body.Close()
|
|
|
+
|
|
|
+ if respCode != http.StatusOK {
|
|
|
+ err = errUnexpectedHTTResponse
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.Debug(notification.Protocol, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v", notification.Action, u.String(), respCode, time.Since(startTime), err)
|
|
|
+
|
|
|
+ return err
|
|
|
}
|
|
|
|
|
|
-func (a *actionNotification) executeNotificationCommand() error {
|
|
|
+func (h defaultActionHandler) handleCommand(notification ActionNotification) error {
|
|
|
if !filepath.IsAbs(Config.Actions.Hook) {
|
|
|
err := fmt.Errorf("invalid notification command %#v", Config.Actions.Hook)
|
|
|
- logger.Warn(a.Protocol, "", "unable to execute notification command: %v", err)
|
|
|
+ logger.Warn(notification.Protocol, "", "unable to execute notification command: %v", err)
|
|
|
+
|
|
|
return err
|
|
|
}
|
|
|
+
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
defer cancel()
|
|
|
- cmd := exec.CommandContext(ctx, Config.Actions.Hook, a.Action, a.Username, a.Path, a.TargetPath, a.SSHCmd)
|
|
|
- cmd.Env = append(os.Environ(), a.asEnvVars()...)
|
|
|
+
|
|
|
+ cmd := exec.CommandContext(ctx, Config.Actions.Hook, notification.Action, notification.Username, notification.Path, notification.TargetPath, notification.SSHCmd)
|
|
|
+ cmd.Env = append(os.Environ(), notificationAsEnvVars(notification)...)
|
|
|
+
|
|
|
startTime := time.Now()
|
|
|
err := cmd.Run()
|
|
|
- logger.Debug(a.Protocol, "", "executed command %#v with arguments: %#v, %#v, %#v, %#v, %#v, elapsed: %v, error: %v",
|
|
|
- Config.Actions.Hook, a.Action, a.Username, a.Path, a.TargetPath, a.SSHCmd, time.Since(startTime), err)
|
|
|
+
|
|
|
+ logger.Debug(notification.Protocol, "", "executed command %#v with arguments: %#v, %#v, %#v, %#v, %#v, elapsed: %v, error: %v",
|
|
|
+ Config.Actions.Hook, notification.Action, notification.Username, notification.Path, notification.TargetPath, notification.SSHCmd, time.Since(startTime), err)
|
|
|
+
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
-func (a *actionNotification) execute() error {
|
|
|
- if !utils.IsStringInSlice(a.Action, Config.Actions.ExecuteOn) {
|
|
|
- return errUnconfiguredAction
|
|
|
- }
|
|
|
- if len(Config.Actions.Hook) == 0 {
|
|
|
- logger.Warn(a.Protocol, "", "Unable to send notification, no hook is defined")
|
|
|
- return errNoHook
|
|
|
- }
|
|
|
- if strings.HasPrefix(Config.Actions.Hook, "http") {
|
|
|
- var url *url.URL
|
|
|
- url, err := url.Parse(Config.Actions.Hook)
|
|
|
- if err != nil {
|
|
|
- logger.Warn(a.Protocol, "", "Invalid hook %#v for operation %#v: %v", Config.Actions.Hook, a.Action, err)
|
|
|
- return err
|
|
|
- }
|
|
|
- startTime := time.Now()
|
|
|
- httpClient := httpclient.GetHTTPClient()
|
|
|
- resp, err := httpClient.Post(url.String(), "application/json", bytes.NewBuffer(a.asJSON()))
|
|
|
- respCode := 0
|
|
|
- if err == nil {
|
|
|
- respCode = resp.StatusCode
|
|
|
- resp.Body.Close()
|
|
|
- if respCode != http.StatusOK {
|
|
|
- err = errUnexpectedHTTResponse
|
|
|
- }
|
|
|
- }
|
|
|
- logger.Debug(a.Protocol, "", "notified operation %#v to URL: %v status code: %v, elapsed: %v err: %v",
|
|
|
- a.Action, url.String(), respCode, time.Since(startTime), err)
|
|
|
- return err
|
|
|
+func notificationAsEnvVars(notification ActionNotification) []string {
|
|
|
+ return []string{
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION=%v", notification.Action),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_USERNAME=%v", notification.Username),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_PATH=%v", notification.Path),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_TARGET=%v", notification.TargetPath),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_SSH_CMD=%v", notification.SSHCmd),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_FILE_SIZE=%v", notification.FileSize),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_FS_PROVIDER=%v", notification.FsProvider),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_BUCKET=%v", notification.Bucket),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", notification.Endpoint),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", notification.Status),
|
|
|
+ fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", notification.Protocol),
|
|
|
}
|
|
|
- return a.executeNotificationCommand()
|
|
|
}
|