|
@@ -1,6 +1,7 @@
|
|
|
package common
|
|
|
|
|
|
import (
|
|
|
+ "bytes"
|
|
|
"fmt"
|
|
|
"os"
|
|
|
"path"
|
|
@@ -9,9 +10,20 @@ import (
|
|
|
|
|
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
|
|
"github.com/drakkan/sftpgo/v2/logger"
|
|
|
+ "github.com/drakkan/sftpgo/v2/smtp"
|
|
|
"github.com/drakkan/sftpgo/v2/util"
|
|
|
)
|
|
|
|
|
|
+// RetentionCheckNotification defines the supported notification methods for a retention check result
|
|
|
+type RetentionCheckNotification = string
|
|
|
+
|
|
|
+const (
|
|
|
+ // no notification, the check results are recorded in the logs
|
|
|
+ RetentionCheckNotificationNone = "None"
|
|
|
+ // notify results by email
|
|
|
+ RetentionCheckNotificationEmail = "Email"
|
|
|
+)
|
|
|
+
|
|
|
var (
|
|
|
// RetentionChecks is the list of active quota scans
|
|
|
RetentionChecks ActiveRetentionChecks
|
|
@@ -33,9 +45,11 @@ func (c *ActiveRetentionChecks) Get() []RetentionCheck {
|
|
|
foldersCopy := make([]FolderRetention, len(check.Folders))
|
|
|
copy(foldersCopy, check.Folders)
|
|
|
checks = append(checks, RetentionCheck{
|
|
|
- Username: check.Username,
|
|
|
- StartTime: check.StartTime,
|
|
|
- Folders: foldersCopy,
|
|
|
+ Username: check.Username,
|
|
|
+ StartTime: check.StartTime,
|
|
|
+ Notification: check.Notification,
|
|
|
+ Email: check.Email,
|
|
|
+ Folders: foldersCopy,
|
|
|
})
|
|
|
}
|
|
|
return checks
|
|
@@ -114,6 +128,16 @@ func (f *FolderRetention) isValid() error {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
+type folderRetentionCheckResult struct {
|
|
|
+ Path string
|
|
|
+ Retention int
|
|
|
+ DeletedFiles int
|
|
|
+ DeletedSize int64
|
|
|
+ Elapsed time.Duration
|
|
|
+ Info string
|
|
|
+ Error string
|
|
|
+}
|
|
|
+
|
|
|
// RetentionCheck defines an active retention check
|
|
|
type RetentionCheck struct {
|
|
|
// Username to which the retention check refers
|
|
@@ -122,8 +146,13 @@ type RetentionCheck struct {
|
|
|
StartTime int64 `json:"start_time"`
|
|
|
// affected folders
|
|
|
Folders []FolderRetention `json:"folders"`
|
|
|
+ // how cleanup results will be notified
|
|
|
+ Notification RetentionCheckNotification `json:"notification"`
|
|
|
+ // email to use if the notification method is set to email
|
|
|
+ Email string `json:"email,omitempty"`
|
|
|
// Cleanup results
|
|
|
- conn *BaseConnection
|
|
|
+ results []*folderRetentionCheckResult `json:"-"`
|
|
|
+ conn *BaseConnection
|
|
|
}
|
|
|
|
|
|
// Validate returns an error if the specified folders are not valid
|
|
@@ -146,6 +175,17 @@ func (c *RetentionCheck) Validate() error {
|
|
|
if nothingToDo {
|
|
|
return util.NewValidationError("nothing to delete!")
|
|
|
}
|
|
|
+ switch c.Notification {
|
|
|
+ case RetentionCheckNotificationEmail:
|
|
|
+ if !smtp.IsEnabled() {
|
|
|
+ return util.NewValidationError("in order to notify results via email you must configure an SMTP server")
|
|
|
+ }
|
|
|
+ if c.Email == "" {
|
|
|
+ return util.NewValidationError("in order to notify results via email you must add a valid email address to your profile")
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ c.Notification = RetentionCheckNotificationNone
|
|
|
+ }
|
|
|
return nil
|
|
|
}
|
|
|
|
|
@@ -180,7 +220,14 @@ func (c *RetentionCheck) removeFile(virtualPath string, info os.FileInfo) error
|
|
|
|
|
|
func (c *RetentionCheck) cleanupFolder(folderPath string) error {
|
|
|
cleanupPerms := []string{dataprovider.PermListItems, dataprovider.PermDelete}
|
|
|
+ startTime := time.Now()
|
|
|
+ result := &folderRetentionCheckResult{
|
|
|
+ Path: folderPath,
|
|
|
+ }
|
|
|
+ c.results = append(c.results, result)
|
|
|
if !c.conn.User.HasPerms(cleanupPerms, folderPath) {
|
|
|
+ result.Elapsed = time.Since(startTime)
|
|
|
+ result.Info = "data retention check skipped: no permissions"
|
|
|
c.conn.Log(logger.LevelInfo, "user %#v does not have permissions to check retention on %#v, retention check skipped",
|
|
|
c.conn.User, folderPath)
|
|
|
return nil
|
|
@@ -188,10 +235,15 @@ func (c *RetentionCheck) cleanupFolder(folderPath string) error {
|
|
|
|
|
|
folderRetention, err := c.getFolderRetention(folderPath)
|
|
|
if err != nil {
|
|
|
+ result.Elapsed = time.Since(startTime)
|
|
|
+ result.Error = "unable to get folder retention"
|
|
|
c.conn.Log(logger.LevelError, "unable to get folder retention for path %#v", folderPath)
|
|
|
return err
|
|
|
}
|
|
|
+ result.Retention = folderRetention.Retention
|
|
|
if folderRetention.Retention == 0 {
|
|
|
+ result.Elapsed = time.Since(startTime)
|
|
|
+ result.Info = "data retention check skipped: retention is set to 0"
|
|
|
c.conn.Log(logger.LevelDebug, "retention check skipped for folder %#v, retention is set to 0", folderPath)
|
|
|
return nil
|
|
|
}
|
|
@@ -199,19 +251,22 @@ func (c *RetentionCheck) cleanupFolder(folderPath string) error {
|
|
|
folderPath, folderRetention.Retention, folderRetention.DeleteEmptyDirs, folderRetention.IgnoreUserPermissions)
|
|
|
files, err := c.conn.ListDir(folderPath)
|
|
|
if err != nil {
|
|
|
+ result.Elapsed = time.Since(startTime)
|
|
|
if err == c.conn.GetNotExistError() {
|
|
|
+ result.Info = "data retention check skipped, folder does not exist"
|
|
|
c.conn.Log(logger.LevelDebug, "folder %#v does not exist, retention check skipped", folderPath)
|
|
|
return nil
|
|
|
}
|
|
|
- c.conn.Log(logger.LevelWarn, "unable to list directory %#v", folderPath)
|
|
|
+ result.Error = fmt.Sprintf("unable to list directory %#v", folderPath)
|
|
|
+ c.conn.Log(logger.LevelWarn, result.Error)
|
|
|
return err
|
|
|
}
|
|
|
- deletedFiles := 0
|
|
|
- deletedSize := int64(0)
|
|
|
for _, info := range files {
|
|
|
virtualPath := path.Join(folderPath, info.Name())
|
|
|
if info.IsDir() {
|
|
|
if err := c.cleanupFolder(virtualPath); err != nil {
|
|
|
+ result.Elapsed = time.Since(startTime)
|
|
|
+ result.Error = fmt.Sprintf("unable to check folder: %v", err)
|
|
|
c.conn.Log(logger.LevelWarn, "unable to cleanup folder %#v: %v", virtualPath, err)
|
|
|
return err
|
|
|
}
|
|
@@ -219,14 +274,16 @@ func (c *RetentionCheck) cleanupFolder(folderPath string) error {
|
|
|
retentionTime := info.ModTime().Add(time.Duration(folderRetention.Retention) * time.Hour)
|
|
|
if retentionTime.Before(time.Now()) {
|
|
|
if err := c.removeFile(virtualPath, info); err != nil {
|
|
|
+ result.Elapsed = time.Since(startTime)
|
|
|
+ result.Error = fmt.Sprintf("unable to remove file %#v: %v", virtualPath, err)
|
|
|
c.conn.Log(logger.LevelWarn, "unable to remove file %#v, retention %v: %v",
|
|
|
virtualPath, retentionTime, err)
|
|
|
return err
|
|
|
}
|
|
|
c.conn.Log(logger.LevelDebug, "removed file %#v, modification time: %v, retention: %v hours, retention time: %v",
|
|
|
virtualPath, info.ModTime(), folderRetention.Retention, retentionTime)
|
|
|
- deletedFiles++
|
|
|
- deletedSize += info.Size()
|
|
|
+ result.DeletedFiles++
|
|
|
+ result.DeletedSize += info.Size()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -234,8 +291,9 @@ func (c *RetentionCheck) cleanupFolder(folderPath string) error {
|
|
|
if folderRetention.DeleteEmptyDirs {
|
|
|
c.checkEmptyDirRemoval(folderPath)
|
|
|
}
|
|
|
+ result.Elapsed = time.Since(startTime)
|
|
|
c.conn.Log(logger.LevelDebug, "retention check completed for folder %#v, deleted files: %v, deleted size: %v bytes",
|
|
|
- folderPath, deletedFiles, deletedSize)
|
|
|
+ folderPath, result.DeletedFiles, result.DeletedSize)
|
|
|
|
|
|
return nil
|
|
|
}
|
|
@@ -256,14 +314,54 @@ func (c *RetentionCheck) Start() {
|
|
|
defer RetentionChecks.remove(c.conn.User.Username)
|
|
|
defer c.conn.CloseFS() //nolint:errcheck
|
|
|
|
|
|
+ startTime := time.Now()
|
|
|
for _, folder := range c.Folders {
|
|
|
if folder.Retention > 0 {
|
|
|
if err := c.cleanupFolder(folder.Path); err != nil {
|
|
|
c.conn.Log(logger.LevelWarn, "retention check failed, unable to cleanup folder %#v", folder.Path)
|
|
|
+ c.sendNotification(startTime, err) //nolint:errcheck
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
c.conn.Log(logger.LevelInfo, "retention check completed")
|
|
|
+ c.sendNotification(startTime, nil) //nolint:errcheck
|
|
|
+}
|
|
|
+
|
|
|
+func (c *RetentionCheck) sendNotification(startTime time.Time, err error) error {
|
|
|
+ switch c.Notification {
|
|
|
+ case RetentionCheckNotificationEmail:
|
|
|
+ body := new(bytes.Buffer)
|
|
|
+ data := make(map[string]interface{})
|
|
|
+ data["Results"] = c.results
|
|
|
+ totalDeletedFiles := 0
|
|
|
+ totalDeletedSize := int64(0)
|
|
|
+ for _, result := range c.results {
|
|
|
+ totalDeletedFiles += result.DeletedFiles
|
|
|
+ totalDeletedSize += result.DeletedSize
|
|
|
+ }
|
|
|
+ data["HumanizeSize"] = util.ByteCountIEC
|
|
|
+ data["TotalFiles"] = totalDeletedFiles
|
|
|
+ data["TotalSize"] = totalDeletedSize
|
|
|
+ data["Elapsed"] = time.Since(startTime)
|
|
|
+ data["Username"] = c.conn.User.Username
|
|
|
+ data["StartTime"] = util.GetTimeFromMsecSinceEpoch(c.StartTime)
|
|
|
+ if err == nil {
|
|
|
+ data["Status"] = "Succeeded"
|
|
|
+ } else {
|
|
|
+ data["Status"] = "Failed"
|
|
|
+ }
|
|
|
+ if err := smtp.RenderRetentionReportTemplate(body, data); err != nil {
|
|
|
+ c.conn.Log(logger.LevelWarn, "unable to render retention check template: %v", err)
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ subject := fmt.Sprintf("Retention check completed for user %#v", c.conn.User.Username)
|
|
|
+ if err := smtp.SendEmail(c.Email, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
|
|
|
+ c.conn.Log(logger.LevelWarn, "unable to notify retention check result via email: %v", err)
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ c.conn.Log(logger.LevelInfo, "retention check result successfully notified via email")
|
|
|
+ }
|
|
|
+ return nil
|
|
|
}
|