瀏覽代碼

sftpd actions: execute defined command on error too

add a new field inside the notification to indicate if an error is
detected
Nicola Murino 5 年之前
父節點
當前提交
94b46e57f1
共有 6 個文件被更改,包括 38 次插入26 次删除
  1. 4 1
      docs/custom-actions.md
  2. 2 2
      sftpd/handler.go
  3. 7 7
      sftpd/internal_test.go
  4. 11 2
      sftpd/sftpd.go
  5. 5 5
      sftpd/ssh_cmd.go
  6. 9 9
      sftpd/transfer.go

+ 4 - 1
docs/custom-actions.md

@@ -2,7 +2,8 @@
 
 The `actions` struct inside the "sftpd" configuration section allows to configure the actions for file operations and SSH commands.
 
-Actions will not be executed if an error is detected, and so a partial file is uploaded or an SSH command is not successfully completed. The `upload` condition includes both uploads to new files and overwrite of existing files. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`.
+The `upload` condition includes both uploads to new files and overwrite of existing files. The `ssh_cmd` condition will be triggered after a command is successfully executed via SSH. `scp` will trigger the `download` and `upload` conditions and not `ssh_cmd`.
+The notification will indicate if an error is detected and so, for example, a partial file is uploaded.
 
 The `command`, if defined, is invoked with the following arguments:
 
@@ -23,6 +24,7 @@ The `command` can also read the following environment variables:
 - `SFTPGO_ACTION_FS_PROVIDER`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend
 - `SFTPGO_ACTION_BUCKET`, non-empty for S3 and GCS backends
 - `SFTPGO_ACTION_ENDPOINT`, non-empty for S3 backend if configured
+- `SFTPGO_ACTION_STATUS`, integer. 0 means an error occurred. 1 means no error
 
 Previous global environment variables aren't cleared when the script is called.
 The `command` must finish within 30 seconds.
@@ -38,6 +40,7 @@ The `http_notification_url`, if defined, will be invoked as HTTP POST. The reque
 - `fs_provider`, `0` for local filesystem, `1` for S3 backend, `2` for Google Cloud Storage (GCS) backend
 - `bucket`, not null for S3 and GCS backends
 - `endpoint`, not null for S3 backend if configured
+- `status`, integer. 0 means an error occurred. 1 means no error
 
 
 The HTTP request is executed with a 15-second timeout.

+ 2 - 2
sftpd/handler.go

@@ -335,7 +335,7 @@ func (c Connection) handleSFTPRename(sourcePath string, targetPath string, reque
 		return vfs.GetSFTPError(c.fs, err)
 	}
 	logger.CommandLog(renameLogSender, sourcePath, targetPath, c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "")
-	go executeAction(newActionNotification(c.User, operationRename, sourcePath, targetPath, "", 0))
+	go executeAction(newActionNotification(c.User, operationRename, sourcePath, targetPath, "", 0, nil))
 	return nil
 }
 
@@ -443,7 +443,7 @@ func (c Connection) handleSFTPRemove(filePath string, request *sftp.Request) err
 	if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
 		dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false)
 	}
-	go executeAction(newActionNotification(c.User, operationDelete, filePath, "", "", fi.Size()))
+	go executeAction(newActionNotification(c.User, operationDelete, filePath, "", "", fi.Size(), nil))
 
 	return sftp.ErrSSHFxOk
 }

+ 7 - 7
sftpd/internal_test.go

@@ -131,7 +131,7 @@ func TestNewActionNotification(t *testing.T) {
 	user.FsConfig.GCSConfig = vfs.GCSFsConfig{
 		Bucket: "gcsbucket",
 	}
-	a := newActionNotification(user, operationDownload, "path", "target", "", 123)
+	a := newActionNotification(user, operationDownload, "path", "target", "", 123, nil)
 	if a.Username != "username" {
 		t.Errorf("unexpected username")
 	}
@@ -142,7 +142,7 @@ func TestNewActionNotification(t *testing.T) {
 		t.Errorf("unexpected endpoint")
 	}
 	user.FsConfig.Provider = 1
-	a = newActionNotification(user, operationDownload, "path", "target", "", 123)
+	a = newActionNotification(user, operationDownload, "path", "target", "", 123, nil)
 	if a.Bucket != "s3bucket" {
 		t.Errorf("unexpected s3 bucket")
 	}
@@ -150,7 +150,7 @@ func TestNewActionNotification(t *testing.T) {
 		t.Errorf("unexpected endpoint")
 	}
 	user.FsConfig.Provider = 2
-	a = newActionNotification(user, operationDownload, "path", "target", "", 123)
+	a = newActionNotification(user, operationDownload, "path", "target", "", 123, nil)
 	if a.Bucket != "gcsbucket" {
 		t.Errorf("unexpected gcs bucket")
 	}
@@ -173,17 +173,17 @@ func TestWrongActions(t *testing.T) {
 	user := dataprovider.User{
 		Username: "username",
 	}
-	err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0))
+	err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
 	if err == nil {
 		t.Errorf("action with bad command must fail")
 	}
-	err = executeAction(newActionNotification(user, operationDelete, "path", "", "", 0))
+	err = executeAction(newActionNotification(user, operationDelete, "path", "", "", 0, nil))
 	if err != nil {
 		t.Errorf("action not configured must silently fail")
 	}
 	actions.Command = ""
 	actions.HTTPNotificationURL = "http://foo\x7f.com/"
-	err = executeAction(newActionNotification(user, operationDownload, "path", "", "", 0))
+	err = executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
 	if err == nil {
 		t.Errorf("action with bad url must fail")
 	}
@@ -200,7 +200,7 @@ func TestActionHTTP(t *testing.T) {
 	user := dataprovider.User{
 		Username: "username",
 	}
-	err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0))
+	err := executeAction(newActionNotification(user, operationDownload, "path", "", "", 0, nil))
 	if err != nil {
 		t.Errorf("unexpected error: %v", err)
 	}

+ 11 - 2
sftpd/sftpd.go

@@ -138,17 +138,23 @@ type actionNotification struct {
 	FsProvider int    `json:"fs_provider"`
 	Bucket     string `json:"bucket,omitempty"`
 	Endpoint   string `json:"endpoint,omitempty"`
+	Status     int    `json:"status"`
 }
 
-func newActionNotification(user dataprovider.User, operation, filePath, target, sshCmd string, fileSize int64) actionNotification {
+func newActionNotification(user dataprovider.User, operation, filePath, target, sshCmd string, fileSize int64,
+	err error) actionNotification {
 	bucket := ""
 	endpoint := ""
+	status := 1
 	if user.FsConfig.Provider == 1 {
 		bucket = user.FsConfig.S3Config.Bucket
 		endpoint = user.FsConfig.S3Config.Endpoint
 	} else if user.FsConfig.Provider == 2 {
 		bucket = user.FsConfig.GCSConfig.Bucket
 	}
+	if err != nil {
+		status = 0
+	}
 	return actionNotification{
 		Action:     operation,
 		Username:   user.Username,
@@ -159,6 +165,7 @@ func newActionNotification(user dataprovider.User, operation, filePath, target,
 		FsProvider: user.FsConfig.Provider,
 		Bucket:     bucket,
 		Endpoint:   endpoint,
+		Status:     status,
 	}
 }
 
@@ -176,7 +183,9 @@ func (a *actionNotification) AsEnvVars() []string {
 		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_ENDPOINT=%v", a.Endpoint),
+		fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", a.Status),
+	}
 }
 
 func init() {

+ 5 - 5
sftpd/ssh_cmd.go

@@ -444,17 +444,17 @@ func (c *sshCommand) sendExitStatus(err error) {
 	}
 	c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
 	c.connection.channel.Close()
-	metrics.SSHCommandCompleted(err)
 	// for scp we notify single uploads/downloads
-	if err == nil && c.command != "scp" {
+	if c.command != "scp" {
+		metrics.SSHCommandCompleted(err)
 		realPath := c.getDestPath()
 		if len(realPath) > 0 {
-			p, err := c.connection.fs.ResolvePath(realPath)
-			if err == nil {
+			p, e := c.connection.fs.ResolvePath(realPath)
+			if e == nil {
 				realPath = p
 			}
 		}
-		go executeAction(newActionNotification(c.connection.User, operationSSHCmd, realPath, "", c.command, 0))
+		go executeAction(newActionNotification(c.connection.User, operationSSHCmd, realPath, "", c.command, 0, err))
 	}
 }
 

+ 9 - 9
sftpd/transfer.go

@@ -147,16 +147,16 @@ func (t *Transfer) Close() error {
 			}
 		}
 	}
-	if t.transferError == nil {
-		elapsed := time.Since(t.start).Nanoseconds() / 1000000
-		if t.transferType == transferDownload {
-			logger.TransferLog(downloadLogSender, t.path, elapsed, t.bytesSent, t.user.Username, t.connectionID, t.protocol)
-			go executeAction(newActionNotification(t.user, operationDownload, t.path, "", "", t.bytesSent))
-		} else {
-			logger.TransferLog(uploadLogSender, t.path, elapsed, t.bytesReceived, t.user.Username, t.connectionID, t.protocol)
-			go executeAction(newActionNotification(t.user, operationUpload, t.path, "", "", t.bytesReceived+t.minWriteOffset))
-		}
+	elapsed := time.Since(t.start).Nanoseconds() / 1000000
+	if t.transferType == transferDownload {
+		logger.TransferLog(downloadLogSender, t.path, elapsed, t.bytesSent, t.user.Username, t.connectionID, t.protocol)
+		go executeAction(newActionNotification(t.user, operationDownload, t.path, "", "", t.bytesSent, t.transferError))
 	} else {
+		logger.TransferLog(uploadLogSender, t.path, elapsed, t.bytesReceived, t.user.Username, t.connectionID, t.protocol)
+		go executeAction(newActionNotification(t.user, operationUpload, t.path, "", "", t.bytesReceived+t.minWriteOffset,
+			t.transferError))
+	}
+	if t.transferError != nil {
 		logger.Warn(logSender, t.connectionID, "transfer error: %v, path: %#v", t.transferError, t.path)
 		if err == nil {
 			err = t.transferError