Просмотр исходного кода

pre-upload action: add file open flags

Reading the flags the hook receiver can detect if the client wants to
truncate the target file
Nicola Murino 4 лет назад
Родитель
Сommit
c1239fbf59
10 измененных файлов с 33 добавлено и 26 удалено
  1. 7 3
      common/actions.go
  2. 10 9
      common/actions_test.go
  3. 1 1
      common/connection.go
  4. 2 0
      docs/custom-actions.md
  5. 3 3
      ftpd/handler.go
  6. 1 1
      httpd/handler.go
  7. 4 4
      sftpd/handler.go
  8. 2 2
      sftpd/scp.go
  9. 1 1
      webdavd/file.go
  10. 2 2
      webdavd/handler.go

+ 7 - 3
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),
 	}
 }

+ 10 - 9
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")
 

+ 1 - 1
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 {

+ 2 - 0
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.
 

+ 3 - 3
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()
 	}

+ 1 - 1
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()
 		}

+ 4 - 4
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

+ 2 - 2
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

+ 1 - 1
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()

+ 2 - 2
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()
 	}