eventmanager: add support for file/directory compression

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-10-10 18:53:58 +02:00
parent a417df60b3
commit 3e44a1dd2d
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
14 changed files with 919 additions and 42 deletions

View file

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

2
go.mod
View file

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

4
go.sum
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -606,6 +606,28 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="form-group row action-type action-fs-type action-fs-compress">
<label for="idFsCompressName" class="col-sm-2 col-form-label">Archive path</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idFsCompressName" name="fs_compress_name" placeholder=""
value="{{.Action.Options.FsConfig.Compress.Name}}" maxlength="255" aria-describedby="fsCompressNameHelpBlock">
<small id="fsCompressPathsHelpBlock" class="form-text text-muted">
Full path, as seen by SFTPGo users, to the zip archive to create. Placeholders are supported. If the specified file already exists, it is overwritten
</small>
</div>
</div>
<div class="form-group row action-type action-fs-type action-fs-compress">
<label for="idFsCompressPaths" class="col-sm-2 col-form-label">Paths</label>
<div class="col-sm-10">
<textarea class="form-control" id="idFsCompressPaths" name="fs_compress_paths" rows="2"
aria-describedby="fsCompressPathsHelpBlock">{{.Action.Options.FsConfig.GetCompressPathsAsString}}</textarea>
<small id="fsCompressPathsHelpBlock" class="form-text text-muted">
Comma separated paths to compress (zip) as seen by SFTPGo users. Placeholders are supported. The required permissions are granted automatically
</small>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<div class="col-sm-12 text-right px-0">
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">Submit</button>
@ -924,6 +946,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
case 4:
$('.action-fs-exist').show();
break;
case '5':
case 5:
$('.action-fs-compress').show();
break;
}
}