mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 17:10:28 +00:00
eventmanager: allow to access the backup file
so it can be used in email and other actions Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
b8ef94ece7
commit
0389605d65
7 changed files with 171 additions and 16 deletions
3
go.mod
3
go.mod
|
@ -77,7 +77,8 @@ require (
|
|||
|
||||
require (
|
||||
cloud.google.com/go v0.105.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.0 // indirect
|
||||
cloud.google.com/go/compute v1.12.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
cloud.google.com/go/iam v0.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
|
|
6
go.sum
6
go.sum
|
@ -50,8 +50,10 @@ cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6m
|
|||
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
|
||||
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
|
||||
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
|
||||
cloud.google.com/go/compute/metadata v0.2.0 h1:nBbNSZyDpkNlo3DepaaLKVuO7ClyifSAmNloSCZrHnQ=
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
|
||||
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
|
||||
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
|
||||
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -476,6 +477,15 @@ func (p *EventParams) AddError(err error) {
|
|||
p.errors = append(p.errors, err.Error())
|
||||
}
|
||||
|
||||
func (p *EventParams) setBackupParams(backupPath string) {
|
||||
if p.sender != "" {
|
||||
return
|
||||
}
|
||||
p.sender = dataprovider.ActionExecutorSystem
|
||||
p.FsPath = backupPath
|
||||
p.VirtualPath = filepath.Base(backupPath)
|
||||
}
|
||||
|
||||
func (p *EventParams) getStatusString() string {
|
||||
switch p.Status {
|
||||
case 1:
|
||||
|
@ -503,6 +513,18 @@ func (p *EventParams) getUsers() ([]dataprovider.User, error) {
|
|||
}
|
||||
|
||||
func (p *EventParams) getUserFromSender() (dataprovider.User, error) {
|
||||
if p.sender == dataprovider.ActionExecutorSystem {
|
||||
return dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Status: 1,
|
||||
Username: p.sender,
|
||||
HomeDir: dataprovider.GetBackupsPath(),
|
||||
Permissions: map[string][]string{
|
||||
"/": {dataprovider.PermAny},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
user, err := dataprovider.UserExists(p.sender)
|
||||
if err != nil {
|
||||
eventManagerLog(logger.LevelError, "unable to get user %q: %+v", p.sender, err)
|
||||
|
@ -1903,7 +1925,11 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
|
|||
case dataprovider.ActionTypeEmail:
|
||||
err = executeEmailRuleAction(action.Options.EmailConfig, params)
|
||||
case dataprovider.ActionTypeBackup:
|
||||
err = dataprovider.ExecuteBackup()
|
||||
var backupPath string
|
||||
backupPath, err = dataprovider.ExecuteBackup()
|
||||
if err == nil {
|
||||
params.setBackupParams(backupPath)
|
||||
}
|
||||
case dataprovider.ActionTypeUserQuotaReset:
|
||||
err = executeUsersQuotaResetRuleAction(conditions, params)
|
||||
case dataprovider.ActionTypeFolderQuotaReset:
|
||||
|
|
|
@ -3353,6 +3353,10 @@ func TestEventRule(t *testing.T) {
|
|||
}
|
||||
a2 := dataprovider.BaseEventAction{
|
||||
Name: "action2",
|
||||
Type: dataprovider.ActionTypeBackup,
|
||||
}
|
||||
a3 := dataprovider.BaseEventAction{
|
||||
Name: "action3",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
|
@ -3362,8 +3366,8 @@ func TestEventRule(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
a3 := dataprovider.BaseEventAction{
|
||||
Name: "action3",
|
||||
a4 := dataprovider.BaseEventAction{
|
||||
Name: "action4",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
|
@ -3379,6 +3383,9 @@ func TestEventRule(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
action3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
action4, _, err := httpdtest.AddEventAction(a4, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
r1 := dataprovider.EventRule{
|
||||
Name: "test rule1",
|
||||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
|
@ -3417,6 +3424,12 @@ func TestEventRule(t *testing.T) {
|
|||
Name: action3.Name,
|
||||
},
|
||||
Order: 3,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action4.Name,
|
||||
},
|
||||
Order: 4,
|
||||
Options: dataprovider.EventActionOptions{
|
||||
IsFailureAction: true,
|
||||
},
|
||||
|
@ -3442,13 +3455,13 @@ func TestEventRule(t *testing.T) {
|
|||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
Name: action3.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action3.Name,
|
||||
Name: action4.Name,
|
||||
},
|
||||
Order: 2,
|
||||
Options: dataprovider.EventActionOptions{
|
||||
|
@ -3469,7 +3482,7 @@ func TestEventRule(t *testing.T) {
|
|||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
Name: action3.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
|
@ -3645,6 +3658,8 @@ func TestEventRule(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action4, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
lastReceivedEmail.reset()
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -4237,6 +4252,89 @@ func TestEventFsActionsGroupFilters(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBackupAsAttachment(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 2525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
a1 := dataprovider.BaseEventAction{
|
||||
Name: "a1",
|
||||
Type: dataprovider.ActionTypeBackup,
|
||||
}
|
||||
a2 := dataprovider.BaseEventAction{
|
||||
Name: "a2",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
Recipients: []string{"test@example.com"},
|
||||
Subject: `"{{Event}} {{StatusString}}"`,
|
||||
Body: "Domain: {{Name}}",
|
||||
Attachments: []string{"/{{VirtualPath}}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
r1 := dataprovider.EventRule{
|
||||
Name: "test rule certificate",
|
||||
Trigger: dataprovider.EventTriggerCertificate,
|
||||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action1.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
},
|
||||
Order: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
lastReceivedEmail.reset()
|
||||
renewalEvent := "Certificate renewal"
|
||||
|
||||
common.HandleCertificateEvent(common.EventParams{
|
||||
Name: "example.com",
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Status: 1,
|
||||
Event: renewalEvent,
|
||||
})
|
||||
assert.Eventually(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
|
||||
assert.Contains(t, email.Data, `Domain: example.com`)
|
||||
assert.Contains(t, email.Data, "Content-Type: application/json")
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventActionHTTPMultipart(t *testing.T) {
|
||||
a1 := dataprovider.BaseEventAction{
|
||||
Name: "action1",
|
||||
|
|
|
@ -524,36 +524,36 @@ func (c *Config) requireCustomTLSForMySQL() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (c *Config) doBackup() error {
|
||||
func (c *Config) doBackup() (string, error) {
|
||||
now := time.Now().UTC()
|
||||
outputFile := filepath.Join(c.BackupsPath, fmt.Sprintf("backup_%s_%d.json", now.Weekday(), now.Hour()))
|
||||
providerLog(logger.LevelDebug, "starting backup to file %q", outputFile)
|
||||
err := os.MkdirAll(filepath.Dir(outputFile), 0700)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to create backup dir %q: %v", outputFile, err)
|
||||
return fmt.Errorf("unable to create backup dir: %w", err)
|
||||
return outputFile, fmt.Errorf("unable to create backup dir: %w", err)
|
||||
}
|
||||
backup, err := DumpData()
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to execute backup: %v", err)
|
||||
return fmt.Errorf("unable to dump backup data: %w", err)
|
||||
return outputFile, fmt.Errorf("unable to dump backup data: %w", err)
|
||||
}
|
||||
dump, err := json.Marshal(backup)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to marshal backup as JSON: %v", err)
|
||||
return fmt.Errorf("unable to marshal backup data as JSON: %w", err)
|
||||
return outputFile, fmt.Errorf("unable to marshal backup data as JSON: %w", err)
|
||||
}
|
||||
err = os.WriteFile(outputFile, dump, 0600)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to save backup: %v", err)
|
||||
return fmt.Errorf("unable to save backup: %w", err)
|
||||
return outputFile, fmt.Errorf("unable to save backup: %w", err)
|
||||
}
|
||||
providerLog(logger.LevelDebug, "backup saved to %q", outputFile)
|
||||
return nil
|
||||
return outputFile, nil
|
||||
}
|
||||
|
||||
// ExecuteBackup executes a backup
|
||||
func ExecuteBackup() error {
|
||||
func ExecuteBackup() (string, error) {
|
||||
return config.doBackup()
|
||||
}
|
||||
|
||||
|
@ -833,6 +833,12 @@ func Initialize(cnf Config, basePath string, checkAdmins bool) error {
|
|||
if cnf.BackupsPath == "" {
|
||||
return fmt.Errorf("required directory is invalid, backup path %#v", cnf.BackupsPath)
|
||||
}
|
||||
absoluteBackupPath, err := util.GetAbsolutePath(cnf.BackupsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get absolute backup path: %w", err)
|
||||
}
|
||||
config.BackupsPath = absoluteBackupPath
|
||||
providerLog(logger.LevelDebug, "absolute backup path %q", config.BackupsPath)
|
||||
|
||||
if err := initializeHashingAlgo(&cnf); err != nil {
|
||||
return err
|
||||
|
|
|
@ -1354,6 +1354,12 @@ func (r *EventRule) hasUserAssociated(providerObjectType string) bool {
|
|||
return providerObjectType == actionObjectUser
|
||||
case EventTriggerFsEvent:
|
||||
return true
|
||||
default:
|
||||
if len(r.Actions) > 0 {
|
||||
// should we allow schedules where backup is not the first action?
|
||||
// maybe we could pass the action index and check before that index
|
||||
return r.Actions[0].Type == ActionTypeBackup
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -727,3 +727,19 @@ func PanicOnError(err error) {
|
|||
panic(fmt.Errorf("unexpected error: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// GetAbsolutePath returns an absolute path using the current dir as base
|
||||
// if name defines a relative path
|
||||
func GetAbsolutePath(name string) (string, error) {
|
||||
if name == "" {
|
||||
return name, errors.New("input path cannot be empty")
|
||||
}
|
||||
if filepath.IsAbs(name) {
|
||||
return name, nil
|
||||
}
|
||||
curDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return name, err
|
||||
}
|
||||
return filepath.Join(curDir, name), nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue