diff --git a/docs/custom-actions.md b/docs/custom-actions.md index faafb870..2592f049 100644 --- a/docs/custom-actions.md +++ b/docs/custom-actions.md @@ -28,9 +28,7 @@ For cloud backends directories are virtual, they are created implicitly when you The notification will indicate if an error is detected and so, for example, a partial file is uploaded. -The `pre-delete` action, if defined, will be called just before files deletion. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute the hook defined for the `delete` action. - -The `pre-download` and `pre-upload` actions, will be called before downloads and uploads. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will allow the operation, otherwise the client will get a permission denied error. +The `pre-delete`, `pre-download` and `pre-upload` actions, will be called before deleting, downloading and uploading files. If the external command completes with a zero exit status or the HTTP notification response code is `200`, SFTPGo will allow the operation, otherwise the client will get a permission denied error. If the `hook` defines a path to an external program, then this program can read the following environment variables: diff --git a/docs/eventmanager.md b/docs/eventmanager.md index 6b4b47d2..d783921f 100644 --- a/docs/eventmanager.md +++ b/docs/eventmanager.md @@ -63,7 +63,7 @@ Actions are executed in a sequential order except for sync actions that are exec - `Stop on failure`, the next action will not be executed if the current one fails. - `Failure action`, this action will be executed only if at least another one fails. :warning: Please note that a failure action isn't executed if the event fails, for example if a download fails the main action is executed. The failure action is executed only if one of the non-failure actions associated to a rule fails. -- `Execute sync`, for upload events, you can execute the action(s) synchronously. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your action have completed its execution. If your acion takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection. For pre-* events at least a sync action is required. If pre-delete sync action(s) completes successfully, SFTPGo will assume that the file was already deleted/moved and so it will not try to remove the file and it will not execute any defined `delete` actions. If pre-upload/download action(s) completes successfully, SFTPGo will allow the operation, otherwise the client will get a permission denied error. +- `Execute sync`, for upload events, you can execute the action(s) synchronously. Executing an action synchronously means that SFTPGo will not return a result code to the client (which is waiting for it) until your action have completed its execution. If your acion takes a long time to complete this could cause a timeout on the client side, which wouldn't receive the server response in a timely manner and eventually drop the connection. For pre-* events at least a sync action is required. If pre-delete,pre-upload, pre-download sync action(s) completes successfully, SFTPGo will allow the operation, otherwise the client will get a permission denied error. If you are running multiple SFTPGo instances connected to the same data provider, you can choose whether to allow simultaneous execution for scheduled actions. diff --git a/go.mod b/go.mod index 8bd61f44..76a508ea 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.17.0 github.com/aws/aws-sdk-go-v2/service/sts v1.17.7 + github.com/bmatcuk/doublestar/v4 v4.4.0 github.com/cockroachdb/cockroach-go/v2 v2.2.20 github.com/coreos/go-oidc/v3 v3.4.0 github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b diff --git a/go.sum b/go.sum index 232f9236..b776c60a 100644 --- a/go.sum +++ b/go.sum @@ -304,6 +304,8 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic= +github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= diff --git a/internal/common/actions.go b/internal/common/actions.go index fcb83e25..e932c690 100644 --- a/internal/common/actions.go +++ b/internal/common/actions.go @@ -41,8 +41,6 @@ import ( ) var ( - errUnconfiguredAction = errors.New("no hook is configured for this action") - errNoHook = errors.New("unable to execute action, no hook defined") errUnexpectedHTTResponse = errors.New("unexpected HTTP hook response code") hooksConcurrencyGuard = make(chan struct{}, 150) activeHooks atomic.Int32 @@ -80,24 +78,18 @@ func InitializeActionHandler(handler ActionHandler) { actionHandler = handler } -func handleUnconfiguredPreAction(operation string) error { - // 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 - // a nil error - if operation == operationPreDelete { - return errUnconfiguredAction - } - return nil -} - -// ExecutePreAction executes a pre-* action and returns the result -func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) error { +// ExecutePreAction executes a pre-* action and returns the result. +// The returned status has the following meaning: +// - 0 not executed +// - 1 executed using an external hook +// - 2 executed using the event manager +func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) { var event *notifier.FsEvent hasNotifiersPlugin := plugin.Handler.HasNotifiers() hasHook := util.Contains(Config.Actions.ExecuteOn, operation) hasRules := eventManager.hasFsRules() if !hasHook && !hasNotifiersPlugin && !hasRules { - return handleUnconfiguredPreAction(operation) + return 0, nil } event = newActionNotification(&conn.User, operation, filePath, virtualPath, "", "", "", conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil)) @@ -124,11 +116,11 @@ func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath str } executedSync, err := eventManager.handleFsEvent(params) if executedSync { - return err + return 2, err } } if !hasHook { - return handleUnconfiguredPreAction(operation) + return 0, nil } return actionHandler.Handle(event) } @@ -176,7 +168,8 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua } if hasHook { if util.Contains(Config.Actions.ExecuteSync, operation) { - return actionHandler.Handle(notification) + _, err := actionHandler.Handle(notification) + return err } go func() { startNewHook() @@ -190,7 +183,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua // ActionHandler handles a notification for a Protocol Action. type ActionHandler interface { - Handle(notification *notifier.FsEvent) error + Handle(notification *notifier.FsEvent) (int, error) } func newActionNotification( @@ -244,28 +237,30 @@ func newActionNotification( type defaultActionHandler struct{} -func (h *defaultActionHandler) Handle(event *notifier.FsEvent) error { +func (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) { if !util.Contains(Config.Actions.ExecuteOn, event.Action) { - return errUnconfiguredAction + return 0, nil } if Config.Actions.Hook == "" { logger.Warn(event.Protocol, "", "Unable to send notification, no hook is defined") - return errNoHook + return 0, nil } if strings.HasPrefix(Config.Actions.Hook, "http") { - return h.handleHTTP(event) + err := h.handleHTTP(event) + return 1, err } - return h.handleCommand(event) + err := h.handleCommand(event) + return 1, err } func (h *defaultActionHandler) handleHTTP(event *notifier.FsEvent) error { u, err := url.Parse(Config.Actions.Hook) if err != nil { - logger.Error(event.Protocol, "", "Invalid hook %#v for operation %#v: %v", + logger.Error(event.Protocol, "", "Invalid hook %q for operation %q: %v", Config.Actions.Hook, event.Action, err) return err } @@ -294,7 +289,7 @@ func (h *defaultActionHandler) handleHTTP(event *notifier.FsEvent) error { func (h *defaultActionHandler) handleCommand(event *notifier.FsEvent) error { if !filepath.IsAbs(Config.Actions.Hook) { - err := fmt.Errorf("invalid notification command %#v", Config.Actions.Hook) + err := fmt.Errorf("invalid notification command %q", Config.Actions.Hook) logger.Warn(event.Protocol, "", "unable to execute notification command: %v", err) return err diff --git a/internal/common/actions_test.go b/internal/common/actions_test.go index 1d37c7dc..ddebe493 100644 --- a/internal/common/actions_test.go +++ b/internal/common/actions_test.go @@ -136,18 +136,21 @@ func TestActionHTTP(t *testing.T) { } a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", xid.New().String(), 123, 0, 1) - err := actionHandler.Handle(a) + status, err := actionHandler.Handle(a) assert.NoError(t, err) + assert.Equal(t, 1, status) Config.Actions.Hook = "http://invalid:1234" - err = actionHandler.Handle(a) + status, err = actionHandler.Handle(a) assert.Error(t, err) + assert.Equal(t, 1, status) Config.Actions.Hook = fmt.Sprintf("http://%v/404", httpAddr) - err = actionHandler.Handle(a) + status, err = actionHandler.Handle(a) if assert.Error(t, err) { assert.EqualError(t, err, errUnexpectedHTTResponse.Error()) } + assert.Equal(t, 1, status) Config.Actions = actionsCopy } @@ -173,8 +176,9 @@ func TestActionCMD(t *testing.T) { sessionID := shortuuid.New() a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID, 123, 0, 1) - err = actionHandler.Handle(a) + status, err := actionHandler.Handle(a) assert.NoError(t, err) + assert.Equal(t, 1, status) c := NewBaseConnection("id", ProtocolSFTP, "", "", *user) err = ExecuteActionNotification(c, OperationSSHCmd, "path", "vpath", "target", "vtarget", "sha1sum", 0, nil) @@ -205,29 +209,32 @@ func TestWrongActions(t *testing.T) { a := newActionNotification(user, operationUpload, "", "", "", "", "", ProtocolSFTP, "", xid.New().String(), 123, 0, 1) - err := actionHandler.Handle(a) + status, err := actionHandler.Handle(a) assert.Error(t, err, "action with bad command must fail") + assert.Equal(t, 1, status) a.Action = operationDelete - err = actionHandler.Handle(a) - assert.EqualError(t, err, errUnconfiguredAction.Error()) + status, err = actionHandler.Handle(a) + assert.NoError(t, err) + assert.Equal(t, 0, status) Config.Actions.Hook = "http://foo\x7f.com/" a.Action = operationUpload - err = actionHandler.Handle(a) + status, err = actionHandler.Handle(a) assert.Error(t, err, "action with bad url must fail") + assert.Equal(t, 1, status) Config.Actions.Hook = "" - err = actionHandler.Handle(a) - if assert.Error(t, err) { - assert.EqualError(t, err, errNoHook.Error()) - } + status, err = actionHandler.Handle(a) + assert.NoError(t, err) + assert.Equal(t, 0, status) Config.Actions.Hook = "relative path" - err = actionHandler.Handle(a) + status, err = actionHandler.Handle(a) if assert.Error(t, err) { - assert.EqualError(t, err, fmt.Sprintf("invalid notification command %#v", Config.Actions.Hook)) + assert.EqualError(t, err, fmt.Sprintf("invalid notification command %q", Config.Actions.Hook)) } + assert.Equal(t, 1, status) Config.Actions = actionsCopy } @@ -242,7 +249,7 @@ func TestPreDeleteAction(t *testing.T) { assert.NoError(t, err) Config.Actions = ProtocolActions{ ExecuteOn: []string{operationPreDelete}, - Hook: hookCmd, + Hook: "missing hook", } homeDir := filepath.Join(os.TempDir(), "test_user") err = os.MkdirAll(homeDir, os.ModePerm) @@ -264,8 +271,12 @@ func TestPreDeleteAction(t *testing.T) { info, err := os.Stat(testfile) assert.NoError(t, err) err = c.RemoveFile(fs, testfile, "testfile", info) - assert.NoError(t, err) + assert.ErrorIs(t, err, c.GetPermissionDeniedError()) assert.FileExists(t, testfile) + Config.Actions.Hook = hookCmd + err = c.RemoveFile(fs, testfile, "testfile", info) + assert.NoError(t, err) + assert.NoFileExists(t, testfile) os.RemoveAll(homeDir) @@ -289,10 +300,12 @@ func TestUnconfiguredHook(t *testing.T) { assert.True(t, plugin.Handler.HasNotifiers()) c := NewBaseConnection("id", ProtocolSFTP, "", "", dataprovider.User{}) - err = ExecutePreAction(c, OperationPreDownload, "", "", 0, 0) + status, err := ExecutePreAction(c, OperationPreDownload, "", "", 0, 0) assert.NoError(t, err) - err = ExecutePreAction(c, operationPreDelete, "", "", 0, 0) - assert.ErrorIs(t, err, errUnconfiguredAction) + assert.Equal(t, status, 0) + status, err = ExecutePreAction(c, operationPreDelete, "", "", 0, 0) + assert.NoError(t, err) + assert.Equal(t, status, 0) err = ExecuteActionNotification(c, operationDownload, "", "", "", "", "", 0, nil) assert.NoError(t, err) @@ -308,10 +321,10 @@ type actionHandlerStub struct { called bool } -func (h *actionHandlerStub) Handle(event *notifier.FsEvent) error { +func (h *actionHandlerStub) Handle(event *notifier.FsEvent) (int, error) { h.called = true - return nil + return 1, nil } func TestInitializeActionHandler(t *testing.T) { @@ -322,8 +335,8 @@ func TestInitializeActionHandler(t *testing.T) { InitializeActionHandler(&defaultActionHandler{}) }) - err := actionHandler.Handle(¬ifier.FsEvent{}) - + status, err := actionHandler.Handle(¬ifier.FsEvent{}) assert.NoError(t, err) assert.True(t, handler.called) + assert.Equal(t, 1, status) } diff --git a/internal/common/connection.go b/internal/common/connection.go index d20592ee..064d319e 100644 --- a/internal/common/connection.go +++ b/internal/common/connection.go @@ -391,11 +391,18 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info } size := info.Size() - actionErr := ExecutePreAction(c, operationPreDelete, fsPath, virtualPath, size, 0) - if actionErr == nil { - c.Log(logger.LevelDebug, "remove for path %q handled by pre-delete action", fsPath) - } else { - if err := fs.Remove(fsPath, false); err != nil { + status, err := ExecutePreAction(c, operationPreDelete, fsPath, virtualPath, size, 0) + if err != nil { + c.Log(logger.LevelDebug, "delete for file %q denied by pre action: %v", virtualPath, err) + return c.GetPermissionDeniedError() + } + updateQuota := true + if err := fs.Remove(fsPath, false); err != nil { + if status > 0 && fs.IsNotExist(err) { + // file removed in the pre-action, if the file was deleted from the EventManager the quota is already updated + c.Log(logger.LevelDebug, "file deleted from the hook, status: %d", status) + updateQuota = (status == 1) + } else { c.Log(logger.LevelError, "failed to remove file/symlink %q: %+v", fsPath, err) return c.GetFsError(fs, err) } @@ -403,7 +410,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info logger.CommandLog(removeLogSender, fsPath, "", c.User.Username, "", c.ID, c.protocol, -1, -1, "", "", "", -1, c.localAddr, c.remoteAddr) - if info.Mode()&os.ModeSymlink == 0 { + if updateQuota && info.Mode()&os.ModeSymlink == 0 { vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath)) if err == nil { dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, -1, -size, false) //nolint:errcheck @@ -414,9 +421,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info dataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck } } - if actionErr != nil { - ExecuteActionNotification(c, operationDelete, fsPath, virtualPath, "", "", "", size, nil) //nolint:errcheck - } + ExecuteActionNotification(c, operationDelete, fsPath, virtualPath, "", "", "", size, nil) //nolint:errcheck return nil } diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index f3575b35..88e60298 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -36,6 +36,7 @@ import ( "sync/atomic" "time" + "github.com/bmatcuk/doublestar/v4" "github.com/klauspost/compress/zip" "github.com/robfig/cron/v3" "github.com/rs/xid" @@ -285,9 +286,7 @@ func (r *eventRulesContainer) checkFsEventMatch(conditions dataprovider.EventCon return false } if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) { - if !checkEventConditionPatterns(params.ObjectName, conditions.Options.FsPaths) { - return false - } + return false } if len(conditions.Options.Protocols) > 0 && !util.Contains(conditions.Options.Protocols, params.Protocol) { return false @@ -966,7 +965,13 @@ func replaceWithReplacer(input string, replacer *strings.Replacer) string { } func checkEventConditionPattern(p dataprovider.ConditionPattern, name string) bool { - matched, err := path.Match(p.Pattern, name) + var matched bool + var err error + if strings.Contains(p.Pattern, "**") { + matched, err = doublestar.Match(p.Pattern, name) + } else { + matched, err = path.Match(p.Pattern, name) + } if err != nil { eventManagerLog(logger.LevelError, "pattern matching error %q, err: %v", p.Pattern, err) return false diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go index b096e055..e43ef131 100644 --- a/internal/common/eventmanager_test.go +++ b/internal/common/eventmanager_test.go @@ -119,7 +119,7 @@ func TestEventRuleMatch(t *testing.T) { }, FsPaths: []dataprovider.ConditionPattern{ { - Pattern: "*.txt", + Pattern: "/**/*.txt", }, }, Protocols: []string{ProtocolSFTP}, @@ -268,6 +268,40 @@ func TestEventRuleMatch(t *testing.T) { assert.False(t, res) } +func TestDoubleStarMatching(t *testing.T) { + c := dataprovider.ConditionPattern{ + Pattern: "/mydir/**", + } + res := checkEventConditionPattern(c, "/mydir") + assert.True(t, res) + res = checkEventConditionPattern(c, "/mydirname") + assert.False(t, res) + res = checkEventConditionPattern(c, "/mydir/sub") + assert.True(t, res) + res = checkEventConditionPattern(c, "/mydir/sub/dir") + assert.True(t, res) + + c.Pattern = "/**/*" + res = checkEventConditionPattern(c, "/mydir") + assert.True(t, res) + res = checkEventConditionPattern(c, "/mydirname") + assert.True(t, res) + res = checkEventConditionPattern(c, "/mydir/sub/dir/file.txt") + assert.True(t, res) + + c.Pattern = "/mydir/**/*.txt" + res = checkEventConditionPattern(c, "/mydir") + assert.False(t, res) + res = checkEventConditionPattern(c, "/mydirname/f.txt") + assert.False(t, res) + res = checkEventConditionPattern(c, "/mydir/sub") + assert.False(t, res) + res = checkEventConditionPattern(c, "/mydir/sub/dir") + assert.False(t, res) + res = checkEventConditionPattern(c, "/mydir/sub/dir/a.txt") + assert.True(t, res) +} + func TestEventManager(t *testing.T) { startEventScheduler() action := &dataprovider.BaseEventAction{ diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index 56e5bf97..b77a4139 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -3470,7 +3470,7 @@ func TestEventRule(t *testing.T) { Pattern: "/subdir/*.dat", }, { - Pattern: "*.txt", + Pattern: "/**/*.txt", }, }, }, @@ -3520,7 +3520,7 @@ func TestEventRule(t *testing.T) { Options: dataprovider.ConditionOptions{ FsPaths: []dataprovider.ConditionPattern{ { - Pattern: "*.dat", + Pattern: "/**/*.dat", }, }, }, @@ -4231,6 +4231,14 @@ func TestEventRulePreDelete(t *testing.T) { Trigger: dataprovider.EventTriggerFsEvent, Conditions: dataprovider.EventConditions{ FsEvents: []string{"pre-delete"}, + Options: dataprovider.ConditionOptions{ + FsPaths: []dataprovider.ConditionPattern{ + { + Pattern: fmt.Sprintf("/%s/**", movePath), + InverseMatch: true, + }, + }, + }, }, Actions: []dataprovider.EventAction{ { @@ -4255,7 +4263,19 @@ func TestEventRulePreDelete(t *testing.T) { } rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err) - user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) + u := getTestUser() + u.QuotaFiles = 1000 + u.VirtualFolders = []vfs.VirtualFolder{ + { + BaseVirtualFolder: vfs.BaseVirtualFolder{ + Name: movePath, + MappedPath: filepath.Join(os.TempDir(), movePath), + }, + VirtualPath: "/" + movePath, + QuotaFiles: 1000, + }, + } + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { @@ -4273,7 +4293,7 @@ func TestEventRulePreDelete(t *testing.T) { assert.NoError(t, err) err = client.Remove(path.Join(testDir, testFileName)) assert.NoError(t, err) - // check + // check files _, err = client.Stat(testFileName) assert.ErrorIs(t, err, os.ErrNotExist) _, err = client.Stat(path.Join(testDir, testFileName)) @@ -4282,6 +4302,23 @@ func TestEventRulePreDelete(t *testing.T) { assert.NoError(t, err) _, err = client.Stat(path.Join("/", movePath, testDir, testFileName)) assert.NoError(t, err) + // check quota + user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, 0, user.UsedQuotaFiles) + assert.Equal(t, int64(0), user.UsedQuotaSize) + folder, _, err := httpdtest.GetFolderByName(movePath, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, 2, folder.UsedQuotaFiles) + assert.Equal(t, int64(200), folder.UsedQuotaSize) + // pre-delete action is not executed in movePath + err = client.Remove(path.Join("/", movePath, testFileName)) + assert.NoError(t, err) + // check quota + folder, _, err = httpdtest.GetFolderByName(movePath, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, 1, folder.UsedQuotaFiles) + assert.Equal(t, int64(100), folder.UsedQuotaSize) } _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) @@ -4294,6 +4331,10 @@ func TestEventRulePreDelete(t *testing.T) { assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) + _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: movePath}, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(filepath.Join(os.TempDir(), movePath)) + assert.NoError(t, err) } func TestEventRulePreDownloadUpload(t *testing.T) { diff --git a/internal/ftpd/handler.go b/internal/ftpd/handler.go index 216f4607..8ea1032a 100644 --- a/internal/ftpd/handler.go +++ b/internal/ftpd/handler.go @@ -342,7 +342,7 @@ func (c *Connection) downloadFile(fs vfs.Fs, fsPath, ftpPath string, offset int6 return nil, c.GetErrorForDeniedFile(policy) } - if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, fsPath, ftpPath, 0, 0); err != nil { + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, fsPath, ftpPath, 0, 0); err != nil { c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", ftpPath, err) return nil, c.GetPermissionDeniedError() } @@ -404,7 +404,7 @@ func (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, flags int, resolvedPath c.Log(logger.LevelInfo, "denying file write due to quota limits") return nil, ftpserver.ErrStorageExceeded } - if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil { + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed) } @@ -449,7 +449,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve c.Log(logger.LevelDebug, "unable to get max write size: %v", err) return nil, err } - if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, flags); err != nil { + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, flags); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed) } diff --git a/internal/httpd/handler.go b/internal/httpd/handler.go index 48fd27e1..e61510c1 100644 --- a/internal/httpd/handler.go +++ b/internal/httpd/handler.go @@ -118,7 +118,7 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io } if method != http.MethodHead { - if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, name, 0, 0); err != nil { + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, name, 0, 0); err != nil { c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", name, err) return nil, c.GetPermissionDeniedError() } @@ -193,7 +193,7 @@ func (c *Connection) handleUploadFile(fs vfs.Fs, resolvedPath, filePath, request c.Log(logger.LevelInfo, "denying file write due to quota limits") return nil, common.ErrQuotaExceeded } - err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, os.O_TRUNC) + _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, os.O_TRUNC) if err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() diff --git a/internal/sftpd/handler.go b/internal/sftpd/handler.go index 60e076a7..f2df3bb6 100644 --- a/internal/sftpd/handler.go +++ b/internal/sftpd/handler.go @@ -93,7 +93,7 @@ func (c *Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) { return nil, err } - if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, request.Filepath, 0, 0); err != nil { + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, request.Filepath, 0, 0); err != nil { c.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", request.Filepath, err) return nil, c.GetPermissionDeniedError() } @@ -401,7 +401,7 @@ func (c *Connection) handleSFTPUploadToNewFile(fs vfs.Fs, pflags sftp.FileOpenFl return nil, c.GetQuotaExceededError() } - if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil { + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() } @@ -449,7 +449,7 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO return nil, err } - if err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, osFlags); err != nil { + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, osFlags); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() } diff --git a/internal/sftpd/scp.go b/internal/sftpd/scp.go index 8b54dac6..b00abf85 100644 --- a/internal/sftpd/scp.go +++ b/internal/sftpd/scp.go @@ -236,7 +236,7 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string, c.sendErrorMessage(nil, err) return err } - err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, + _, err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, fileSize, os.O_TRUNC) if err != nil { c.connection.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) @@ -532,7 +532,7 @@ func (c *scpCommand) handleDownload(filePath string) error { return common.ErrPermissionDenied } - if err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreDownload, p, filePath, 0, 0); err != nil { + if _, err := common.ExecutePreAction(c.connection.BaseConnection, common.OperationPreDownload, p, filePath, 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/internal/webdavd/file.go b/internal/webdavd/file.go index f6ffeaf8..b8a8ae39 100644 --- a/internal/webdavd/file.go +++ b/internal/webdavd/file.go @@ -169,7 +169,7 @@ func (f *webDavFile) checkFirstRead() error { f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath()) return f.Connection.GetErrorForDeniedFile(policy) } - err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0) + _, err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0) if err != nil { f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err) return f.Connection.GetPermissionDeniedError() diff --git a/internal/webdavd/handler.go b/internal/webdavd/handler.go index 841353cf..a70d62e3 100644 --- a/internal/webdavd/handler.go +++ b/internal/webdavd/handler.go @@ -211,7 +211,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.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil { + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 0, 0); err != nil { c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err) return nil, c.GetPermissionDeniedError() } @@ -243,7 +243,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.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, + if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreUpload, resolvedPath, requestPath, 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() diff --git a/templates/webadmin/eventrule.html b/templates/webadmin/eventrule.html index b951cff0..d14cc2fd 100644 --- a/templates/webadmin/eventrule.html +++ b/templates/webadmin/eventrule.html @@ -350,7 +350,7 @@ along with this program. If not, see . Path filters
-
Shell-like pattern filters for filesystem events. For example "/adir/*.txt"" will match paths in the "/adir" directory ending with ".txt"
+
Shell-like pattern filters for filesystem events. For example "/adir/*.txt"" will match paths in the "/adir" directory ending with ".txt". Double asterisk is supported, for example "/**/*.txt" will match any file ending with ".txt". "/mydir/**" will match any entry in "/mydir"
{{range $idx, $val := .Rule.Conditions.Options.FsPaths}}