mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 17:10:28 +00:00
sftpd actions: execute defined command on error too
add a new field inside the notification to indicate if an error is detected
This commit is contained in:
parent
9046acbe68
commit
94b46e57f1
6 changed files with 38 additions and 26 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue