mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
eventmanager: add support for file/directory compression
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
a417df60b3
commit
3e44a1dd2d
14 changed files with 919 additions and 42 deletions
|
@ -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
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue