pre-upload action: add file open flags

Reading the flags the hook receiver can detect if the client wants to
truncate the target file
This commit is contained in:
Nicola Murino 2021-05-31 22:33:23 +02:00
parent c63b923ec3
commit c1239fbf59
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
10 changed files with 33 additions and 26 deletions

View file

@ -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),
}
}

View file

@ -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")

View file

@ -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 {

View file

@ -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.

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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()
}