diff --git a/common/actions.go b/common/actions.go index 8131a347..ac967309 100644 --- a/common/actions.go +++ b/common/actions.go @@ -50,7 +50,7 @@ func InitializeActionHandler(handler ActionHandler) { } // ExecutePreAction executes a pre-* action and returns the result -func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, protocol string, fileSize int64) error { +func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, protocol string, fileSize int64, openFlags int) error { if !utils.IsStringInSlice(operation, Config.Actions.ExecuteOn) { // for pre-delete we execute the internal handling on error, so we must return errUnconfiguredAction. // Other pre action will deny the operation on error so if we have no configuration we must return @@ -60,13 +60,13 @@ func ExecutePreAction(user *dataprovider.User, operation, filePath, virtualPath, } return nil } - notification := newActionNotification(user, operation, filePath, virtualPath, "", "", protocol, fileSize, nil) + notification := newActionNotification(user, operation, filePath, virtualPath, "", "", protocol, fileSize, openFlags, nil) return actionHandler.Handle(notification) } // ExecuteActionNotification executes the defined hook, if any, for the specified action func ExecuteActionNotification(user *dataprovider.User, operation, filePath, virtualPath, target, sshCmd, protocol string, fileSize int64, err error) { - notification := newActionNotification(user, operation, filePath, virtualPath, target, sshCmd, protocol, fileSize, err) + notification := newActionNotification(user, operation, filePath, virtualPath, target, sshCmd, protocol, fileSize, 0, err) if utils.IsStringInSlice(operation, Config.Actions.ExecuteSync) { actionHandler.Handle(notification) //nolint:errcheck @@ -94,12 +94,14 @@ type ActionNotification struct { Endpoint string `json:"endpoint,omitempty"` Status int `json:"status"` Protocol string `json:"protocol"` + OpenFlags int `json:"open_flags,omitempty"` } func newActionNotification( user *dataprovider.User, operation, filePath, virtualPath, target, sshCmd, protocol string, fileSize int64, + openFlags int, err error, ) *ActionNotification { var bucket, endpoint string @@ -142,6 +144,7 @@ func newActionNotification( Endpoint: endpoint, Status: status, Protocol: protocol, + OpenFlags: openFlags, } } @@ -230,5 +233,6 @@ func notificationAsEnvVars(notification *ActionNotification) []string { fmt.Sprintf("SFTPGO_ACTION_ENDPOINT=%v", notification.Endpoint), fmt.Sprintf("SFTPGO_ACTION_STATUS=%v", notification.Status), fmt.Sprintf("SFTPGO_ACTION_PROTOCOL=%v", notification.Protocol), + fmt.Sprintf("SFTPGO_ACTION_OPEN_FLAGS=%v", notification.OpenFlags), } } diff --git a/common/actions_test.go b/common/actions_test.go index 8891b838..e40b7d56 100644 --- a/common/actions_test.go +++ b/common/actions_test.go @@ -35,38 +35,39 @@ func TestNewActionNotification(t *testing.T) { user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{ Endpoint: "sftpendpoint", } - a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, errors.New("fake error")) + a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, errors.New("fake error")) assert.Equal(t, user.Username, a.Username) assert.Equal(t, 0, len(a.Bucket)) assert.Equal(t, 0, len(a.Endpoint)) assert.Equal(t, 0, a.Status) user.FsConfig.Provider = vfs.S3FilesystemProvider - a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSSH, 123, nil) + a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSSH, 123, 0, nil) assert.Equal(t, "s3bucket", a.Bucket) assert.Equal(t, "endpoint", a.Endpoint) assert.Equal(t, 1, a.Status) user.FsConfig.Provider = vfs.GCSFilesystemProvider - a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, ErrQuotaExceeded) + a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, 0, ErrQuotaExceeded) assert.Equal(t, "gcsbucket", a.Bucket) assert.Equal(t, 0, len(a.Endpoint)) assert.Equal(t, 2, a.Status) user.FsConfig.Provider = vfs.AzureBlobFilesystemProvider - a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, nil) + a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, 0, nil) assert.Equal(t, "azcontainer", a.Bucket) assert.Equal(t, "azsasurl", a.Endpoint) assert.Equal(t, 1, a.Status) user.FsConfig.AzBlobConfig.SASURL = "" - a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, nil) + a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSCP, 123, os.O_APPEND, nil) assert.Equal(t, "azcontainer", a.Bucket) assert.Equal(t, "azendpoint", a.Endpoint) assert.Equal(t, 1, a.Status) + assert.Equal(t, os.O_APPEND, a.OpenFlags) user.FsConfig.Provider = vfs.SFTPFilesystemProvider - a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, nil) + a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil) assert.Equal(t, "sftpendpoint", a.Endpoint) } @@ -80,7 +81,7 @@ func TestActionHTTP(t *testing.T) { user := &dataprovider.User{ Username: "username", } - a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, nil) + a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil) err := actionHandler.Handle(a) assert.NoError(t, err) @@ -113,7 +114,7 @@ func TestActionCMD(t *testing.T) { user := &dataprovider.User{ Username: "username", } - a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, nil) + a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", ProtocolSFTP, 123, 0, nil) err = actionHandler.Handle(a) assert.NoError(t, err) @@ -137,7 +138,7 @@ func TestWrongActions(t *testing.T) { Username: "username", } - a := newActionNotification(user, operationUpload, "", "", "", "", ProtocolSFTP, 123, nil) + a := newActionNotification(user, operationUpload, "", "", "", "", ProtocolSFTP, 123, 0, nil) err := actionHandler.Handle(a) assert.Error(t, err, "action with bad command must fail") diff --git a/common/connection.go b/common/connection.go index b25019b3..de61ed58 100644 --- a/common/connection.go +++ b/common/connection.go @@ -261,7 +261,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info } size := info.Size() - actionErr := ExecutePreAction(&c.User, operationPreDelete, fsPath, virtualPath, c.protocol, size) + actionErr := ExecutePreAction(&c.User, operationPreDelete, fsPath, virtualPath, c.protocol, size, 0) if actionErr == nil { c.Log(logger.LevelDebug, "remove for path %#v handled by pre-delete action", fsPath) } else { diff --git a/docs/custom-actions.md b/docs/custom-actions.md index bce3288e..70a67639 100644 --- a/docs/custom-actions.md +++ b/docs/custom-actions.md @@ -43,6 +43,7 @@ The external program can also read the following environment variables: - `SFTPGO_ACTION_ENDPOINT`, non-empty for S3, SFTP and Azure backend if configured. For Azure this is the SAS URL, if configured otherwise the endpoint - `SFTPGO_ACTION_STATUS`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error - `SFTPGO_ACTION_PROTOCOL`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP` +- `SFTPGO_ACTION_OPEN_FLAGS`, integer. File open flags, can be non-zero for `pre-upload` action. If `SFTPGO_ACTION_FILE_SIZE` is greater than zero and `SFTPGO_ACTION_OPEN_FLAGS&512 == 0` the target file will not be truncated Previous global environment variables aren't cleared when the script is called. The program must finish within 30 seconds. @@ -60,6 +61,7 @@ If the `hook` defines an HTTP URL then this URL will be invoked as HTTP POST. Th - `endpoint`, included for S3, SFTP and Azure backend if configured. For Azure this is the SAS URL, if configured, otherwise the endpoint - `status`, integer. Status for `upload`, `download` and `ssh_cmd` actions. 0 means a generic error occurred. 1 means no error, 2 means quota exceeded error - `protocol`, string. Possible values are `SSH`, `SFTP`, `SCP`, `FTP`, `DAV`, `HTTP` +- `open_flags`, integer, File open flags, can be non-zero for `pre-upload` action. If `file_size` is greater than zero and `file_size&512 == 0` the target file will not be truncated The HTTP hook will use the global configuration for HTTP clients and will respect the retry configurations. diff --git a/ftpd/handler.go b/ftpd/handler.go index 4407a090..69f38a43 100644 --- a/ftpd/handler.go +++ b/ftpd/handler.go @@ -297,7 +297,7 @@ func (c *Connection) downloadFile(fs vfs.Fs, fsPath, ftpPath string, offset int6 return nil, c.GetPermissionDeniedError() } - if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, fsPath, ftpPath, c.GetProtocol(), 0); err != nil { + if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, fsPath, ftpPath, c.GetProtocol(), 0, 0); err != nil { c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", ftpPath, err) return nil, c.GetPermissionDeniedError() } @@ -358,7 +358,7 @@ func (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, c.Log(logger.LevelInfo, "denying file write due to quota limits") return nil, common.ErrQuotaExceeded } - if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0); err != nil { + if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0, 0); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() } @@ -388,7 +388,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve c.Log(logger.LevelInfo, "denying file write due to quota limits") return nil, common.ErrQuotaExceeded } - if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize); err != nil { + if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize, flags); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() } diff --git a/httpd/handler.go b/httpd/handler.go index abb864f3..6d201f0f 100644 --- a/httpd/handler.go +++ b/httpd/handler.go @@ -93,7 +93,7 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io } if method != http.MethodHead { - if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, name, c.GetProtocol(), 0); err != nil { + if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, name, c.GetProtocol(), 0, 0); err != nil { c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", name, err) return nil, c.GetPermissionDeniedError() } diff --git a/sftpd/handler.go b/sftpd/handler.go index fc9309cb..bb689bcf 100644 --- a/sftpd/handler.go +++ b/sftpd/handler.go @@ -59,7 +59,7 @@ func (c *Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) { return nil, err } - if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, request.Filepath, c.GetProtocol(), 0); err != nil { + if err := common.ExecutePreAction(&c.User, common.OperationPreDownload, p, request.Filepath, c.GetProtocol(), 0, 0); err != nil { c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", request.Filepath, err) return nil, c.GetPermissionDeniedError() } @@ -330,7 +330,7 @@ func (c *Connection) handleSFTPUploadToNewFile(fs vfs.Fs, resolvedPath, filePath return nil, sftp.ErrSSHFxFailure } - if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0); err != nil { + if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0, 0); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() } @@ -361,13 +361,13 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO c.Log(logger.LevelInfo, "denying file write due to quota limits") return nil, sftp.ErrSSHFxFailure } - if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize); err != nil { + osFlags := getOSOpenFlags(pflags) + if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize, osFlags); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() } minWriteOffset := int64(0) - osFlags := getOSOpenFlags(pflags) isTruncate := osFlags&os.O_TRUNC != 0 // for upload resumes OpenSSH sets the APPEND flag while WinSCP does not set it, // so we suppose this is an upload resume if the TRUNCATE flag is not set diff --git a/sftpd/scp.go b/sftpd/scp.go index 660af757..d103031a 100644 --- a/sftpd/scp.go +++ b/sftpd/scp.go @@ -219,7 +219,7 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string, c.sendErrorMessage(fs, err) return err } - err := common.ExecutePreAction(&c.connection.User, common.OperationPreUpload, resolvedPath, requestPath, c.connection.GetProtocol(), fileSize) + err := common.ExecutePreAction(&c.connection.User, common.OperationPreUpload, resolvedPath, requestPath, c.connection.GetProtocol(), fileSize, os.O_TRUNC) if err != nil { c.connection.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) err = c.connection.GetPermissionDeniedError() @@ -514,7 +514,7 @@ func (c *scpCommand) handleDownload(filePath string) error { return common.ErrPermissionDenied } - if err := common.ExecutePreAction(&c.connection.User, common.OperationPreDownload, p, filePath, c.connection.GetProtocol(), 0); err != nil { + if err := common.ExecutePreAction(&c.connection.User, common.OperationPreDownload, p, filePath, c.connection.GetProtocol(), 0, 0); err != nil { c.connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", filePath, err) c.sendErrorMessage(fs, common.ErrPermissionDenied) return common.ErrPermissionDenied diff --git a/webdavd/file.go b/webdavd/file.go index 9ee3548a..9b593e6d 100644 --- a/webdavd/file.go +++ b/webdavd/file.go @@ -148,7 +148,7 @@ func (f *webDavFile) Read(p []byte) (n int, err error) { return 0, f.Connection.GetPermissionDeniedError() } err := common.ExecutePreAction(&f.Connection.User, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), - f.Connection.GetProtocol(), 0) + f.Connection.GetProtocol(), 0, 0) if err != nil { f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err) return 0, f.Connection.GetPermissionDeniedError() diff --git a/webdavd/handler.go b/webdavd/handler.go index d73cf09a..05562074 100644 --- a/webdavd/handler.go +++ b/webdavd/handler.go @@ -194,7 +194,7 @@ func (c *Connection) handleUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, re c.Log(logger.LevelInfo, "denying file write due to quota limits") return nil, common.ErrQuotaExceeded } - if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0); err != nil { + if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), 0, 0); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() } @@ -223,7 +223,7 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat c.Log(logger.LevelInfo, "denying file write due to quota limits") return nil, common.ErrQuotaExceeded } - if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize); err != nil { + if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), fileSize, os.O_TRUNC); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() }