diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml
index a04c2c43..5b75cfb9 100644
--- a/.github/workflows/development.yml
+++ b/.github/workflows/development.yml
@@ -11,11 +11,11 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- go: [1.18]
+ go: [1.19]
os: [ubuntu-latest, macos-latest]
upload-coverage: [true]
include:
- - go: 1.18
+ - go: 1.19
os: windows-latest
upload-coverage: false
@@ -232,7 +232,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
- go-version: 1.18
+ go-version: 1.19
- name: Build
run: |
@@ -306,7 +306,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
- go-version: 1.18
+ go-version: 1.19
- name: Build
run: |
@@ -383,7 +383,7 @@ jobs:
matrix:
include:
- arch: amd64
- go: 1.18
+ go: 1.19
go-arch: amd64
- arch: aarch64
distro: ubuntu18.04
@@ -504,7 +504,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
- go-version: 1.18
+ go-version: 1.19
- uses: actions/checkout@v3
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 062f422b..04ae2ccc 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,7 +5,7 @@ on:
tags: 'v*'
env:
- GO_VERSION: 1.18.3
+ GO_VERSION: 1.19
jobs:
prepare-sources-with-deps:
diff --git a/Dockerfile b/Dockerfile
index 18b06bfa..028850de 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.18-bullseye as builder
+FROM golang:1.19-bullseye as builder
ENV GOFLAGS="-mod=readonly"
diff --git a/Dockerfile.alpine b/Dockerfile.alpine
index 4ac4dfc0..96b87540 100644
--- a/Dockerfile.alpine
+++ b/Dockerfile.alpine
@@ -1,4 +1,4 @@
-FROM golang:1.18-alpine3.16 AS builder
+FROM golang:1.19-alpine3.16 AS builder
ENV GOFLAGS="-mod=readonly"
diff --git a/Dockerfile.distroless b/Dockerfile.distroless
index 9dd1d4a2..f144bb22 100644
--- a/Dockerfile.distroless
+++ b/Dockerfile.distroless
@@ -1,4 +1,4 @@
-FROM golang:1.18-bullseye as builder
+FROM golang:1.19-bullseye as builder
ENV CGO_ENABLED=0 GOFLAGS="-mod=readonly"
diff --git a/docs/eventmanager.md b/docs/eventmanager.md
index 3ab1c683..ac5e4a35 100644
--- a/docs/eventmanager.md
+++ b/docs/eventmanager.md
@@ -11,6 +11,7 @@ The following actions are supported:
- `User quota reset`. The quota used by users will be updated based on current usage.
- `Folder quota reset`. The quota used by virtual folders will be updated based on current usage.
- `Transfer quota reset`. The transfer quota values will be reset to `0`.
+- `Data retention check`. You can define per-folder retention policies.
The following placeholders are supported:
diff --git a/go.mod b/go.mod
index f2880856..7a05cd1b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/drakkan/sftpgo/v2
-go 1.18
+go 1.19
require (
cloud.google.com/go/storage v1.24.0
@@ -64,13 +64,13 @@ require (
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
go.etcd.io/bbolt v1.3.6
go.uber.org/automaxprocs v1.5.1
- gocloud.dev v0.25.0
+ gocloud.dev v0.26.0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c
- golang.org/x/sys v0.0.0-20220731174439-a90be440212d
+ golang.org/x/sys v0.0.0-20220803195053-6e608f9ce704
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
- google.golang.org/api v0.90.0
+ google.golang.org/api v0.91.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
@@ -155,7 +155,7 @@ require (
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect
+ google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612 // indirect
google.golang.org/grpc v1.48.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
diff --git a/go.sum b/go.sum
index b2c6fa84..0a2f485b 100644
--- a/go.sum
+++ b/go.sum
@@ -602,7 +602,6 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
-github.com/mattn/go-ieproxy v0.0.3/go.mod h1:6ZpRmhBaYuBX1U2za+9rC9iCGLsSp2tftelZne7CPko=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@@ -808,8 +807,8 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
-gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk=
-gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y=
+gocloud.dev v0.26.0 h1:4rM/SVL0lLs+rhC0Gmc+gt/82DBpb7nbpIZKXXnfMXg=
+gocloud.dev v0.26.0/go.mod h1:mkUgejbnbLotorqDyvedJO20XcZNTynmSeVSQS9btVg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -955,7 +954,6 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -973,8 +971,8 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80=
-golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220803195053-6e608f9ce704 h1:Y7NOhdqIOU8kYI7BxsgL38d0ot0raxvcW+EMQU2QrT4=
+golang.org/x/sys v0.0.0-20220803195053-6e608f9ce704/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1117,8 +1115,8 @@ google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
-google.golang.org/api v0.90.0 h1:WMnUWAvihIClUYFNeFA69VTuR3duKS3IalMGDQcLvq8=
-google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.91.0 h1:731+JzuwaJoZXRQGmPoBiV+SrsAfUaIkdMCWTcQNPyA=
+google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1225,8 +1223,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 h1:QntLWYqZeuBtJkth3m/6DLznnI0AHJr+AgJXvVh/izw=
-google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
+google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612 h1:NX3L5YesD5qgxxrPHdKqHH38Ao0AG6poRXG+JljPsGU=
+google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/internal/common/connection.go b/internal/common/connection.go
index 06a6e8ca..a70e5a5f 100644
--- a/internal/common/connection.go
+++ b/internal/common/connection.go
@@ -622,7 +622,7 @@ func (c *BaseConnection) DoStat(virtualPath string, mode int, checkFilePatterns
info, err = fs.Stat(c.getRealFsPath(fsPath))
}
if err != nil {
- c.Log(logger.LevelError, "stat error for path %#v: %+v", virtualPath, err)
+ c.Log(logger.LevelWarn, "stat error for path %#v: %+v", virtualPath, err)
return info, c.GetFsError(fs, err)
}
if vfs.IsCryptOsFs(fs) {
diff --git a/internal/common/dataretention.go b/internal/common/dataretention.go
index 0f73ddbb..214832c2 100644
--- a/internal/common/dataretention.go
+++ b/internal/common/dataretention.go
@@ -66,7 +66,7 @@ func (c *ActiveRetentionChecks) Get() []RetentionCheck {
checks := make([]RetentionCheck, 0, len(c.Checks))
for _, check := range c.Checks {
- foldersCopy := make([]FolderRetention, len(check.Folders))
+ foldersCopy := make([]dataprovider.FolderRetention, len(check.Folders))
copy(foldersCopy, check.Folders)
notificationsCopy := make([]string, len(check.Notifications))
copy(notificationsCopy, check.Notifications)
@@ -124,37 +124,6 @@ func (c *ActiveRetentionChecks) remove(username string) bool {
return false
}
-// FolderRetention defines the retention policy for the specified directory path
-type FolderRetention struct {
- // Path is the exposed virtual directory path, if no other specific retention is defined,
- // the retention applies for sub directories too. For example if retention is defined
- // for the paths "/" and "/sub" then the retention for "/" is applied for any file outside
- // the "/sub" directory
- Path string `json:"path"`
- // Retention time in hours. 0 means exclude this path
- Retention int `json:"retention"`
- // DeleteEmptyDirs defines if empty directories will be deleted.
- // The user need the delete permission
- DeleteEmptyDirs bool `json:"delete_empty_dirs,omitempty"`
- // IgnoreUserPermissions defines if delete files even if the user does not have the delete permission.
- // The default is "false" which means that files will be skipped if the user does not have the permission
- // to delete them. This applies to sub directories too.
- IgnoreUserPermissions bool `json:"ignore_user_permissions,omitempty"`
-}
-
-func (f *FolderRetention) isValid() error {
- f.Path = path.Clean(f.Path)
- if !path.IsAbs(f.Path) {
- return util.NewValidationError(fmt.Sprintf("folder retention: invalid path %#v, please specify an absolute POSIX path",
- f.Path))
- }
- if f.Retention < 0 {
- return util.NewValidationError(fmt.Sprintf("invalid folder retention %v, it must be greater or equal to zero",
- f.Retention))
- }
- return nil
-}
-
type folderRetentionCheckResult struct {
Path string `json:"path"`
Retention int `json:"retention"`
@@ -172,7 +141,7 @@ type RetentionCheck struct {
// retention check start time as unix timestamp in milliseconds
StartTime int64 `json:"start_time"`
// affected folders
- Folders []FolderRetention `json:"folders"`
+ Folders []dataprovider.FolderRetention `json:"folders"`
// how cleanup results will be notified
Notifications []RetentionCheckNotification `json:"notifications,omitempty"`
// email to use if the notification method is set to email
@@ -188,7 +157,7 @@ func (c *RetentionCheck) Validate() error {
nothingToDo := true
for idx := range c.Folders {
f := &c.Folders[idx]
- if err := f.isValid(); err != nil {
+ if err := f.Validate(); err != nil {
return err
}
if f.Retention > 0 {
@@ -230,7 +199,7 @@ func (c *RetentionCheck) updateUserPermissions() {
}
}
-func (c *RetentionCheck) getFolderRetention(folderPath string) (FolderRetention, error) {
+func (c *RetentionCheck) getFolderRetention(folderPath string) (dataprovider.FolderRetention, error) {
dirsForPath := util.GetDirsForVirtualPath(folderPath)
for _, dirPath := range dirsForPath {
for _, folder := range c.Folders {
@@ -240,7 +209,7 @@ func (c *RetentionCheck) getFolderRetention(folderPath string) (FolderRetention,
}
}
- return FolderRetention{}, fmt.Errorf("unable to find folder retention for %#v", folderPath)
+ return dataprovider.FolderRetention{}, fmt.Errorf("unable to find folder retention for %#v", folderPath)
}
func (c *RetentionCheck) removeFile(virtualPath string, info os.FileInfo) error {
@@ -346,7 +315,7 @@ func (c *RetentionCheck) checkEmptyDirRemoval(folderPath string) {
}
// Start starts the retention check
-func (c *RetentionCheck) Start() {
+func (c *RetentionCheck) Start() error {
c.conn.Log(logger.LevelInfo, "retention check started")
defer RetentionChecks.remove(c.conn.User.Username)
defer c.conn.CloseFS() //nolint:errcheck
@@ -357,13 +326,14 @@ func (c *RetentionCheck) Start() {
if err := c.cleanupFolder(folder.Path); err != nil {
c.conn.Log(logger.LevelError, "retention check failed, unable to cleanup folder %#v", folder.Path)
c.sendNotifications(time.Since(startTime), err)
- return
+ return err
}
}
}
c.conn.Log(logger.LevelInfo, "retention check completed")
c.sendNotifications(time.Since(startTime), nil)
+ return nil
}
func (c *RetentionCheck) sendNotifications(elapsed time.Duration, err error) {
diff --git a/internal/common/dataretention_test.go b/internal/common/dataretention_test.go
index 1c66b20d..3a566c54 100644
--- a/internal/common/dataretention_test.go
+++ b/internal/common/dataretention_test.go
@@ -32,25 +32,17 @@ import (
func TestRetentionValidation(t *testing.T) {
check := RetentionCheck{}
- check.Folders = append(check.Folders, FolderRetention{
- Path: "relative",
- Retention: 10,
- })
- err := check.Validate()
- require.Error(t, err)
- assert.Contains(t, err.Error(), "please specify an absolute POSIX path")
-
- check.Folders = []FolderRetention{
+ check.Folders = []dataprovider.FolderRetention{
{
Path: "/",
Retention: -1,
},
}
- err = check.Validate()
+ err := check.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid folder retention")
- check.Folders = []FolderRetention{
+ check.Folders = []dataprovider.FolderRetention{
{
Path: "/ab/..",
Retention: 0,
@@ -61,7 +53,7 @@ func TestRetentionValidation(t *testing.T) {
assert.Contains(t, err.Error(), "nothing to delete")
assert.Equal(t, "/", check.Folders[0].Path)
- check.Folders = append(check.Folders, FolderRetention{
+ check.Folders = append(check.Folders, dataprovider.FolderRetention{
Path: "/../..",
Retention: 24,
})
@@ -69,7 +61,7 @@ func TestRetentionValidation(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), `duplicated folder path "/"`)
- check.Folders = []FolderRetention{
+ check.Folders = []dataprovider.FolderRetention{
{
Path: "/dir1",
Retention: 48,
@@ -240,7 +232,7 @@ func TestRetentionPermissionsAndGetFolder(t *testing.T) {
user.Permissions["/dir2/sub2"] = []string{dataprovider.PermDelete}
check := RetentionCheck{
- Folders: []FolderRetention{
+ Folders: []dataprovider.FolderRetention{
{
Path: "/dir2",
Retention: 24 * 7,
@@ -300,7 +292,7 @@ func TestRetentionCheckAddRemove(t *testing.T) {
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
check := RetentionCheck{
- Folders: []FolderRetention{
+ Folders: []dataprovider.FolderRetention{
{
Path: "/",
Retention: 48,
@@ -334,7 +326,7 @@ func TestCleanupErrors(t *testing.T) {
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
check := &RetentionCheck{
- Folders: []FolderRetention{
+ Folders: []dataprovider.FolderRetention{
{
Path: "/path",
Retention: 48,
diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go
index 6e3aec41..90a1dd13 100644
--- a/internal/common/eventmanager.go
+++ b/internal/common/eventmanager.go
@@ -495,6 +495,31 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params EventP
return err
}
+func executeQuotaResetForUser(user dataprovider.User) error {
+ if err := user.LoadAndApplyGroupSettings(); err != nil {
+ eventManagerLog(logger.LevelDebug, "skipping scheduled quota reset for user %s, cannot apply group settings: %v",
+ user.Username, err)
+ return err
+ }
+ if !QuotaScans.AddUserQuotaScan(user.Username) {
+ eventManagerLog(logger.LevelError, "another quota scan is already in progress for user %s", user.Username)
+ return fmt.Errorf("another quota scan is in progress for user %s", user.Username)
+ }
+ defer QuotaScans.RemoveUserQuotaScan(user.Username)
+
+ numFiles, size, err := user.ScanQuota()
+ if err != nil {
+ eventManagerLog(logger.LevelError, "error scanning quota for user %s: %v", user.Username, err)
+ return err
+ }
+ err = dataprovider.UpdateUserQuota(&user, numFiles, size, true)
+ if err != nil {
+ eventManagerLog(logger.LevelError, "error updating quota for user %s: %v", user.Username, err)
+ return err
+ }
+ return nil
+}
+
func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions) error {
users, err := dataprovider.DumpUsers()
if err != nil {
@@ -507,21 +532,7 @@ func executeUsersQuotaResetRuleAction(conditions dataprovider.ConditionOptions)
user.Username)
continue
}
- if !QuotaScans.AddUserQuotaScan(user.Username) {
- eventManagerLog(logger.LevelError, "another quota scan is already in progress for user %s", user.Username)
- failedResets = append(failedResets, user.Username)
- continue
- }
- numFiles, size, err := user.ScanQuota()
- QuotaScans.RemoveUserQuotaScan(user.Username)
- if err != nil {
- eventManagerLog(logger.LevelError, "error scanning quota for user %s: %v", user.Username, err)
- failedResets = append(failedResets, user.Username)
- continue
- }
- err = dataprovider.UpdateUserQuota(&user, numFiles, size, true)
- if err != nil {
- eventManagerLog(logger.LevelError, "error updating quota for user %s: %v", user.Username, err)
+ if err = executeQuotaResetForUser(user); err != nil {
failedResets = append(failedResets, user.Username)
continue
}
@@ -564,7 +575,6 @@ func executeFoldersQuotaResetRuleAction(conditions dataprovider.ConditionOptions
if err != nil {
eventManagerLog(logger.LevelError, "error updating quota for folder %s: %v", folder.Name, err)
failedResets = append(failedResets, folder.Name)
- continue
}
}
if len(failedResets) > 0 {
@@ -589,7 +599,6 @@ func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOption
if err != nil {
eventManagerLog(logger.LevelError, "error updating transfer quota for user %s: %v", user.Username, err)
failedResets = append(failedResets, user.Username)
- continue
}
}
if len(failedResets) > 0 {
@@ -598,6 +607,52 @@ func executeTransferQuotaResetRuleAction(conditions dataprovider.ConditionOption
return nil
}
+func executeDataRetentionCheckForUser(user dataprovider.User, folders []dataprovider.FolderRetention) error {
+ if err := user.LoadAndApplyGroupSettings(); err != nil {
+ eventManagerLog(logger.LevelDebug, "skipping scheduled retention check for user %s, cannot apply group settings: %v",
+ user.Username, err)
+ return err
+ }
+ check := RetentionCheck{
+ Folders: folders,
+ }
+ c := RetentionChecks.Add(check, &user)
+ if c == nil {
+ eventManagerLog(logger.LevelError, "another retention check is already in progress for user %s", user.Username)
+ return fmt.Errorf("another retention check is in progress for user %s", user.Username)
+ }
+ if err := c.Start(); err != nil {
+ eventManagerLog(logger.LevelError, "error checking retention for user %s: %v", user.Username, err)
+ return err
+ }
+ return nil
+}
+
+func executeDataRetentionCheckRuleAction(config dataprovider.EventActionDataRetentionConfig,
+ conditions dataprovider.ConditionOptions,
+) error {
+ users, err := dataprovider.DumpUsers()
+ if err != nil {
+ return fmt.Errorf("unable to get users: %w", err)
+ }
+ var failedChecks []string
+ for _, user := range users {
+ if !checkEventConditionPatterns(user.Username, conditions.Names) {
+ eventManagerLog(logger.LevelDebug, "skipping scheduled retention check for user %s, name conditions don't match",
+ user.Username)
+ continue
+ }
+ if err = executeDataRetentionCheckForUser(user, config.Folders); err != nil {
+ failedChecks = append(failedChecks, user.Username)
+ continue
+ }
+ }
+ if len(failedChecks) > 0 {
+ return fmt.Errorf("retention check failed for users: %+v", failedChecks)
+ }
+ return nil
+}
+
func executeRuleAction(action dataprovider.BaseEventAction, params EventParams, conditions dataprovider.ConditionOptions) error {
switch action.Type {
case dataprovider.ActionTypeHTTP:
@@ -614,6 +669,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params EventParams,
return executeFoldersQuotaResetRuleAction(conditions)
case dataprovider.ActionTypeTransferQuotaReset:
return executeTransferQuotaResetRuleAction(conditions)
+ case dataprovider.ActionTypeDataRetentionCheck:
+ return executeDataRetentionCheckRuleAction(action.Options.RetentionConfig, conditions)
default:
return fmt.Errorf("unsupported action type: %d", action.Type)
}
diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go
index 7cff17d0..cbdb8de9 100644
--- a/internal/common/eventmanager_test.go
+++ b/internal/common/eventmanager_test.go
@@ -265,6 +265,48 @@ func TestEventManagerErrors(t *testing.T) {
assert.Error(t, err)
err = executeTransferQuotaResetRuleAction(dataprovider.ConditionOptions{})
assert.Error(t, err)
+ err = executeQuotaResetForUser(dataprovider.User{
+ Groups: []sdk.GroupMapping{
+ {
+ Name: "agroup",
+ Type: sdk.GroupTypePrimary,
+ },
+ },
+ })
+ assert.Error(t, err)
+ err = executeDataRetentionCheckForUser(dataprovider.User{
+ Groups: []sdk.GroupMapping{
+ {
+ Name: "agroup",
+ Type: sdk.GroupTypePrimary,
+ },
+ },
+ }, nil)
+ assert.Error(t, err)
+
+ dataRetentionAction := dataprovider.BaseEventAction{
+ Type: dataprovider.ActionTypeDataRetentionCheck,
+ Options: dataprovider.BaseEventActionOptions{
+ RetentionConfig: dataprovider.EventActionDataRetentionConfig{
+ Folders: []dataprovider.FolderRetention{
+ {
+ Path: "/",
+ Retention: 24,
+ },
+ },
+ },
+ },
+ }
+ err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: "username1",
+ },
+ },
+ })
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "unable to get users")
+ }
eventManager.loadRules()
@@ -447,6 +489,88 @@ func TestEventRuleActions(t *testing.T) {
assert.Error(t, err)
assert.True(t, QuotaScans.RemoveUserQuotaScan(username1))
+ dataRetentionAction := dataprovider.BaseEventAction{
+ Type: dataprovider.ActionTypeDataRetentionCheck,
+ Options: dataprovider.BaseEventActionOptions{
+ RetentionConfig: dataprovider.EventActionDataRetentionConfig{
+ Folders: []dataprovider.FolderRetention{
+ {
+ Path: "",
+ Retention: 24,
+ },
+ },
+ },
+ },
+ }
+ err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: username1,
+ },
+ },
+ })
+ assert.Error(t, err) // invalid config, no folder path specified
+ retentionDir := "testretention"
+ dataRetentionAction = dataprovider.BaseEventAction{
+ Type: dataprovider.ActionTypeDataRetentionCheck,
+ Options: dataprovider.BaseEventActionOptions{
+ RetentionConfig: dataprovider.EventActionDataRetentionConfig{
+ Folders: []dataprovider.FolderRetention{
+ {
+ Path: path.Join("/", retentionDir),
+ Retention: 24,
+ DeleteEmptyDirs: true,
+ },
+ },
+ },
+ },
+ }
+ // create some test files
+ file1 := filepath.Join(user1.GetHomeDir(), "file1.txt")
+ file2 := filepath.Join(user1.GetHomeDir(), retentionDir, "file2.txt")
+ file3 := filepath.Join(user1.GetHomeDir(), retentionDir, "file3.txt")
+ file4 := filepath.Join(user1.GetHomeDir(), retentionDir, "sub", "file4.txt")
+
+ err = os.MkdirAll(filepath.Dir(file4), os.ModePerm)
+ assert.NoError(t, err)
+
+ for _, f := range []string{file1, file2, file3, file4} {
+ err = os.WriteFile(f, []byte(""), 0666)
+ assert.NoError(t, err)
+ }
+ timeBeforeRetention := time.Now().Add(-48 * time.Hour)
+ err = os.Chtimes(file1, timeBeforeRetention, timeBeforeRetention)
+ assert.NoError(t, err)
+ err = os.Chtimes(file2, timeBeforeRetention, timeBeforeRetention)
+ assert.NoError(t, err)
+ err = os.Chtimes(file4, timeBeforeRetention, timeBeforeRetention)
+ assert.NoError(t, err)
+
+ err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: username1,
+ },
+ },
+ })
+ assert.NoError(t, err)
+ assert.FileExists(t, file1)
+ assert.NoFileExists(t, file2)
+ assert.FileExists(t, file3)
+ assert.NoDirExists(t, filepath.Dir(file4))
+ // simulate another check in progress
+ c := RetentionChecks.Add(RetentionCheck{}, &user1)
+ assert.NotNil(t, c)
+ err = executeRuleAction(dataRetentionAction, EventParams{}, dataprovider.ConditionOptions{
+ Names: []dataprovider.ConditionPattern{
+ {
+ Pattern: username1,
+ },
+ },
+ })
+ assert.Error(t, err)
+ RetentionChecks.remove(user1.Username)
+
err = os.RemoveAll(user1.GetHomeDir())
assert.NoError(t, err)
diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go
index ee40a988..99080e8e 100644
--- a/internal/common/protocol_test.go
+++ b/internal/common/protocol_test.go
@@ -3454,7 +3454,7 @@ func TestRetentionAPI(t *testing.T) {
err = writeSFTPFile(uploadPath, 32, client)
assert.NoError(t, err)
- folderRetention := []common.FolderRetention{
+ folderRetention := []dataprovider.FolderRetention{
{
Path: "/",
Retention: 24,
@@ -3535,7 +3535,7 @@ func TestRetentionAPI(t *testing.T) {
err = client.Chtimes(innerUploadFilePath, time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))
assert.NoError(t, err)
- folderRetention := []common.FolderRetention{
+ folderRetention := []dataprovider.FolderRetention{
{
Path: "/missing",
Retention: 24,
@@ -3576,7 +3576,7 @@ func TestRetentionAPI(t *testing.T) {
_, err = client.Stat(innerUploadFilePath)
assert.NoError(t, err)
- folderRetention = []common.FolderRetention{
+ folderRetention = []dataprovider.FolderRetention{
{
Path: "/" + testDir,
@@ -3611,7 +3611,7 @@ func TestRetentionAPI(t *testing.T) {
err = os.Chmod(dirPath, 0001)
assert.NoError(t, err)
- folderRetention := []common.FolderRetention{
+ folderRetention := []dataprovider.FolderRetention{
{
Path: "/adir",
diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go
index e210e167..11a36a01 100644
--- a/internal/dataprovider/eventrule.go
+++ b/internal/dataprovider/eventrule.go
@@ -40,11 +40,13 @@ const (
ActionTypeUserQuotaReset
ActionTypeFolderQuotaReset
ActionTypeTransferQuotaReset
+ ActionTypeDataRetentionCheck
)
var (
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeBackup,
- ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset}
+ ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
+ ActionTypeDataRetentionCheck}
)
func isActionTypeValid(action int) bool {
@@ -65,6 +67,8 @@ func getActionTypeAsString(action int) string {
return "Folder quota reset"
case ActionTypeTransferQuotaReset:
return "Transfer quota reset"
+ case ActionTypeDataRetentionCheck:
+ return "Data retention check"
default:
return "Command"
}
@@ -149,13 +153,13 @@ type KeyValue struct {
// EventActionHTTPConfig defines the configuration for an HTTP event target
type EventActionHTTPConfig struct {
- Endpoint string `json:"endpoint"`
+ Endpoint string `json:"endpoint,omitempty"`
Username string `json:"username,omitempty"`
Password *kms.Secret `json:"password,omitempty"`
Headers []KeyValue `json:"headers,omitempty"`
- Timeout int `json:"timeout"`
+ Timeout int `json:"timeout,omitempty"`
SkipTLSVerify bool `json:"skip_tls_verify,omitempty"`
- Method string `json:"method"`
+ Method string `json:"method,omitempty"`
QueryParameters []KeyValue `json:"query_parameters,omitempty"`
Body string `json:"post_body,omitempty"`
}
@@ -218,9 +222,9 @@ func (c *EventActionHTTPConfig) GetHTTPClient() *http.Client {
// EventActionCommandConfig defines the configuration for a command event target
type EventActionCommandConfig struct {
- Cmd string `json:"cmd"`
- Timeout int `json:"timeout"`
- EnvVars []KeyValue `json:"env_vars"`
+ Cmd string `json:"cmd,omitempty"`
+ Timeout int `json:"timeout,omitempty"`
+ EnvVars []KeyValue `json:"env_vars,omitempty"`
}
func (c *EventActionCommandConfig) validate() error {
@@ -243,46 +247,111 @@ func (c *EventActionCommandConfig) validate() error {
// EventActionEmailConfig defines the configuration options for SMTP event actions
type EventActionEmailConfig struct {
- Recipients []string `json:"recipients"`
- Subject string `json:"subject"`
- Body string `json:"body"`
+ Recipients []string `json:"recipients,omitempty"`
+ Subject string `json:"subject,omitempty"`
+ Body string `json:"body,omitempty"`
}
// GetRecipientsAsString returns the list of recipients as comma separated string
-func (o EventActionEmailConfig) GetRecipientsAsString() string {
- return strings.Join(o.Recipients, ",")
+func (c EventActionEmailConfig) GetRecipientsAsString() string {
+ return strings.Join(c.Recipients, ",")
}
-func (o *EventActionEmailConfig) validate() error {
- if len(o.Recipients) == 0 {
+func (c *EventActionEmailConfig) validate() error {
+ if len(c.Recipients) == 0 {
return util.NewValidationError("at least one email recipient is required")
}
- o.Recipients = util.RemoveDuplicates(o.Recipients, false)
- for _, r := range o.Recipients {
+ c.Recipients = util.RemoveDuplicates(c.Recipients, false)
+ for _, r := range c.Recipients {
if r == "" {
return util.NewValidationError("invalid email recipients")
}
}
- if o.Subject == "" {
+ if c.Subject == "" {
return util.NewValidationError("email subject is required")
}
- if o.Body == "" {
+ if c.Body == "" {
return util.NewValidationError("email body is required")
}
return nil
}
+// FolderRetention defines a folder retention configuration
+type FolderRetention struct {
+ // Path is the exposed virtual directory path, if no other specific retention is defined,
+ // the retention applies for sub directories too. For example if retention is defined
+ // for the paths "/" and "/sub" then the retention for "/" is applied for any file outside
+ // the "/sub" directory
+ Path string `json:"path"`
+ // Retention time in hours. 0 means exclude this path
+ Retention int `json:"retention"`
+ // DeleteEmptyDirs defines if empty directories will be deleted.
+ // The user need the delete permission
+ DeleteEmptyDirs bool `json:"delete_empty_dirs,omitempty"`
+ // IgnoreUserPermissions defines whether to delete files even if the user does not have the delete permission.
+ // The default is "false" which means that files will be skipped if the user does not have the permission
+ // to delete them. This applies to sub directories too.
+ IgnoreUserPermissions bool `json:"ignore_user_permissions,omitempty"`
+}
+
+// Validate returns an error if the configuration is not valid
+func (f *FolderRetention) Validate() error {
+ f.Path = util.CleanPath(f.Path)
+ if f.Retention < 0 {
+ return util.NewValidationError(fmt.Sprintf("invalid folder retention %v, it must be greater or equal to zero",
+ f.Retention))
+ }
+ return nil
+}
+
+// EventActionDataRetentionConfig defines the configuration for a data retention check
+type EventActionDataRetentionConfig struct {
+ Folders []FolderRetention `json:"folders,omitempty"`
+}
+
+func (c *EventActionDataRetentionConfig) validate() error {
+ folderPaths := make(map[string]bool)
+ nothingToDo := true
+ for idx := range c.Folders {
+ f := &c.Folders[idx]
+ if err := f.Validate(); err != nil {
+ return err
+ }
+ if f.Retention > 0 {
+ nothingToDo = false
+ }
+ if _, ok := folderPaths[f.Path]; ok {
+ return util.NewValidationError(fmt.Sprintf("duplicated folder path %#v", f.Path))
+ }
+ folderPaths[f.Path] = true
+ }
+ if nothingToDo {
+ return util.NewValidationError("nothing to delete!")
+ }
+ return nil
+}
+
// BaseEventActionOptions defines the supported configuration options for a base event actions
type BaseEventActionOptions struct {
- HTTPConfig EventActionHTTPConfig `json:"http_config"`
- CmdConfig EventActionCommandConfig `json:"cmd_config"`
- EmailConfig EventActionEmailConfig `json:"email_config"`
+ HTTPConfig EventActionHTTPConfig `json:"http_config"`
+ CmdConfig EventActionCommandConfig `json:"cmd_config"`
+ EmailConfig EventActionEmailConfig `json:"email_config"`
+ RetentionConfig EventActionDataRetentionConfig `json:"retention_config"`
}
func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
o.SetEmptySecretsIfNil()
emailRecipients := make([]string, len(o.EmailConfig.Recipients))
copy(emailRecipients, o.EmailConfig.Recipients)
+ folders := make([]FolderRetention, 0, len(o.RetentionConfig.Folders))
+ for _, folder := range o.RetentionConfig.Folders {
+ folders = append(folders, FolderRetention{
+ Path: folder.Path,
+ Retention: folder.Retention,
+ DeleteEmptyDirs: folder.DeleteEmptyDirs,
+ IgnoreUserPermissions: folder.IgnoreUserPermissions,
+ })
+ }
return BaseEventActionOptions{
HTTPConfig: EventActionHTTPConfig{
@@ -306,6 +375,9 @@ func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
Subject: o.EmailConfig.Subject,
Body: o.EmailConfig.Body,
},
+ RetentionConfig: EventActionDataRetentionConfig{
+ Folders: folders,
+ },
}
}
@@ -334,19 +406,28 @@ func (o *BaseEventActionOptions) validate(action int, name string) error {
case ActionTypeHTTP:
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
+ o.RetentionConfig = EventActionDataRetentionConfig{}
return o.HTTPConfig.validate(name)
case ActionTypeCommand:
o.HTTPConfig = EventActionHTTPConfig{}
o.EmailConfig = EventActionEmailConfig{}
+ o.RetentionConfig = EventActionDataRetentionConfig{}
return o.CmdConfig.validate()
case ActionTypeEmail:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
+ o.RetentionConfig = EventActionDataRetentionConfig{}
return o.EmailConfig.validate()
+ case ActionTypeDataRetentionCheck:
+ o.HTTPConfig = EventActionHTTPConfig{}
+ o.CmdConfig = EventActionCommandConfig{}
+ o.EmailConfig = EventActionEmailConfig{}
+ return o.RetentionConfig.validate()
default:
o.HTTPConfig = EventActionHTTPConfig{}
o.CmdConfig = EventActionCommandConfig{}
o.EmailConfig = EventActionEmailConfig{}
+ o.RetentionConfig = EventActionDataRetentionConfig{}
}
return nil
}
diff --git a/internal/httpd/api_retention.go b/internal/httpd/api_retention.go
index ff98cbff..a3ba39f5 100644
--- a/internal/httpd/api_retention.go
+++ b/internal/httpd/api_retention.go
@@ -71,6 +71,6 @@ func startRetentionCheck(w http.ResponseWriter, r *http.Request) {
http.StatusConflict)
return
}
- go c.Start()
+ go c.Start() //nolint:errcheck
sendAPIResponse(w, r, err, "Check started", http.StatusAccepted)
}
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index 54ff1df1..11c63f36 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -1100,6 +1100,27 @@ func TestBasicActionRulesHandling(t *testing.T) {
}
assert.True(t, found)
a.Description = "new description"
+ a.Type = dataprovider.ActionTypeDataRetentionCheck
+ a.Options = dataprovider.BaseEventActionOptions{
+ RetentionConfig: dataprovider.EventActionDataRetentionConfig{
+ Folders: []dataprovider.FolderRetention{
+ {
+ Path: "/",
+ Retention: 144,
+ },
+ {
+ Path: "/p1",
+ Retention: 0,
+ },
+ {
+ Path: "/p2",
+ Retention: 12,
+ },
+ },
+ },
+ }
+ _, _, err = httpdtest.UpdateEventAction(a, http.StatusOK)
+ assert.NoError(t, err)
a.Type = dataprovider.ActionTypeCommand
a.Options = dataprovider.BaseEventActionOptions{
CmdConfig: dataprovider.EventActionCommandConfig{
@@ -1526,6 +1547,51 @@ func TestEventActionValidation(t *testing.T) {
_, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
assert.NoError(t, err)
assert.Contains(t, string(resp), "email body is required")
+
+ action.Type = dataprovider.ActionTypeDataRetentionCheck
+ action.Options.RetentionConfig = dataprovider.EventActionDataRetentionConfig{
+ Folders: nil,
+ }
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "nothing to delete")
+ action.Options.RetentionConfig = dataprovider.EventActionDataRetentionConfig{
+ Folders: []dataprovider.FolderRetention{
+ {
+ Path: "/",
+ Retention: 0,
+ },
+ },
+ }
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "nothing to delete")
+ action.Options.RetentionConfig = dataprovider.EventActionDataRetentionConfig{
+ Folders: []dataprovider.FolderRetention{
+ {
+ Path: "../path",
+ Retention: 1,
+ },
+ {
+ Path: "/path",
+ Retention: 10,
+ },
+ },
+ }
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "duplicated folder path")
+ action.Options.RetentionConfig = dataprovider.EventActionDataRetentionConfig{
+ Folders: []dataprovider.FolderRetention{
+ {
+ Path: "p",
+ Retention: -1,
+ },
+ },
+ }
+ _, resp, err = httpdtest.AddEventAction(action, http.StatusBadRequest)
+ assert.NoError(t, err)
+ assert.Contains(t, string(resp), "invalid folder retention")
}
func TestEventRuleValidation(t *testing.T) {
@@ -3235,7 +3301,7 @@ func TestRetentionAPI(t *testing.T) {
err = os.WriteFile(localFilePath, []byte("test data"), os.ModePerm)
assert.NoError(t, err)
- folderRetention := []common.FolderRetention{
+ folderRetention := []dataprovider.FolderRetention{
{
Path: "/",
Retention: 0,
@@ -3282,7 +3348,8 @@ func TestRetentionAPI(t *testing.T) {
_, err = httpdtest.StartRetentionCheck(user.Username, folderRetention, http.StatusConflict)
assert.NoError(t, err)
- c.Start()
+ err = c.Start()
+ assert.NoError(t, err)
assert.Len(t, common.RetentionChecks.Get(), 0)
admin := getTestAdmin()
@@ -18692,6 +18759,53 @@ func TestWebEventAction(t *testing.T) {
assert.Empty(t, actionGet.Options.CmdConfig.Cmd)
assert.Equal(t, 0, actionGet.Options.CmdConfig.Timeout)
assert.Len(t, actionGet.Options.CmdConfig.EnvVars, 0)
+ // change action type to data retention check
+ action.Type = dataprovider.ActionTypeDataRetentionCheck
+ form.Set("type", fmt.Sprintf("%d", action.Type))
+ form.Set("folder_retention_path10", "p1")
+ form.Set("folder_retention_val10", "a")
+ req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
+ bytes.NewBuffer([]byte(form.Encode())))
+ assert.NoError(t, err)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusOK, rr)
+ assert.Contains(t, rr.Body.String(), "invalid retention for path")
+ form.Set("folder_retention_val10", "24")
+ form.Set("folder_retention_options10", "1")
+ form.Add("folder_retention_options10", "2")
+ form.Set("folder_retention_path11", "../p2")
+ form.Set("folder_retention_val11", "48")
+ form.Set("folder_retention_options11", "1")
+ form.Add("folder_retention_options12", "2") // ignored
+ req, err = http.NewRequest(http.MethodPost, path.Join(webAdminEventActionPath, action.Name),
+ bytes.NewBuffer([]byte(form.Encode())))
+ assert.NoError(t, err)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusSeeOther, rr)
+ // check the update
+ actionGet, _, err = httpdtest.GetEventActionByName(action.Name, http.StatusOK)
+ assert.NoError(t, err)
+ assert.Equal(t, action.Type, actionGet.Type)
+ if assert.Len(t, actionGet.Options.RetentionConfig.Folders, 2) {
+ for _, folder := range actionGet.Options.RetentionConfig.Folders {
+ switch folder.Path {
+ case "/p1":
+ assert.Equal(t, 24, folder.Retention)
+ assert.True(t, folder.DeleteEmptyDirs)
+ assert.True(t, folder.IgnoreUserPermissions)
+ case "/p2":
+ assert.Equal(t, 48, folder.Retention)
+ assert.True(t, folder.DeleteEmptyDirs)
+ assert.False(t, folder.IgnoreUserPermissions)
+ default:
+ t.Errorf("unexpected folder path %v", folder.Path)
+ }
+ }
+ }
req, err = http.NewRequest(http.MethodDelete, path.Join(webAdminEventActionPath, action.Name), nil)
assert.NoError(t, err)
diff --git a/internal/httpd/internal_test.go b/internal/httpd/internal_test.go
index 5868aa9b..477de947 100644
--- a/internal/httpd/internal_test.go
+++ b/internal/httpd/internal_test.go
@@ -767,7 +767,7 @@ func TestRetentionInvalidTokenClaims(t *testing.T) {
user.Filters.AllowAPIKeyAuth = true
err := dataprovider.AddUser(&user, "", "")
assert.NoError(t, err)
- folderRetention := []common.FolderRetention{
+ folderRetention := []dataprovider.FolderRetention{
{
Path: "/",
Retention: 0,
diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go
index 38a65fda..8b07f398 100644
--- a/internal/httpd/webadmin.go
+++ b/internal/httpd/webadmin.go
@@ -1836,6 +1836,30 @@ func getKeyValsFromPostFields(r *http.Request, key, val string) []dataprovider.K
return res
}
+func getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRetention, error) {
+ var res []dataprovider.FolderRetention
+ for k := range r.Form {
+ if strings.HasPrefix(k, "folder_retention_path") {
+ folderPath := r.Form.Get(k)
+ if folderPath != "" {
+ idx := strings.TrimPrefix(k, "folder_retention_path")
+ retention, err := strconv.Atoi(r.Form.Get(fmt.Sprintf("folder_retention_val%s", idx)))
+ if err != nil {
+ return nil, fmt.Errorf("invalid retention for path %q: %w", folderPath, err)
+ }
+ options := r.Form[fmt.Sprintf("folder_retention_options%s", idx)]
+ res = append(res, dataprovider.FolderRetention{
+ Path: folderPath,
+ Retention: retention,
+ DeleteEmptyDirs: util.Contains(options, "1"),
+ IgnoreUserPermissions: util.Contains(options, "2"),
+ })
+ }
+ }
+ }
+ return res, nil
+}
+
func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEventActionOptions, error) {
httpTimeout, err := strconv.Atoi(r.Form.Get("http_timeout"))
if err != nil {
@@ -1845,6 +1869,10 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
if err != nil {
return dataprovider.BaseEventActionOptions{}, fmt.Errorf("invalid command timeout: %w", err)
}
+ foldersRetention, err := getFoldersRetentionFromPostFields(r)
+ if err != nil {
+ return dataprovider.BaseEventActionOptions{}, err
+ }
options := dataprovider.BaseEventActionOptions{
HTTPConfig: dataprovider.EventActionHTTPConfig{
Endpoint: r.Form.Get("http_endpoint"),
@@ -1867,6 +1895,9 @@ func getEventActionOptionsFromPostFields(r *http.Request) (dataprovider.BaseEven
Subject: r.Form.Get("email_subject"),
Body: r.Form.Get("email_body"),
},
+ RetentionConfig: dataprovider.EventActionDataRetentionConfig{
+ Folders: foldersRetention,
+ },
}
return options, nil
}
diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go
index b1ca67ab..fa5182ba 100644
--- a/internal/httpdtest/httpdtest.go
+++ b/internal/httpdtest/httpdtest.go
@@ -920,7 +920,7 @@ func GetRetentionChecks(expectedStatusCode int) ([]common.ActiveRetentionChecks,
}
// StartRetentionCheck starts a new retention check
-func StartRetentionCheck(username string, retention []common.FolderRetention, expectedStatusCode int) ([]byte, error) {
+func StartRetentionCheck(username string, retention []dataprovider.FolderRetention, expectedStatusCode int) ([]byte, error) {
var body []byte
asJSON, _ := json.Marshal(retention)
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(retentionBasePath, username, "check"),
@@ -1346,6 +1346,9 @@ func checkEventAction(expected, actual dataprovider.BaseEventAction) error {
if err := compareEventActionEmailConfigFields(expected.Options.EmailConfig, actual.Options.EmailConfig); err != nil {
return err
}
+ if err := compareEventActionDataRetentionFields(expected.Options.RetentionConfig, actual.Options.RetentionConfig); err != nil {
+ return err
+ }
return compareEventActionHTTPConfigFields(expected.Options.HTTPConfig, actual.Options.HTTPConfig)
}
@@ -2248,6 +2251,34 @@ func compareEventActionCmdConfigFields(expected, actual dataprovider.EventAction
return nil
}
+func compareEventActionDataRetentionFields(expected, actual dataprovider.EventActionDataRetentionConfig) error {
+ if len(expected.Folders) != len(actual.Folders) {
+ return errors.New("retention folders mismatch")
+ }
+ for _, f1 := range expected.Folders {
+ found := false
+ for _, f2 := range actual.Folders {
+ if f1.Path == f2.Path {
+ found = true
+ if f1.Retention != f2.Retention {
+ return fmt.Errorf("retention mismatch for folder %s", f1.Path)
+ }
+ if f1.DeleteEmptyDirs != f2.DeleteEmptyDirs {
+ return fmt.Errorf("delete_empty_dirs mismatch for folder %s", f1.Path)
+ }
+ if f1.IgnoreUserPermissions != f2.IgnoreUserPermissions {
+ return fmt.Errorf("ignore_user_permissions mismatch for folder %s", f1.Path)
+ }
+ break
+ }
+ }
+ if !found {
+ return errors.New("retention folders mismatch")
+ }
+ }
+ return nil
+}
+
func compareEqualGroupSettingsFields(expected sdk.BaseGroupUserSettings, actual sdk.BaseGroupUserSettings) error {
if expected.HomeDir != actual.HomeDir {
return errors.New("home dir mismatch")
diff --git a/internal/sftpd/scp.go b/internal/sftpd/scp.go
index e7dc1093..7006a6ab 100644
--- a/internal/sftpd/scp.go
+++ b/internal/sftpd/scp.go
@@ -635,8 +635,10 @@ func (c *scpCommand) readProtocolMessage() (string, error) {
return command.String(), err
}
-// send an error message and close the channel
-//nolint:errcheck // we don't check write errors here, we have to close the channel anyway
+// sendErrorMessage sends an error message and close the channel
+// we don't check write errors here, we have to close the channel anyway
+//
+//nolint:errcheck
func (c *scpCommand) sendErrorMessage(fs vfs.Fs, err error) {
c.connection.channel.Write(errMsg)
if fs != nil {
diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go
index 76adf40b..2b9377be 100644
--- a/internal/telemetry/telemetry.go
+++ b/internal/telemetry/telemetry.go
@@ -13,9 +13,9 @@
// along with this program. If not, see