diff --git a/docs/eventmanager.md b/docs/eventmanager.md
index afbd361c..91bf9569 100644
--- a/docs/eventmanager.md
+++ b/docs/eventmanager.md
@@ -18,6 +18,7 @@ The following actions are supported:
- `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.
+ - `Compress paths`. You can compress (currently as zip) ore or more files and directories.
The following placeholders are supported:
diff --git a/go.mod b/go.mod
index 8f55dc17..fb8a078a 100644
--- a/go.mod
+++ b/go.mod
@@ -34,7 +34,7 @@ require (
github.com/hashicorp/go-hclog v1.3.1
github.com/hashicorp/go-plugin v1.4.5
github.com/hashicorp/go-retryablehttp v0.7.1
- github.com/jackc/pgx/v5 v5.0.1
+ github.com/jackc/pgx/v5 v5.0.2
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.15.11
github.com/lestrrat-go/jwx v1.2.25
diff --git a/go.sum b/go.sum
index ce4694c7..fbb64325 100644
--- a/go.sum
+++ b/go.sum
@@ -1014,8 +1014,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
-github.com/jackc/pgx/v5 v5.0.1 h1:JZu9othr7l8so2JMDAGeDUMXqERAuZpovyfl4H50tdg=
-github.com/jackc/pgx/v5 v5.0.1/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o=
+github.com/jackc/pgx/v5 v5.0.2 h1:V+EonE9i33VwJR9YIHRdglAmrODLLkwIdHjko6b1rRk=
+github.com/jackc/pgx/v5 v5.0.2/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
diff --git a/internal/common/connection.go b/internal/common/connection.go
index 5e3495cd..43b0b7f0 100644
--- a/internal/common/connection.go
+++ b/internal/common/connection.go
@@ -608,8 +608,9 @@ func (c *BaseConnection) getPathForSetStatPerms(fs vfs.Fs, fsPath, virtualPath s
return pathForPerms
}
-// DoStat execute a Stat if mode = 0, Lstat if mode = 1
-func (c *BaseConnection) DoStat(virtualPath string, mode int, checkFilePatterns bool) (os.FileInfo, error) {
+func (c *BaseConnection) doStatInternal(virtualPath string, mode int, checkFilePatterns,
+ convertResult bool,
+) (os.FileInfo, error) {
// for some vfs we don't create intermediary folders so we cannot simply check
// if virtualPath is a virtual folder
vfolders := c.User.GetVirtualFoldersInPath(path.Dir(virtualPath))
@@ -639,12 +640,17 @@ func (c *BaseConnection) DoStat(virtualPath string, mode int, checkFilePatterns
c.Log(logger.LevelWarn, "stat error for path %#v: %+v", virtualPath, err)
return info, c.GetFsError(fs, err)
}
- if vfs.IsCryptOsFs(fs) {
+ if convertResult && vfs.IsCryptOsFs(fs) {
info = fs.(*vfs.CryptFs).ConvertFileInfo(info)
}
return info, nil
}
+// DoStat execute a Stat if mode = 0, Lstat if mode = 1
+func (c *BaseConnection) DoStat(virtualPath string, mode int, checkFilePatterns bool) (os.FileInfo, error) {
+ return c.doStatInternal(virtualPath, mode, checkFilePatterns, true)
+}
+
func (c *BaseConnection) createDirIfMissing(name string) error {
_, err := c.DoStat(name, 0, false)
if c.IsNotExistError(err) {
diff --git a/internal/common/connection_test.go b/internal/common/connection_test.go
index 22256245..061d5bed 100644
--- a/internal/common/connection_test.go
+++ b/internal/common/connection_test.go
@@ -319,7 +319,7 @@ func TestErrorsMapping(t *testing.T) {
fs := vfs.NewOsFs("", os.TempDir(), "")
conn := NewBaseConnection("", ProtocolSFTP, "", "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})
osErrorsProtocols := []string{ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare,
- ProtocolDataRetention, ProtocolOIDC}
+ ProtocolDataRetention, ProtocolOIDC, protocolEventAction}
for _, protocol := range supportedProtocols {
conn.SetProtocol(protocol)
err := conn.GetFsError(fs, os.ErrNotExist)
diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go
index 029936c2..17c2377e 100644
--- a/internal/common/eventmanager.go
+++ b/internal/common/eventmanager.go
@@ -621,6 +621,161 @@ func getCSVRetentionReport(results []folderRetentionCheckResult) ([]byte, error)
return b.Bytes(), err
}
+func closeWriterAndUpdateQuota(w io.WriteCloser, conn *BaseConnection, virtualPath string, numFiles int,
+ truncatedSize int64, errTransfer error,
+) error {
+ errWrite := w.Close()
+ info, err := conn.doStatInternal(virtualPath, 0, false, false)
+ if err == nil {
+ updateUserQuotaAfterFileWrite(conn, virtualPath, numFiles, info.Size()-truncatedSize)
+ _, fsPath, errFs := conn.GetFsAndResolvedPath(virtualPath)
+ if errFs == nil {
+ if errTransfer == nil {
+ errTransfer = errWrite
+ }
+ ExecuteActionNotification(conn, operationUpload, fsPath, virtualPath, "", "", "", info.Size(), errTransfer) //nolint:errcheck
+ }
+ } else {
+ eventManagerLog(logger.LevelWarn, "unable to update quota after writing %q: %v", virtualPath, err)
+ }
+ return errWrite
+}
+
+func updateUserQuotaAfterFileWrite(conn *BaseConnection, virtualPath string, numFiles int, fileSize int64) {
+ vfolder, err := conn.User.GetVirtualFolderForPath(path.Dir(virtualPath))
+ if err != nil {
+ dataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck
+ return
+ }
+ dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, numFiles, fileSize, false) //nolint:errcheck
+ if vfolder.IsIncludedInUserQuota() {
+ dataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck
+ }
+}
+
+func getFileWriter(conn *BaseConnection, virtualPath string) (io.WriteCloser, int, int64, func(), error) {
+ fs, fsPath, err := conn.GetFsAndResolvedPath(virtualPath)
+ if err != nil {
+ return nil, 0, 0, nil, err
+ }
+ var truncatedSize, fileSize int64
+ numFiles := 1
+ isFileOverwrite := false
+
+ info, err := fs.Lstat(fsPath)
+ if err == nil {
+ fileSize = info.Size()
+ if info.IsDir() {
+ return nil, numFiles, truncatedSize, nil, fmt.Errorf("cannot write to a directory: %q", virtualPath)
+ }
+ if info.Mode().IsRegular() {
+ isFileOverwrite = true
+ truncatedSize = fileSize
+ }
+ numFiles = 0
+ }
+ if err != nil && !fs.IsNotExist(err) {
+ return nil, numFiles, truncatedSize, nil, conn.GetFsError(fs, err)
+ }
+ f, w, cancelFn, err := fs.Create(fsPath, 0)
+ if err != nil {
+ return nil, numFiles, truncatedSize, nil, conn.GetFsError(fs, err)
+ }
+ vfs.SetPathPermissions(fs, fsPath, conn.User.GetUID(), conn.User.GetGID())
+
+ if isFileOverwrite {
+ if vfs.HasTruncateSupport(fs) || vfs.IsCryptOsFs(fs) {
+ updateUserQuotaAfterFileWrite(conn, virtualPath, numFiles, -fileSize)
+ truncatedSize = 0
+ }
+ }
+ if cancelFn == nil {
+ cancelFn = func() {}
+ }
+ if f != nil {
+ return f, numFiles, truncatedSize, cancelFn, nil
+ }
+ return w, numFiles, truncatedSize, cancelFn, nil
+}
+
+func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir string) error {
+ if entryPath == wr.Name {
+ // skip the archive itself
+ return nil
+ }
+ info, err := conn.DoStat(entryPath, 1, false)
+ if err != nil {
+ eventManagerLog(logger.LevelError, "unable to add zip entry %#v, stat error: %v", entryPath, err)
+ return err
+ }
+ entryName, err := getZipEntryName(entryPath, baseDir)
+ if err != nil {
+ eventManagerLog(logger.LevelError, "unable to get zip entry name: %v", err)
+ return err
+ }
+ if _, ok := wr.Entries[entryName]; ok {
+ eventManagerLog(logger.LevelInfo, "skipping duplicate zip entry %q, is dir %t", entryPath, info.IsDir())
+ return nil
+ }
+ wr.Entries[entryName] = true
+ if info.IsDir() {
+ _, err = wr.Writer.CreateHeader(&zip.FileHeader{
+ Name: entryName + "/",
+ Method: zip.Deflate,
+ Modified: info.ModTime(),
+ })
+ if err != nil {
+ eventManagerLog(logger.LevelError, "unable to create zip entry %q: %v", entryPath, err)
+ return fmt.Errorf("unable to create zip entry %q: %w", entryPath, err)
+ }
+ contents, err := conn.ListDir(entryPath)
+ if err != nil {
+ eventManagerLog(logger.LevelError, "unable to add zip entry %q, read dir error: %v", entryPath, err)
+ return fmt.Errorf("unable to add zip entry %q: %w", entryPath, err)
+ }
+ for _, info := range contents {
+ fullPath := util.CleanPath(path.Join(entryPath, info.Name()))
+ if err := addZipEntry(wr, conn, fullPath, baseDir); err != nil {
+ eventManagerLog(logger.LevelError, "unable to add zip entry: %v", err)
+ return err
+ }
+ }
+ return nil
+ }
+ if !info.Mode().IsRegular() {
+ // we only allow regular files
+ eventManagerLog(logger.LevelInfo, "skipping zip entry for non regular file %q", entryPath)
+ return nil
+ }
+ reader, cancelFn, err := getFileReader(conn, entryPath)
+ if err != nil {
+ eventManagerLog(logger.LevelError, "unable to add zip entry %q, cannot open file: %v", entryPath, err)
+ return fmt.Errorf("unable to open %q: %w", entryPath, err)
+ }
+ defer cancelFn()
+ defer reader.Close()
+
+ f, err := wr.Writer.CreateHeader(&zip.FileHeader{
+ Name: entryName,
+ Method: zip.Deflate,
+ Modified: info.ModTime(),
+ })
+ if err != nil {
+ eventManagerLog(logger.LevelError, "unable to create zip entry %q: %v", entryPath, err)
+ return fmt.Errorf("unable to create zip entry %q: %w", entryPath, err)
+ }
+ _, err = io.Copy(f, reader)
+ return err
+}
+
+func getZipEntryName(entryPath, baseDir string) (string, error) {
+ if !strings.HasPrefix(entryPath, baseDir) {
+ return "", fmt.Errorf("entry path %q is outside base dir %q", entryPath, baseDir)
+ }
+ entryPath = strings.TrimPrefix(entryPath, baseDir)
+ return strings.TrimPrefix(entryPath, "/"), nil
+}
+
func getFileReader(conn *BaseConnection, virtualPath string) (io.ReadCloser, func(), error) {
fs, fsPath, err := conn.GetFsAndResolvedPath(virtualPath)
if err != nil {
@@ -628,7 +783,7 @@ func getFileReader(conn *BaseConnection, virtualPath string) (io.ReadCloser, fun
}
f, r, cancelFn, err := fs.Open(fsPath, 0)
if err != nil {
- return nil, nil, err
+ return nil, nil, conn.GetFsError(fs, err)
}
if cancelFn == nil {
cancelFn = func() {}
@@ -1035,8 +1190,13 @@ func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
eventManagerLog(logger.LevelError, "unable to get group for user %q: %+v", user.Username, err)
return dataprovider.User{}, fmt.Errorf("unable to get groups for user %q", user.Username)
}
+ user.UploadDataTransfer = 0
+ user.UploadBandwidth = 0
+ user.DownloadBandwidth = 0
user.Filters.DisableFsChecks = false
user.Filters.FilePatterns = nil
+ user.Filters.BandwidthLimits = nil
+ user.Filters.DataTransferLimits = nil
for k := range user.Permissions {
user.Permissions[k] = []string{dataprovider.PermAny}
}
@@ -1279,6 +1439,72 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string
return nil
}
+func getArchiveBaseDir(paths []string) string {
+ var parentDirs []string
+ for _, p := range paths {
+ parentDirs = append(parentDirs, path.Dir(p))
+ }
+ parentDirs = util.RemoveDuplicates(parentDirs, false)
+ baseDir := "/"
+ if len(parentDirs) == 1 {
+ baseDir = parentDirs[0]
+ }
+ return baseDir
+}
+
+func executeCompressFsActionForUser(c dataprovider.EventActionFsCompress, 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 fmt.Errorf("compress error, unable to check root fs for user %q: %w", user.Username, err)
+ }
+ conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
+ name := util.CleanPath(replaceWithReplacer(c.Name, replacer))
+ paths := make([]string, 0, len(c.Paths))
+ for idx := range c.Paths {
+ p := util.CleanPath(replaceWithReplacer(c.Paths[idx], replacer))
+ if p == name {
+ return fmt.Errorf("cannot compress the archive to create: %q", name)
+ }
+ paths = append(paths, p)
+ }
+ writer, numFiles, truncatedSize, cancelFn, err := getFileWriter(conn, name)
+ if err != nil {
+ eventManagerLog(logger.LevelError, "unable to create archive %q: %v", name, err)
+ return fmt.Errorf("unable to create archive: %w", err)
+ }
+ defer cancelFn()
+
+ paths = util.RemoveDuplicates(paths, false)
+ baseDir := getArchiveBaseDir(paths)
+ eventManagerLog(logger.LevelDebug, "creating archive %q for paths %+v", name, paths)
+
+ zipWriter := &zipWriterWrapper{
+ Name: name,
+ Writer: zip.NewWriter(writer),
+ Entries: make(map[string]bool),
+ }
+ for _, item := range paths {
+ if err := addZipEntry(zipWriter, conn, item, baseDir); err != nil {
+ closeWriterAndUpdateQuota(writer, conn, name, numFiles, truncatedSize, err) //nolint:errcheck
+ return err
+ }
+ }
+ if err := zipWriter.Writer.Close(); err != nil {
+ eventManagerLog(logger.LevelError, "unable to close zip file %q: %v", name, err)
+ closeWriterAndUpdateQuota(writer, conn, name, numFiles, truncatedSize, err) //nolint:errcheck
+ return fmt.Errorf("unable to close zip file %q: %w", name, err)
+ }
+ return closeWriterAndUpdateQuota(writer, conn, name, numFiles, truncatedSize, err)
+}
+
func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, conditions dataprovider.ConditionOptions,
params *EventParams,
) error {
@@ -1319,6 +1545,46 @@ func executeExistFsRuleAction(exist []string, replacer *strings.Replacer, condit
return nil
}
+func executeCompressFsRuleAction(c dataprovider.EventActionFsCompress, 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 == "" {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping fs compress for user %s, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if !checkEventGroupConditionPatters(user.Groups, conditions.GroupNames) {
+ eventManagerLog(logger.LevelDebug, "skipping fs compress for user %s, group name conditions don't match",
+ user.Username)
+ continue
+ }
+ }
+ executed++
+ if err = executeCompressFsActionForUser(c, replacer, user); err != nil {
+ failures = append(failures, user.Username)
+ params.AddError(err)
+ continue
+ }
+ }
+ if len(failures) > 0 {
+ return fmt.Errorf("fs compress failed for users: %+v", failures)
+ }
+ if executed == 0 {
+ eventManagerLog(logger.LevelError, "no file/folder compressed")
+ return errors.New("no file/folder compressed")
+ }
+ return nil
+}
+
func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions dataprovider.ConditionOptions,
params *EventParams,
) error {
@@ -1334,6 +1600,8 @@ func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions
return executeMkdirFsRuleAction(c.MkDirs, replacer, conditions, params)
case dataprovider.FilesystemActionExist:
return executeExistFsRuleAction(c.Exist, replacer, conditions, params)
+ case dataprovider.FilesystemActionCompress:
+ return executeCompressFsRuleAction(c.Compress, replacer, conditions, params)
default:
return fmt.Errorf("unsupported filesystem action %d", c.Type)
}
@@ -1818,6 +2086,12 @@ func (j *eventCronJob) Run() {
eventManagerLog(logger.LevelDebug, "execution for scheduled rule %q finished", j.ruleName)
}
+type zipWriterWrapper struct {
+ Name string
+ Entries map[string]bool
+ Writer *zip.Writer
+}
+
func eventManagerLog(level logger.LogLevel, format string, v ...any) {
logger.Log(level, "eventmanager", "", format, v...)
}
diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go
index a7224530..0f32434f 100644
--- a/internal/common/eventmanager_test.go
+++ b/internal/common/eventmanager_test.go
@@ -15,6 +15,7 @@
package common
import (
+ "bytes"
"crypto/rand"
"fmt"
"io"
@@ -28,6 +29,7 @@ import (
"testing"
"time"
+ "github.com/klauspost/compress/zip"
"github.com/sftpgo/sdk"
sdkkms "github.com/sftpgo/sdk/kms"
"github.com/stretchr/testify/assert"
@@ -333,6 +335,8 @@ func TestEventManagerErrors(t *testing.T) {
assert.Error(t, err)
err = executeExistFsRuleAction(nil, nil, dataprovider.ConditionOptions{}, &EventParams{})
assert.Error(t, err)
+ err = executeCompressFsRuleAction(dataprovider.EventActionFsCompress{}, nil, dataprovider.ConditionOptions{}, &EventParams{})
+ assert.Error(t, err)
groupName := "agroup"
err = executeQuotaResetForUser(dataprovider.User{
@@ -398,6 +402,15 @@ func TestEventManagerErrors(t *testing.T) {
},
})
assert.Error(t, err)
+ err = executeCompressFsActionForUser(dataprovider.EventActionFsCompress{}, nil, dataprovider.User{
+ Groups: []sdk.GroupMapping{
+ {
+ Name: groupName,
+ Type: sdk.GroupTypePrimary,
+ },
+ },
+ })
+ assert.Error(t, err)
_, err = getMailAttachments(dataprovider.User{
Groups: []sdk.GroupMapping{
{
@@ -576,9 +589,8 @@ func TestEventRuleActions(t *testing.T) {
},
}
err = executeRuleAction(action, params, dataprovider.ConditionOptions{})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "error getting user")
- }
+ assert.Contains(t, getErrorString(err), "error getting user")
+
action.Options.HTTPConfig.Parts = nil
action.Options.HTTPConfig.Body = "{{ObjectData}}"
// test disk and transfer quota reset
@@ -656,9 +668,8 @@ func TestEventRuleActions(t *testing.T) {
},
},
})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "no user quota reset executed")
- }
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no user quota reset executed")
action = dataprovider.BaseEventAction{
Type: dataprovider.ActionTypeMetadataCheck,
@@ -671,9 +682,8 @@ func TestEventRuleActions(t *testing.T) {
},
},
})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "no metadata check executed")
- }
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no metadata check executed")
err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
Names: []dataprovider.ConditionPattern{
@@ -784,9 +794,9 @@ func TestEventRuleActions(t *testing.T) {
},
},
})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "no retention check executed")
- }
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no retention check executed")
+
// test file exists action
action = dataprovider.BaseEventAction{
Type: dataprovider.ActionTypeFilesystem,
@@ -804,9 +814,9 @@ func TestEventRuleActions(t *testing.T) {
},
},
})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "no existence check executed")
- }
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no existence check executed")
+
err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
Names: []dataprovider.ConditionPattern{
{
@@ -852,9 +862,9 @@ func TestEventRuleActions(t *testing.T) {
},
},
})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "no transfer quota reset executed")
- }
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no transfer quota reset executed")
+
action.Type = dataprovider.ActionTypeFilesystem
action.Options = dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
@@ -874,9 +884,9 @@ func TestEventRuleActions(t *testing.T) {
},
},
})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "no rename executed")
- }
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no rename executed")
+
action.Options = dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionDelete,
@@ -890,9 +900,9 @@ func TestEventRuleActions(t *testing.T) {
},
},
})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "no delete executed")
- }
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no delete executed")
+
action.Options = dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionMkdirs,
@@ -906,9 +916,37 @@ func TestEventRuleActions(t *testing.T) {
},
},
})
- if assert.Error(t, err) {
- assert.Contains(t, err.Error(), "no mkdir executed")
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no mkdir executed")
+
+ action.Options = dataprovider.BaseEventActionOptions{
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionCompress,
+ Compress: dataprovider.EventActionFsCompress{
+ Name: "test.zip",
+ Paths: []string{"/{{VirtualPath}}"},
+ },
+ },
}
+ err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: "no match",
+ },
+ },
+ })
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no file/folder compressed")
+
+ err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
+ GroupNames: []dataprovider.ConditionPattern{
+ {
+ Pattern: "no match",
+ },
+ },
+ })
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "no file/folder compressed")
err = dataprovider.DeleteUser(username1, "", "")
assert.NoError(t, err)
@@ -1164,6 +1202,10 @@ func TestFilesystemActionErrors(t *testing.T) {
assert.Error(t, err)
err = executeExistFsActionForUser(nil, testReplacer, user)
assert.Error(t, err)
+ err = executeCompressFsActionForUser(dataprovider.EventActionFsCompress{}, testReplacer, user)
+ assert.Error(t, err)
+ _, _, _, _, err = getFileWriter(conn, "/path.txt") //nolint:dogsled
+ assert.Error(t, err)
err = executeEmailRuleAction(dataprovider.EventActionEmailConfig{
Recipients: []string{"test@example.net"},
Subject: "subject",
@@ -1230,7 +1272,7 @@ func TestFilesystemActionErrors(t *testing.T) {
err := os.MkdirAll(dirPath, os.ModePerm)
assert.NoError(t, err)
filePath := filepath.Join(dirPath, "f.dat")
- err = os.WriteFile(filePath, nil, 0666)
+ err = os.WriteFile(filePath, []byte("test file content"), 0666)
assert.NoError(t, err)
err = os.Chmod(dirPath, 0001)
assert.NoError(t, err)
@@ -1290,6 +1332,16 @@ func TestFilesystemActionErrors(t *testing.T) {
err = os.Chmod(dirPath, os.ModePerm)
assert.NoError(t, err)
+
+ conn = NewBaseConnection("", protocolEventAction, "", "", user)
+ wr := &zipWriterWrapper{
+ Name: "test.zip",
+ Writer: zip.NewWriter(bytes.NewBuffer(nil)),
+ Entries: map[string]bool{},
+ }
+ err = addZipEntry(wr, conn, "/adir/sub/f.dat", "/adir/sub/sub")
+ assert.Error(t, err)
+ assert.Contains(t, getErrorString(err), "is outside base dir")
}
err = dataprovider.DeleteUser(username, "", "")
@@ -1545,3 +1597,10 @@ func TestWriteHTTPPartsError(t *testing.T) {
}, nil, nil, nil, &EventParams{})
assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
}
+
+func getErrorString(err error) string {
+ if err == nil {
+ return ""
+ }
+ return err.Error()
+}
diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go
index cac1bad8..355b912b 100644
--- a/internal/common/protocol_test.go
+++ b/internal/common/protocol_test.go
@@ -38,6 +38,7 @@ import (
_ "github.com/jackc/pgx/v5/stdlib"
_ "github.com/mattn/go-sqlite3"
"github.com/mhale/smtpd"
+ "github.com/minio/sio"
"github.com/pkg/sftp"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
@@ -4108,6 +4109,385 @@ func TestEventActionHTTPMultipart(t *testing.T) {
assert.NoError(t, err)
}
+func TestEventActionCompress(t *testing.T) {
+ a1 := dataprovider.BaseEventAction{
+ Name: "action1",
+ Type: dataprovider.ActionTypeFilesystem,
+ Options: dataprovider.BaseEventActionOptions{
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionCompress,
+ Compress: dataprovider.EventActionFsCompress{
+ Name: "/{{VirtualPath}}.zip",
+ Paths: []string{"/{{VirtualPath}}"},
+ },
+ },
+ },
+ }
+ action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
+ assert.NoError(t, err)
+ r1 := dataprovider.EventRule{
+ Name: "test compress",
+ Trigger: dataprovider.EventTriggerFsEvent,
+ Conditions: dataprovider.EventConditions{
+ FsEvents: []string{"upload"},
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action1.Name,
+ },
+ Order: 1,
+ Options: dataprovider.EventActionOptions{
+ ExecuteSync: true,
+ },
+ },
+ },
+ }
+ rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
+ assert.NoError(t, err)
+
+ u := getTestUser()
+ u.QuotaFiles = 1000
+ localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
+ assert.NoError(t, err)
+ u = getTestSFTPUser()
+ u.FsConfig.SFTPConfig.BufferSize = 1
+ u.QuotaFiles = 1000
+ sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
+ assert.NoError(t, err)
+ u = getCryptFsUser()
+ u.QuotaFiles = 1000
+ cryptFsUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
+ assert.NoError(t, err)
+ for _, user := range []dataprovider.User{localUser, sftpUser, cryptFsUser} {
+ // cleanup home dir
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+ rule1.Conditions.Options.Names = []dataprovider.ConditionPattern{
+ {
+ Pattern: user.Username,
+ },
+ }
+ _, _, err = httpdtest.UpdateEventRule(rule1, http.StatusOK)
+ assert.NoError(t, err)
+
+ conn, client, err := getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+
+ expectedQuotaSize := int64(len(testFileContent))
+ expectedQuotaFiles := 1
+ if user.Username == cryptFsUser.Username {
+ encryptedFileSize, err := getEncryptedFileSize(expectedQuotaSize)
+ assert.NoError(t, err)
+ expectedQuotaSize = encryptedFileSize
+ }
+
+ f, err := client.Create(testFileName)
+ assert.NoError(t, err)
+ _, err = f.Write(testFileContent)
+ assert.NoError(t, err)
+ err = f.Close()
+ assert.NoError(t, err)
+ info, err := client.Stat(testFileName + ".zip")
+ if assert.NoError(t, err) {
+ assert.Greater(t, info.Size(), int64(0))
+ // check quota
+ archiveSize := info.Size()
+ if user.Username == cryptFsUser.Username {
+ encryptedFileSize, err := getEncryptedFileSize(archiveSize)
+ assert.NoError(t, err)
+ archiveSize = encryptedFileSize
+ }
+ user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+ assert.NoError(t, err)
+ assert.Equal(t, expectedQuotaFiles+1, user.UsedQuotaFiles,
+ "quota file does no match for user %q", user.Username)
+ assert.Equal(t, expectedQuotaSize+archiveSize, user.UsedQuotaSize,
+ "quota size does no match for user %q", user.Username)
+ }
+ // now overwrite the same file
+ f, err = client.Create(testFileName)
+ assert.NoError(t, err)
+ _, err = f.Write(testFileContent)
+ assert.NoError(t, err)
+ err = f.Close()
+ assert.NoError(t, err)
+ info, err = client.Stat(testFileName + ".zip")
+ if assert.NoError(t, err) {
+ assert.Greater(t, info.Size(), int64(0))
+ archiveSize := info.Size()
+ if user.Username == cryptFsUser.Username {
+ encryptedFileSize, err := getEncryptedFileSize(archiveSize)
+ assert.NoError(t, err)
+ archiveSize = encryptedFileSize
+ }
+ user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+ assert.NoError(t, err)
+ assert.Equal(t, expectedQuotaFiles+1, user.UsedQuotaFiles,
+ "quota file after overwrite does no match for user %q", user.Username)
+ assert.Equal(t, expectedQuotaSize+archiveSize, user.UsedQuotaSize,
+ "quota size after overwrite does no match for user %q", user.Username)
+ }
+ }
+ if user.Username == localUser.Username {
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+ }
+ }
+
+ _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveUser(localUser, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(localUser.GetHomeDir())
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveUser(cryptFsUser, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(cryptFsUser.GetHomeDir())
+ assert.NoError(t, err)
+}
+
+func TestEventActionCompressQuotaFolder(t *testing.T) {
+ testDir := "/folder"
+ a1 := dataprovider.BaseEventAction{
+ Name: "action1",
+ Type: dataprovider.ActionTypeFilesystem,
+ Options: dataprovider.BaseEventActionOptions{
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionCompress,
+ Compress: dataprovider.EventActionFsCompress{
+ Name: "/{{VirtualPath}}.zip",
+ Paths: []string{"/{{VirtualPath}}", testDir},
+ },
+ },
+ },
+ }
+ action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
+ assert.NoError(t, err)
+ r1 := dataprovider.EventRule{
+ Name: "test compress",
+ Trigger: dataprovider.EventTriggerFsEvent,
+ Conditions: dataprovider.EventConditions{
+ FsEvents: []string{"upload"},
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action1.Name,
+ },
+ Order: 1,
+ Options: dataprovider.EventActionOptions{
+ ExecuteSync: true,
+ },
+ },
+ },
+ }
+ rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
+ assert.NoError(t, err)
+ u := getTestUser()
+ u.QuotaFiles = 1000
+ mappedPath := filepath.Join(os.TempDir(), "virtualpath")
+ folderName := filepath.Base(mappedPath)
+ vdirPath := "/virtualpath"
+ u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
+ BaseVirtualFolder: vfs.BaseVirtualFolder{
+ Name: folderName,
+ MappedPath: mappedPath,
+ },
+ VirtualPath: vdirPath,
+ QuotaSize: -1,
+ QuotaFiles: -1,
+ })
+ user, _, err := httpdtest.AddUser(u, http.StatusCreated)
+ assert.NoError(t, err)
+ conn, client, err := getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+
+ err = client.Mkdir(testDir)
+ assert.NoError(t, err)
+ expectedQuotaSize := int64(len(testFileContent))
+ expectedQuotaFiles := 1
+ err = client.Symlink(path.Join(testDir, testFileName), path.Join(testDir, testFileName+"_link"))
+ assert.NoError(t, err)
+ f, err := client.Create(path.Join(testDir, testFileName))
+ assert.NoError(t, err)
+ _, err = f.Write(testFileContent)
+ assert.NoError(t, err)
+ err = f.Close()
+ assert.NoError(t, err)
+ info, err := client.Stat(path.Join(testDir, testFileName) + ".zip")
+ if assert.NoError(t, err) {
+ assert.Greater(t, info.Size(), int64(0))
+ user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+ assert.NoError(t, err)
+ expectedQuotaFiles++
+ expectedQuotaSize += info.Size()
+ assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
+ assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
+ }
+ vfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, vfolder.UsedQuotaFiles)
+ assert.Equal(t, int64(0), vfolder.UsedQuotaSize)
+ // upload in the virtual path
+ f, err = client.Create(path.Join(vdirPath, testFileName))
+ assert.NoError(t, err)
+ _, err = f.Write(testFileContent)
+ assert.NoError(t, err)
+ err = f.Close()
+ assert.NoError(t, err)
+ info, err = client.Stat(path.Join(vdirPath, testFileName) + ".zip")
+ if assert.NoError(t, err) {
+ assert.Greater(t, info.Size(), int64(0))
+ user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
+ assert.NoError(t, err)
+ expectedQuotaFiles += 2
+ expectedQuotaSize += info.Size() + int64(len(testFileContent))
+ assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
+ assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
+ vfolder, _, err := httpdtest.GetFolderByName(folderName, http.StatusOK)
+ assert.NoError(t, err)
+ assert.Equal(t, 2, vfolder.UsedQuotaFiles)
+ assert.Equal(t, info.Size()+int64(len(testFileContent)), vfolder.UsedQuotaSize)
+ }
+ }
+
+ _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveUser(user, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(mappedPath)
+ assert.NoError(t, err)
+}
+
+func TestEventActionCompressErrors(t *testing.T) {
+ a1 := dataprovider.BaseEventAction{
+ Name: "action1",
+ Type: dataprovider.ActionTypeFilesystem,
+ Options: dataprovider.BaseEventActionOptions{
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionCompress,
+ Compress: dataprovider.EventActionFsCompress{
+ Name: "/{{VirtualPath}}.zip",
+ Paths: []string{"/{{VirtualPath}}.zip"}, // cannot compress itself
+ },
+ },
+ },
+ }
+ action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
+ assert.NoError(t, err)
+ r1 := dataprovider.EventRule{
+ Name: "test compress",
+ Trigger: dataprovider.EventTriggerFsEvent,
+ Conditions: dataprovider.EventConditions{
+ FsEvents: []string{"upload"},
+ },
+ Actions: []dataprovider.EventAction{
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action1.Name,
+ },
+ Order: 1,
+ Options: dataprovider.EventActionOptions{
+ ExecuteSync: true,
+ },
+ },
+ },
+ }
+ rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
+ assert.NoError(t, err)
+
+ user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
+ assert.NoError(t, err)
+ conn, client, err := getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+
+ f, err := client.Create(testFileName)
+ assert.NoError(t, err)
+ _, err = f.Write(testFileContent)
+ assert.NoError(t, err)
+ err = f.Close()
+ assert.Error(t, err)
+ }
+ // try to compress a missing file
+ action1.Options.FsConfig.Compress.Paths = []string{"/missing file"}
+ _, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)
+ assert.NoError(t, err)
+ conn, client, err = getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+
+ f, err := client.Create(testFileName)
+ assert.NoError(t, err)
+ _, err = f.Write(testFileContent)
+ assert.NoError(t, err)
+ err = f.Close()
+ assert.Error(t, err)
+ }
+ // try to overwrite a directory
+ testDir := "/adir"
+ action1.Options.FsConfig.Compress.Name = testDir
+ action1.Options.FsConfig.Compress.Paths = []string{"/{{VirtualPath}}"}
+ _, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)
+ assert.NoError(t, err)
+ conn, client, err = getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+
+ err = client.Mkdir(testDir)
+ assert.NoError(t, err)
+ f, err := client.Create(testFileName)
+ assert.NoError(t, err)
+ _, err = f.Write(testFileContent)
+ assert.NoError(t, err)
+ err = f.Close()
+ assert.Error(t, err)
+ }
+ // try to write to a missing directory
+ action1.Options.FsConfig.Compress.Name = "/subdir/missing/path/file.zip"
+ _, _, err = httpdtest.UpdateEventAction(action1, http.StatusOK)
+ assert.NoError(t, err)
+ conn, client, err = getSftpClient(user)
+ if assert.NoError(t, err) {
+ defer conn.Close()
+ defer client.Close()
+
+ f, err := client.Create(testFileName)
+ assert.NoError(t, err)
+ _, err = f.Write(testFileContent)
+ assert.NoError(t, err)
+ err = f.Close()
+ assert.Error(t, err)
+ }
+
+ _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveUser(user, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+}
+
func TestEventActionEmailAttachments(t *testing.T) {
smtpCfg := smtp.Config{
Host: "127.0.0.1",
@@ -4120,17 +4500,32 @@ func TestEventActionEmailAttachments(t *testing.T) {
a1 := dataprovider.BaseEventAction{
Name: "action1",
+ Type: dataprovider.ActionTypeFilesystem,
+ Options: dataprovider.BaseEventActionOptions{
+ FsConfig: dataprovider.EventActionFilesystemConfig{
+ Type: dataprovider.FilesystemActionCompress,
+ Compress: dataprovider.EventActionFsCompress{
+ Name: "/{{VirtualPath}}.zip",
+ Paths: []string{"/{{VirtualPath}}"},
+ },
+ },
+ },
+ }
+ action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
+ assert.NoError(t, err)
+ a2 := dataprovider.BaseEventAction{
+ Name: "action2",
Type: dataprovider.ActionTypeEmail,
Options: dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: []string{"test@example.com"},
Subject: `"{{Event}}" from "{{Name}}"`,
Body: "Fs path {{FsPath}}, size: {{FileSize}}, protocol: {{Protocol}}, IP: {{IP}}",
- Attachments: []string{"/{{VirtualPath}}"},
+ Attachments: []string{"/{{VirtualPath}}.zip"},
},
},
}
- action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
+ action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
assert.NoError(t, err)
r1 := dataprovider.EventRule{
Name: "test email with attachment",
@@ -4145,6 +4540,12 @@ func TestEventActionEmailAttachments(t *testing.T) {
},
Order: 1,
},
+ {
+ BaseEventAction: dataprovider.BaseEventAction{
+ Name: action2.Name,
+ },
+ Order: 2,
+ },
},
}
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
@@ -4185,6 +4586,8 @@ func TestEventActionEmailAttachments(t *testing.T) {
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
assert.NoError(t, err)
+ _, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
+ assert.NoError(t, err)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
@@ -5957,6 +6360,11 @@ func isDbDefenderSupported() bool {
}
}
+func getEncryptedFileSize(size int64) (int64, error) {
+ encSize, err := sio.EncryptedSize(uint64(size))
+ return int64(encSize) + 33, err
+}
+
func printLatestLogs(maxNumberOfLines int) {
var lines []string
f, err := os.Open(logFilePath)
diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go
index 24b29b02..ae5b251e 100644
--- a/internal/dataprovider/eventrule.go
+++ b/internal/dataprovider/eventrule.go
@@ -48,9 +48,9 @@ const (
)
var (
- supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeBackup,
- ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
- ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem}
+ supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
+ ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
+ ActionTypeDataRetentionCheck, ActionTypeMetadataCheck}
)
func isActionTypeValid(action int) bool {
@@ -123,6 +123,7 @@ const (
FilesystemActionDelete
FilesystemActionMkdirs
FilesystemActionExist
+ FilesystemActionCompress
)
const (
@@ -132,7 +133,7 @@ const (
var (
supportedFsActions = []int{FilesystemActionRename, FilesystemActionDelete, FilesystemActionMkdirs,
- FilesystemActionExist}
+ FilesystemActionCompress, FilesystemActionExist}
)
func isFilesystemActionValid(value int) bool {
@@ -147,6 +148,8 @@ func getFsActionTypeAsString(value int) string {
return "Delete"
case FilesystemActionExist:
return "Paths exist"
+ case FilesystemActionCompress:
+ return "Compress"
default:
return "Create directories"
}
@@ -539,6 +542,36 @@ func (c *EventActionDataRetentionConfig) validate() error {
return nil
}
+// EventActionFsCompress defines the configuration for the compress filesystem action
+type EventActionFsCompress struct {
+ // Archive path
+ Name string `json:"name,omitempty"`
+ // Paths to compress
+ Paths []string `json:"paths,omitempty"`
+}
+
+func (c *EventActionFsCompress) validate() error {
+ if c.Name == "" {
+ return util.NewValidationError("archive name is mandatory")
+ }
+ c.Name = util.CleanPath(strings.TrimSpace(c.Name))
+ if c.Name == "/" {
+ return util.NewValidationError("invalid archive name")
+ }
+ if len(c.Paths) == 0 {
+ return util.NewValidationError("no path to compress specified")
+ }
+ for idx, val := range c.Paths {
+ val = strings.TrimSpace(val)
+ if val == "" {
+ return util.NewValidationError("invalid path to compress")
+ }
+ c.Paths[idx] = util.CleanPath(val)
+ }
+ c.Paths = util.RemoveDuplicates(c.Paths, false)
+ return nil
+}
+
// EventActionFilesystemConfig defines the configuration for filesystem actions
type EventActionFilesystemConfig struct {
// Filesystem actions, see the above enum
@@ -551,6 +584,8 @@ type EventActionFilesystemConfig struct {
Deletes []string `json:"deletes,omitempty"`
// file/dirs to check for existence
Exist []string `json:"exist,omitempty"`
+ // paths to compress and archive name
+ Compress EventActionFsCompress `json:"compress"`
}
// GetDeletesAsString returns the list of items to delete as comma separated string.
@@ -571,6 +606,12 @@ func (c EventActionFilesystemConfig) GetExistAsString() string {
return strings.Join(c.Exist, ",")
}
+// GetCompressPathsAsString returns the list of items to compress as comma separated string.
+// Using a pointer receiver will not work in web templates
+func (c EventActionFilesystemConfig) GetCompressPathsAsString() string {
+ return strings.Join(c.Compress.Paths, ",")
+}
+
func (c *EventActionFilesystemConfig) validateRenames() error {
if len(c.Renames) == 0 {
return util.NewValidationError("no path to rename specified")
@@ -651,6 +692,7 @@ func (c *EventActionFilesystemConfig) validate() error {
c.MkDirs = nil
c.Deletes = nil
c.Exist = nil
+ c.Compress = EventActionFsCompress{}
if err := c.validateRenames(); err != nil {
return err
}
@@ -658,6 +700,7 @@ func (c *EventActionFilesystemConfig) validate() error {
c.Renames = nil
c.MkDirs = nil
c.Exist = nil
+ c.Compress = EventActionFsCompress{}
if err := c.validateDeletes(); err != nil {
return err
}
@@ -665,6 +708,7 @@ func (c *EventActionFilesystemConfig) validate() error {
c.Renames = nil
c.Deletes = nil
c.Exist = nil
+ c.Compress = EventActionFsCompress{}
if err := c.validateMkdirs(); err != nil {
return err
}
@@ -672,9 +716,18 @@ func (c *EventActionFilesystemConfig) validate() error {
c.Renames = nil
c.Deletes = nil
c.MkDirs = nil
+ c.Compress = EventActionFsCompress{}
if err := c.validateExist(); err != nil {
return err
}
+ case FilesystemActionCompress:
+ c.Renames = nil
+ c.MkDirs = nil
+ c.Deletes = nil
+ c.Exist = nil
+ if err := c.Compress.validate(); err != nil {
+ return err
+ }
}
return nil
}
@@ -686,6 +739,8 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
copy(deletes, c.Deletes)
exist := make([]string, len(c.Exist))
copy(exist, c.Exist)
+ compressPaths := make([]string, len(c.Compress.Paths))
+ copy(compressPaths, c.Compress.Paths)
return EventActionFilesystemConfig{
Type: c.Type,
@@ -693,6 +748,10 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
MkDirs: mkdirs,
Deletes: deletes,
Exist: exist,
+ Compress: EventActionFsCompress{
+ Paths: compressPaths,
+ Name: c.Compress.Name,
+ },
}
}
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index b78be1c6..2f36bdb7 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -1694,6 +1694,18 @@ func TestEventActionValidation(t *testing.T) {
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "invalid path to check for existence")
+ action.Options.FsConfig.Type = dataprovider.FilesystemActionCompress
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "archive name is mandatory")
+ action.Options.FsConfig.Compress.Name = "archive.zip"
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "no path to compress specified")
+ action.Options.FsConfig.Compress.Paths = []string{"item1", ""}
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "invalid path to compress")
}
func TestEventRuleValidation(t *testing.T) {
diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go
index a6c3507c..d6467fb2 100644
--- a/internal/httpd/webadmin.go
+++ b/internal/httpd/webadmin.go
@@ -2054,6 +2054,10 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
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"), " ", ""), ","),
+ Compress: dataprovider.EventActionFsCompress{
+ Name: r.Form.Get("fs_compress_name"),
+ Paths: strings.Split(strings.ReplaceAll(r.Form.Get("fs_compress_paths"), " ", ""), ","),
+ },
},
}
return options, nil
diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go
index b248f204..b6564db6 100644
--- a/internal/httpdtest/httpdtest.go
+++ b/internal/httpdtest/httpdtest.go
@@ -2322,6 +2322,21 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
return nil
}
+func compareEventActionFsCompressFields(expected, actual dataprovider.EventActionFsCompress) error {
+ if expected.Name != actual.Name {
+ return errors.New("fs compress name mismatch")
+ }
+ if len(expected.Paths) != len(actual.Paths) {
+ return errors.New("fs compress paths mismatch")
+ }
+ for _, v := range expected.Paths {
+ if !util.Contains(actual.Paths, v) {
+ return errors.New("fs compress paths content mismatch")
+ }
+ }
+ return nil
+}
+
func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionFilesystemConfig) error {
if expected.Type != actual.Type {
return errors.New("fs type mismatch")
@@ -2353,7 +2368,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
return errors.New("fs exist content mismatch")
}
}
- return nil
+ return compareEventActionFsCompressFields(expected.Compress, actual.Compress)
}
func compareEventActionCmdConfigFields(expected, actual dataprovider.EventActionCommandConfig) error {
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 446d5078..d330026a 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -6156,6 +6156,17 @@ components:
type: array
items:
$ref: '#/components/schemas/FolderRetention'
+ EventActionFsCompress:
+ type: object
+ properties:
+ name:
+ type: string
+ description: 'Full path to the (zip) archive to create. The parent dir must exist'
+ paths:
+ type: array
+ items:
+ type: string
+ description: 'paths to add the archive'
EventActionFilesystemConfig:
type: object
properties:
@@ -6177,6 +6188,8 @@ components:
type: array
items:
type: string
+ compress:
+ $ref: '#/components/schemas/EventActionFsCompress'
BaseEventActionOptions:
type: object
properties:
diff --git a/templates/webadmin/eventaction.html b/templates/webadmin/eventaction.html
index baa506d1..3a31dad1 100644
--- a/templates/webadmin/eventaction.html
+++ b/templates/webadmin/eventaction.html
@@ -606,6 +606,28 @@ along with this program. If not, see