diff --git a/docs/eventmanager.md b/docs/eventmanager.md index ae2f2743..ef1b90da 100644 --- a/docs/eventmanager.md +++ b/docs/eventmanager.md @@ -16,6 +16,7 @@ The following actions are supported: - `Rename`. You can rename one or more files or directories. - `Delete`. You can delete one or more files and directories. - `Create directories`. You can create one or more directories including sub-directories. + - `Path exists`. Check if the specified path exists. The following placeholders are supported: diff --git a/go.mod b/go.mod index 764cf50e..03b6a714 100644 --- a/go.mod +++ b/go.mod @@ -65,8 +65,8 @@ require ( go.etcd.io/bbolt v1.3.6 go.uber.org/automaxprocs v1.5.1 gocloud.dev v0.26.0 - golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/net v0.0.0-20220811182439-13a9a731de15 + golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 + golang.org/x/net v0.0.0-20220812174116-3211cb980234 golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7 golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 @@ -155,7 +155,7 @@ require ( golang.org/x/tools v0.1.12 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1 // indirect + google.golang.org/genproto v0.0.0-20220819174105-e9f053255caa // indirect google.golang.org/grpc v1.48.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -167,6 +167,6 @@ require ( replace ( github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d - golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e - golang.org/x/net => github.com/drakkan/net v0.0.0-20220812153436-025c6c7680ee + golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220820120743-96e237d06b82 + golang.org/x/net => github.com/drakkan/net v0.0.0-20220820120527-aa746bf1d738 ) diff --git a/go.sum b/go.sum index f9e6392c..69679eae 100644 --- a/go.sum +++ b/go.sum @@ -262,12 +262,12 @@ github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e h1:ZvOJ5DqEUZig5lGlwGy78KrSIk9OpXFOpoBFrZC0HCo= -github.com/drakkan/crypto v0.0.0-20220723143649-81550382d55e/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro= +github.com/drakkan/crypto v0.0.0-20220820120743-96e237d06b82 h1:TezoLY9GhuhvionRxoU1FyfIbbC2lOm+OipXzuXAC2A= +github.com/drakkan/crypto v0.0.0-20220820120743-96e237d06b82/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro= github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA= github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU= -github.com/drakkan/net v0.0.0-20220812153436-025c6c7680ee h1:hTHRVJ//MvApWBRVLrZOCfh+row8txg1G9BJVKsq+qk= -github.com/drakkan/net v0.0.0-20220812153436-025c6c7680ee/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +github.com/drakkan/net v0.0.0-20220820120527-aa746bf1d738 h1:y++pz0G+bwPzCJyHiqslCAXzAWj4Azk5FbGTwZ5nq0g= +github.com/drakkan/net v0.0.0-20220820120527-aa746bf1d738/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d h1:kNk/KRhszPJASp7WvjagNW254aKK643Lu8/fr4/ukiM= github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4= @@ -1229,8 +1229,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1 h1:C2UVWqrgLYKrT5nh5oU6hLRm1AeEklCK5eloQA1NtFY= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220819174105-e9f053255caa h1:Ux9yJCyf598uEniFPSyp8g1jtGTt77m+lzYyVgrWQaQ= +google.golang.org/genproto v0.0.0-20220819174105-e9f053255caa/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index 39bba743..90350c6f 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -662,7 +662,7 @@ func executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range deletes { - item = replaceWithReplacer(item, replacer) + item = util.CleanPath(replaceWithReplacer(item, replacer)) info, err := conn.DoStat(item, 0, false) if err != nil { if conn.IsNotExistError(err) { @@ -729,7 +729,7 @@ func executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, use } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range dirs { - item = replaceWithReplacer(item, replacer) + item = util.CleanPath(replaceWithReplacer(item, replacer)) if err = conn.CheckParentDirs(path.Dir(item)); err != nil { return fmt.Errorf("unable to check parent dirs for %q, user %q: %w", item, user.Username, err) } @@ -788,8 +788,8 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str } conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) for _, item := range renames { - source := replaceWithReplacer(item.Key, replacer) - target := replaceWithReplacer(item.Value, replacer) + source := util.CleanPath(replaceWithReplacer(item.Key, replacer)) + target := util.CleanPath(replaceWithReplacer(item.Value, replacer)) if err = conn.Rename(source, target); err != nil { return fmt.Errorf("unable to rename %q->%q, user %q: %w", source, target, user.Username, err) } @@ -798,6 +798,30 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str return nil } +func executeExistFsActionForUser(exist []string, replacer *strings.Replacer, + user dataprovider.User, +) error { + user, err := getUserForEventAction(user) + if err != nil { + return err + } + connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String()) + err = user.CheckFsRoot(connectionID) + defer user.CloseFs() //nolint:errcheck + if err != nil { + return err + } + conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user) + for _, item := range exist { + item = util.CleanPath(replaceWithReplacer(item, replacer)) + if _, err = conn.DoStat(item, 0, false); err != nil { + return fmt.Errorf("error checking existence for path %q, user %q: %w", item, user.Username, err) + } + eventManagerLog(logger.LevelDebug, "path %q exists for user %q", item, user.Username) + } + return nil +} + func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer, conditions dataprovider.ConditionOptions, params EventParams, ) error { @@ -830,6 +854,38 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string return nil } +func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, conditions dataprovider.ConditionOptions, + params EventParams, +) error { + users, err := params.getUsers() + if err != nil { + return fmt.Errorf("unable to get users: %w", err) + } + var failures []string + executed := 0 + for _, user := range users { + // if sender is set, the conditions have already been evaluated + if params.sender == "" && !checkEventConditionPatterns(user.Username, conditions.Names) { + eventManagerLog(logger.LevelDebug, "skipping fs exist for user %s, name conditions don't match", + user.Username) + continue + } + executed++ + if err = executeExistFsActionForUser(exist, replacer, user); err != nil { + failures = append(failures, user.Username) + continue + } + } + if len(failures) > 0 { + return fmt.Errorf("fs existence check failed for users: %+v", failures) + } + if executed == 0 { + eventManagerLog(logger.LevelError, "no existence check executed") + return errors.New("no existence check executed") + } + return nil +} + func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions dataprovider.ConditionOptions, params EventParams, ) error { @@ -843,6 +899,8 @@ func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions return executeDeleteFsRuleAction(c.Deletes, replacer, conditions, params) case dataprovider.FilesystemActionMkdirs: return executeMkdirFsRuleAction(c.MkDirs, replacer, conditions, params) + case dataprovider.FilesystemActionExist: + return executeExistFsRuleAction(c.Exist, replacer, conditions, params) default: return fmt.Errorf("unsupported filesystem action %d", c.Type) } diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go index 9eda71c7..89f1dc10 100644 --- a/internal/common/eventmanager_test.go +++ b/internal/common/eventmanager_test.go @@ -281,6 +281,8 @@ func TestEventManagerErrors(t *testing.T) { assert.Error(t, err) err = executeRenameFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) assert.Error(t, err) + err = executeExistFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, EventParams{}) + assert.Error(t, err) groupName := "agroup" err = executeQuotaResetForUser(dataprovider.User{ @@ -328,6 +330,15 @@ func TestEventManagerErrors(t *testing.T) { }, }) assert.Error(t, err) + err = executeExistFsActionForUser(nil, nil, dataprovider.User{ + Groups: []sdk.GroupMapping{ + { + Name: groupName, + Type: sdk.GroupTypePrimary, + }, + }, + }) + assert.Error(t, err) dataRetentionAction := dataprovider.BaseEventAction{ Type: dataprovider.ActionTypeDataRetentionCheck, @@ -637,6 +648,43 @@ func TestEventRuleActions(t *testing.T) { if assert.Error(t, err) { assert.Contains(t, err.Error(), "no retention check executed") } + // test file exists action + action = dataprovider.BaseEventAction{ + Type: dataprovider.ActionTypeFilesystem, + Options: dataprovider.BaseEventActionOptions{ + FsConfig: dataprovider.EventActionFilesystemConfig{ + Type: dataprovider.FilesystemActionExist, + Exist: []string{"/file1.txt", path.Join("/", retentionDir, "file3.txt")}, + }, + }, + } + err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + Names: []dataprovider.ConditionPattern{ + { + Pattern: "no match", + }, + }, + }) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "no existence check executed") + } + err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + Names: []dataprovider.ConditionPattern{ + { + Pattern: username1, + }, + }, + }) + assert.NoError(t, err) + action.Options.FsConfig.Exist = []string{"/file1.txt", path.Join("/", retentionDir, "file2.txt")} + err = executeRuleAction(action, EventParams{}, dataprovider.ConditionOptions{ + Names: []dataprovider.ConditionPattern{ + { + Pattern: username1, + }, + }, + }) + assert.Error(t, err) err = os.RemoveAll(user1.GetHomeDir()) assert.NoError(t, err) @@ -838,6 +886,8 @@ func TestFilesystemActionErrors(t *testing.T) { assert.Error(t, err) err = executeRenameFsActionForUser(nil, testReplacer, user) assert.Error(t, err) + err = executeExistFsActionForUser(nil, testReplacer, user) + assert.Error(t, err) user.FsConfig.Provider = sdk.LocalFilesystemProvider user.Permissions["/"] = []string{dataprovider.PermUpload} diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index 1231ec61..d7ebd69c 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -3535,6 +3535,16 @@ func TestEventRuleFsActions(t *testing.T) { Name: "a5", Type: dataprovider.ActionTypeUserQuotaReset, } + a6 := dataprovider.BaseEventAction{ + Name: "a6", + Type: dataprovider.ActionTypeFilesystem, + Options: dataprovider.BaseEventActionOptions{ + FsConfig: dataprovider.EventActionFilesystemConfig{ + Type: dataprovider.FilesystemActionExist, + Exist: []string{"/{{VirtualPath}}"}, + }, + }, + } action1, resp, err := httpdtest.AddEventAction(a1, http.StatusCreated) assert.NoError(t, err, string(resp)) action2, resp, err := httpdtest.AddEventAction(a2, http.StatusCreated) @@ -3545,6 +3555,8 @@ func TestEventRuleFsActions(t *testing.T) { assert.NoError(t, err, string(resp)) action5, resp, err := httpdtest.AddEventAction(a5, http.StatusCreated) assert.NoError(t, err, string(resp)) + action6, resp, err := httpdtest.AddEventAction(a6, http.StatusCreated) + assert.NoError(t, err, string(resp)) r1 := dataprovider.EventRule{ Name: "r1", @@ -3598,6 +3610,12 @@ func TestEventRuleFsActions(t *testing.T) { }, Order: 1, }, + { + BaseEventAction: dataprovider.BaseEventAction{ + Name: action6.Name, + }, + Order: 2, + }, }, } r4 := dataprovider.EventRule{ @@ -3742,6 +3760,8 @@ func TestEventRuleFsActions(t *testing.T) { assert.NoError(t, err) _, err = httpdtest.RemoveEventAction(action5, http.StatusOK) assert.NoError(t, err) + _, err = httpdtest.RemoveEventAction(action6, http.StatusOK) + assert.NoError(t, err) } func TestEventRuleCertificate(t *testing.T) { diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go index 1c2a5a5f..fd60feda 100644 --- a/internal/dataprovider/eventrule.go +++ b/internal/dataprovider/eventrule.go @@ -117,10 +117,12 @@ const ( FilesystemActionRename = iota + 1 FilesystemActionDelete FilesystemActionMkdirs + FilesystemActionExist ) var ( - supportedFsActions = []int{FilesystemActionRename, FilesystemActionDelete, FilesystemActionMkdirs} + supportedFsActions = []int{FilesystemActionRename, FilesystemActionDelete, FilesystemActionMkdirs, + FilesystemActionExist} ) func isFilesystemActionValid(value int) bool { @@ -133,6 +135,8 @@ func getFsActionTypeAsString(value int) string { return "Rename" case FilesystemActionDelete: return "Delete" + case FilesystemActionExist: + return "Paths exist" default: return "Create directories" } @@ -384,6 +388,8 @@ type EventActionFilesystemConfig struct { MkDirs []string `json:"mkdirs,omitempty"` // files/dirs to delete Deletes []string `json:"deletes,omitempty"` + // file/dirs to check for existence + Exist []string `json:"exist,omitempty"` } // GetDeletesAsString returns the list of items to delete as comma separated string. @@ -398,15 +404,21 @@ func (c EventActionFilesystemConfig) GetMkDirsAsString() string { return strings.Join(c.MkDirs, ",") } +// GetExistAsString returns the list of items to check for existence as comma separated string. +// Using a pointer receiver will not work in web templates +func (c EventActionFilesystemConfig) GetExistAsString() string { + return strings.Join(c.Exist, ",") +} + func (c *EventActionFilesystemConfig) validateRenames() error { if len(c.Renames) == 0 { - return util.NewValidationError("no items to rename specified") + return util.NewValidationError("no path to rename specified") } for idx, kv := range c.Renames { key := strings.TrimSpace(kv.Key) value := strings.TrimSpace(kv.Value) if key == "" || value == "" { - return util.NewValidationError("invalid items to rename") + return util.NewValidationError("invalid paths to rename") } key = util.CleanPath(key) value = util.CleanPath(value) @@ -424,6 +436,51 @@ func (c *EventActionFilesystemConfig) validateRenames() error { return nil } +func (c *EventActionFilesystemConfig) validateDeletes() error { + if len(c.Deletes) == 0 { + return util.NewValidationError("no path to delete specified") + } + for idx, val := range c.Deletes { + val = strings.TrimSpace(val) + if val == "" { + return util.NewValidationError("invalid path to delete") + } + c.Deletes[idx] = util.CleanPath(val) + } + c.Deletes = util.RemoveDuplicates(c.Deletes, false) + return nil +} + +func (c *EventActionFilesystemConfig) validateMkdirs() error { + if len(c.MkDirs) == 0 { + return util.NewValidationError("no directory to create specified") + } + for idx, val := range c.MkDirs { + val = strings.TrimSpace(val) + if val == "" { + return util.NewValidationError("invalid directory to create") + } + c.MkDirs[idx] = util.CleanPath(val) + } + c.MkDirs = util.RemoveDuplicates(c.MkDirs, false) + return nil +} + +func (c *EventActionFilesystemConfig) validateExist() error { + if len(c.Exist) == 0 { + return util.NewValidationError("no path to check for existence specified") + } + for idx, val := range c.Exist { + val = strings.TrimSpace(val) + if val == "" { + return util.NewValidationError("invalid path to check for existence") + } + c.Exist[idx] = util.CleanPath(val) + } + c.Exist = util.RemoveDuplicates(c.Exist, false) + return nil +} + func (c *EventActionFilesystemConfig) validate() error { if !isFilesystemActionValid(c.Type) { return util.NewValidationError(fmt.Sprintf("invalid filesystem action type: %d", c.Type)) @@ -432,37 +489,31 @@ func (c *EventActionFilesystemConfig) validate() error { case FilesystemActionRename: c.MkDirs = nil c.Deletes = nil + c.Exist = nil if err := c.validateRenames(); err != nil { return err } case FilesystemActionDelete: c.Renames = nil c.MkDirs = nil - if len(c.Deletes) == 0 { - return util.NewValidationError("no item to delete specified") + c.Exist = nil + if err := c.validateDeletes(); err != nil { + return err } - for idx, val := range c.Deletes { - val = strings.TrimSpace(val) - if val == "" { - return util.NewValidationError("invalid item to delete") - } - c.Deletes[idx] = util.CleanPath(val) - } - c.Deletes = util.RemoveDuplicates(c.Deletes, false) case FilesystemActionMkdirs: c.Renames = nil c.Deletes = nil - if len(c.MkDirs) == 0 { - return util.NewValidationError("no directory to create specified") + c.Exist = nil + if err := c.validateMkdirs(); err != nil { + return err } - for idx, val := range c.MkDirs { - val = strings.TrimSpace(val) - if val == "" { - return util.NewValidationError("invalid directory to create") - } - c.MkDirs[idx] = util.CleanPath(val) + case FilesystemActionExist: + c.Renames = nil + c.Deletes = nil + c.MkDirs = nil + if err := c.validateExist(); err != nil { + return err } - c.MkDirs = util.RemoveDuplicates(c.MkDirs, false) } return nil } @@ -472,12 +523,15 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig { copy(mkdirs, c.MkDirs) deletes := make([]string, len(c.Deletes)) copy(deletes, c.Deletes) + exist := make([]string, len(c.Exist)) + copy(exist, c.Exist) return EventActionFilesystemConfig{ Type: c.Type, Renames: cloneKeyValues(c.Renames), MkDirs: mkdirs, Deletes: deletes, + Exist: exist, } } diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 90cb2740..c831bf65 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -1598,7 +1598,7 @@ func TestEventActionValidation(t *testing.T) { } _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest) assert.NoError(t, err) - assert.Contains(t, string(resp), "no items to rename specified") + assert.Contains(t, string(resp), "no path to rename specified") action.Options.FsConfig.Renames = []dataprovider.KeyValue{ { Key: "", @@ -1607,7 +1607,7 @@ func TestEventActionValidation(t *testing.T) { } _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest) assert.NoError(t, err) - assert.Contains(t, string(resp), "invalid items to rename") + assert.Contains(t, string(resp), "invalid paths to rename") action.Options.FsConfig.Renames = []dataprovider.KeyValue{ { Key: "adir", @@ -1637,11 +1637,19 @@ func TestEventActionValidation(t *testing.T) { action.Options.FsConfig.Type = dataprovider.FilesystemActionDelete _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest) assert.NoError(t, err) - assert.Contains(t, string(resp), "no item to delete specified") + assert.Contains(t, string(resp), "no path to delete specified") action.Options.FsConfig.Deletes = []string{"item1", ""} _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest) assert.NoError(t, err) - assert.Contains(t, string(resp), "invalid item to delete") + assert.Contains(t, string(resp), "invalid path to delete") + action.Options.FsConfig.Type = dataprovider.FilesystemActionExist + _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest) + assert.NoError(t, err) + assert.Contains(t, string(resp), "no path to check for existence specified") + action.Options.FsConfig.Exist = []string{"item1", ""} + _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest) + assert.NoError(t, err) + assert.Contains(t, string(resp), "invalid path to check for existence") } func TestEventRuleValidation(t *testing.T) { @@ -18906,6 +18914,34 @@ func TestWebEventAction(t *testing.T) { } } + action.Options.FsConfig = dataprovider.EventActionFilesystemConfig{ + Type: dataprovider.FilesystemActionExist, + Exist: []string{"b ", " c/d"}, + } + form.Set("fs_action_type", fmt.Sprintf("%d", action.Options.FsConfig.Type)) + form.Set("fs_exist_paths", strings.Join(action.Options.FsConfig.Exist, ",")) + req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name), + bytes.NewBuffer([]byte(form.Encode()))) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, webToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr) + // check the update + actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK) + assert.NoError(t, err) + assert.Equal(t, action.Type, actionGet.Type) + if assert.Len(t, actionGet.Options.FsConfig.Exist, 2) { + for _, p := range actionGet.Options.FsConfig.Exist { + switch p { + case "/b": + case "/c/d": + default: + t.Errorf("unexpected path %v", p) + } + } + } + req, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventActionPath, action.Name), nil) assert.NoError(t, err) setBearerForReq(req, apiToken) @@ -18929,7 +18965,13 @@ func TestWebEventRule(t *testing.T) { assert.NoError(t, err) a := dataprovider.BaseEventAction{ Name: "web_action", - Type: dataprovider.ActionTypeBackup, + Type: dataprovider.ActionTypeFilesystem, + Options: dataprovider.BaseEventActionOptions{ + FsConfig: dataprovider.EventActionFilesystemConfig{ + Type: dataprovider.FilesystemActionExist, + Exist: []string{"/dir1"}, + }, + }, } action, _, err := httpdtest.AddEventAction(a, http.StatusCreated) assert.NoError(t, err) diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 21898e44..207b2002 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -1919,6 +1919,7 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven Renames: getKeyValsFromPostFields(r, "fs_rename_source", "fs_rename_target"), Deletes: strings.Split(strings.ReplaceAll(r.Form.Get("fs_delete_paths"), " ", ""), ","), MkDirs: strings.Split(strings.ReplaceAll(r.Form.Get("fs_mkdir_paths"), " ", ""), ","), + Exist: strings.Split(strings.ReplaceAll(r.Form.Get("fs_exist_paths"), " ", ""), ","), }, } return options, nil diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go index 2b978a2c..7431fbfe 100644 --- a/internal/httpdtest/httpdtest.go +++ b/internal/httpdtest/httpdtest.go @@ -2270,6 +2270,14 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF return errors.New("fs mkdir content mismatch") } } + if len(expected.Exist) != len(actual.Exist) { + return errors.New("fs exist mismatch") + } + for _, v := range expected.Exist { + if !util.Contains(actual.Exist, v) { + return errors.New("fs exist content mismatch") + } + } return nil } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 6f202046..946039d2 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -4356,6 +4356,19 @@ components: * `7` - Transfer quota reset * `8` - Data retention check * `9` - Filesystem + FilesystemActionTypes: + type: integer + enum: + - 1 + - 2 + - 3 + - 4 + description: | + Supported filesystem action types: + * `1` - Rename + * `2` - Delete + * `3` - Mkdis + * `4` - Exist EventTriggerTypes: type: integer enum: @@ -6057,6 +6070,27 @@ components: type: array items: $ref: '#/components/schemas/FolderRetention' + EventActionFilesystemConfig: + type: object + properties: + type: + $ref: '#/components/schemas/FilesystemActionTypes' + renames: + type: array + items: + $ref: '#/components/schemas/KeyValue' + mkdirs: + type: array + items: + type: string + deletes: + type: array + items: + type: string + exist: + type: array + items: + type: string BaseEventActionOptions: type: object properties: @@ -6068,6 +6102,8 @@ components: $ref: '#/components/schemas/EventActionEmailConfig' retention_config: $ref: '#/components/schemas/EventActionDataRetentionConfig' + fs_config: + $ref: '#/components/schemas/EventActionFilesystemConfig' BaseEventAction: type: object properties: diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html index 589fca6c..ab79ec4a 100644 --- a/templates/webadmin/eventaction.html +++ b/templates/webadmin/eventaction.html @@ -487,6 +487,17 @@ along with this program. If not, see . +
+ +
+ + + Comma separated paths to check for existence as seen by SFTPGo users. Placeholders are supported. The required permissions are granted automatically + +
+
+
@@ -748,6 +759,10 @@ along with this program. If not, see . case 3: $('.action-fs-mkdir').show(); break; + case '4': + case 4: + $('.action-fs-exist').show(); + break; } }