Skip to content

Commit 1efeb81

Browse files
add run-after-run parameter
to run shell commands after a restic command failed breaking change: the auto initialization of a repository now happens after the "run-before" scripts (in version 0.7.0 it was happening before)
1 parent 77682a7 commit 1efeb81

File tree

5 files changed

+144
-84
lines changed

5 files changed

+144
-84
lines changed

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ With resticprofile:
1616
* You can create groups of profiles that will run sequentially
1717
* You can run shell commands before or after a backup
1818
* You can also run shell commands before or after running a profile (any command): useful if you need to mount your backup disk
19+
* You can run a shell command if an error occurred (at any time)
1920
* You can send a backup stream via _stdin_
2021
* You can start restic at a lower or higher priority (Priority Class in Windows, *nice* in all unixes) and/or _ionice_ (only available on Linux)
2122

@@ -202,6 +203,9 @@ initialize = false
202203
# will run these scripts before and after each command (including 'backup')
203204
run-before = "mount /backup"
204205
run-after = "umount /backup"
206+
# if a restic command fails, the run-after won't be running
207+
# add this parameter to run the script in case of a failure
208+
run-after-fail = "umount /backup"
205209

206210
[default.env]
207211
TMPDIR= "/tmp"
@@ -433,6 +437,7 @@ Flags used by resticprofile only
433437
* **lock**: string: specify a local lockfile
434438
* **run-before**: string OR list of strings
435439
* **run-after**: string OR list of strings
440+
* **run-after-fail**: string OR list of strings
436441

437442
Flags passed to the restic command line
438443

examples/linux.toml

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ repository = "/tmp/backup"
1313
password-file = "key"
1414
initialize = false
1515
no-cache = true
16+
run-before = "echo Started!"
17+
run-after = "echo Finished!"
18+
run-after-fail = "echo An error occured!"
1619

1720
[src]
1821
inherit = "default"

main.go

-24
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/creativeprojects/resticprofile/config"
1616
"github.com/creativeprojects/resticprofile/constants"
1717
"github.com/creativeprojects/resticprofile/filesearch"
18-
"github.com/creativeprojects/resticprofile/lock"
1918
"github.com/creativeprojects/resticprofile/priority"
2019
"github.com/spf13/viper"
2120
)
@@ -267,29 +266,6 @@ func runProfile(global *config.Global, flags commandLineFlags, profileName strin
267266
}
268267
}
269268

270-
// lockRun is making sure the function is only run once by putting a lockfile on the disk
271-
func lockRun(filename string, run func() error) error {
272-
if filename == "" {
273-
// No lock
274-
return run()
275-
}
276-
// Make sure the path to the lock exists
277-
dir := filepath.Dir(filename)
278-
if dir != "" {
279-
err := os.MkdirAll(dir, 0755)
280-
if err != nil {
281-
clog.Warningf("The profile will run without a lockfile: %v", err)
282-
return run()
283-
}
284-
}
285-
runLock := lock.NewLock(filename)
286-
if !runLock.TryAcquire() {
287-
return fmt.Errorf("another process is already running this profile: %s", runLock.Who())
288-
}
289-
defer runLock.Release()
290-
return run()
291-
}
292-
293269
// randomBool returns true for Heads and false for Tails
294270
func randomBool() bool {
295271
return rand.Int31n(10000) < 5000

wrapper.go

+101-58
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package main
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
67
"strings"
78

89
"github.com/creativeprojects/resticprofile/clog"
910
"github.com/creativeprojects/resticprofile/config"
1011
"github.com/creativeprojects/resticprofile/constants"
12+
"github.com/creativeprojects/resticprofile/lock"
1113
)
1214

1315
type resticWrapper struct {
@@ -38,79 +40,88 @@ func newResticWrapper(
3840
}
3941

4042
func (r *resticWrapper) runProfile() error {
41-
if r.initialize && r.command != constants.CommandInit {
42-
_ = r.runInitialize()
43-
// it's ok for the initialize to error out when the repository exists
44-
}
45-
4643
err := lockRun(r.profile.Lock, func() error {
47-
var err error
48-
49-
// pre-profile commands
50-
err = r.runProfilePreCommand()
51-
if err != nil {
52-
return err
53-
}
44+
return runOnFailure(
45+
func() error {
46+
var err error
5447

55-
// pre-commands (for backup)
56-
if r.command == constants.CommandBackup {
57-
// Shell commands
58-
err = r.runPreCommand(r.command)
59-
if err != nil {
60-
return err
61-
}
62-
// Check
63-
if r.profile.Backup != nil && r.profile.Backup.CheckBefore {
64-
err = r.runCheck()
48+
// pre-profile commands
49+
err = r.runProfilePreCommand()
6550
if err != nil {
6651
return err
6752
}
68-
}
69-
// Retention
70-
if r.profile.Retention != nil && r.profile.Retention.BeforeBackup {
71-
err = r.runRetention()
72-
if err != nil {
73-
return err
53+
54+
// breaking change from 0.7.0 and 0.7.1:
55+
// run the initialization after the pre-profile commands
56+
if r.initialize && r.command != constants.CommandInit {
57+
_ = r.runInitialize()
58+
// it's ok for the initialize to error out when the repository exists
7459
}
75-
}
76-
}
7760

78-
// Main command
79-
err = r.runCommand(r.command)
80-
if err != nil {
81-
return err
82-
}
61+
// pre-commands (for backup)
62+
if r.command == constants.CommandBackup {
63+
// Shell commands
64+
err = r.runPreCommand(r.command)
65+
if err != nil {
66+
return err
67+
}
68+
// Check
69+
if r.profile.Backup != nil && r.profile.Backup.CheckBefore {
70+
err = r.runCheck()
71+
if err != nil {
72+
return err
73+
}
74+
}
75+
// Retention
76+
if r.profile.Retention != nil && r.profile.Retention.BeforeBackup {
77+
err = r.runRetention()
78+
if err != nil {
79+
return err
80+
}
81+
}
82+
}
8383

84-
// post-commands (for backup)
85-
if r.command == constants.CommandBackup {
86-
// Retention
87-
if r.profile.Retention != nil && r.profile.Retention.AfterBackup {
88-
err = r.runRetention()
84+
// Main command
85+
err = r.runCommand(r.command)
8986
if err != nil {
9087
return err
9188
}
92-
}
93-
// Check
94-
if r.profile.Backup != nil && r.profile.Backup.CheckAfter {
95-
err = r.runCheck()
89+
90+
// post-commands (for backup)
91+
if r.command == constants.CommandBackup {
92+
// Retention
93+
if r.profile.Retention != nil && r.profile.Retention.AfterBackup {
94+
err = r.runRetention()
95+
if err != nil {
96+
return err
97+
}
98+
}
99+
// Check
100+
if r.profile.Backup != nil && r.profile.Backup.CheckAfter {
101+
err = r.runCheck()
102+
if err != nil {
103+
return err
104+
}
105+
}
106+
// Shell commands
107+
err = r.runPostCommand(r.command)
108+
if err != nil {
109+
return err
110+
}
111+
}
112+
113+
// post-profile commands
114+
err = r.runProfilePostCommand()
96115
if err != nil {
97116
return err
98117
}
99-
}
100-
// Shell commands
101-
err = r.runPostCommand(r.command)
102-
if err != nil {
103-
return err
104-
}
105-
}
106-
107-
// post-profile commands
108-
err = r.runProfilePostCommand()
109-
if err != nil {
110-
return err
111-
}
112118

113-
return nil
119+
return nil
120+
},
121+
func() {
122+
_ = r.runProfilePostFailCommand()
123+
},
124+
)
114125
})
115126
if err != nil {
116127
return err
@@ -312,3 +323,35 @@ func convertIntoArgs(flags map[string][]string) []string {
312323
}
313324
return args
314325
}
326+
327+
// lockRun is making sure the function is only run once by putting a lockfile on the disk
328+
func lockRun(filename string, run func() error) error {
329+
if filename == "" {
330+
// No lock
331+
return run()
332+
}
333+
// Make sure the path to the lock exists
334+
dir := filepath.Dir(filename)
335+
if dir != "" {
336+
err := os.MkdirAll(dir, 0755)
337+
if err != nil {
338+
clog.Warningf("The profile will run without a lockfile: %v", err)
339+
return run()
340+
}
341+
}
342+
runLock := lock.NewLock(filename)
343+
if !runLock.TryAcquire() {
344+
return fmt.Errorf("another process is already running this profile: %s", runLock.Who())
345+
}
346+
defer runLock.Release()
347+
return run()
348+
}
349+
350+
// runOnFailure will run the onFailure function if an error occurred in the run function
351+
func runOnFailure(run func() error, onFailure func()) error {
352+
err := run()
353+
if err != nil {
354+
onFailure()
355+
}
356+
return err
357+
}

wrapper_test.go

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"os"
45
"testing"
56

67
"github.com/creativeprojects/resticprofile/config"
@@ -69,14 +70,46 @@ func TestConversionToArgs(t *testing.T) {
6970
func TestPreProfileScriptFail(t *testing.T) {
7071
profile := config.NewProfile("name")
7172
profile.RunBefore = []string{"exit 1"} // this should both work on unix shell and windows batch
72-
wrapper := newResticWrapper("restic", false, profile, "test", nil, nil)
73+
wrapper := newResticWrapper("echo", false, profile, "test", nil, nil)
7374
err := wrapper.runProfile()
7475
assert.EqualError(t, err, "exit status 1")
7576
}
7677

77-
func TestRunEmptyProfile(t *testing.T) {
78+
func TestPostProfileScriptFail(t *testing.T) {
79+
profile := config.NewProfile("name")
80+
profile.RunAfter = []string{"exit 1"} // this should both work on unix shell and windows batch
81+
wrapper := newResticWrapper("echo", false, profile, "test", nil, nil)
82+
err := wrapper.runProfile()
83+
assert.EqualError(t, err, "exit status 1")
84+
}
85+
86+
func TestRunEchoProfile(t *testing.T) {
7887
profile := config.NewProfile("name")
7988
wrapper := newResticWrapper("echo", false, profile, "test", nil, nil)
8089
err := wrapper.runProfile()
8190
assert.NoError(t, err)
8291
}
92+
93+
func TestPostProfileAfterFail(t *testing.T) {
94+
testFile := "TestPostProfileAfterFail.txt"
95+
_ = os.Remove(testFile)
96+
profile := config.NewProfile("name")
97+
profile.RunAfter = []string{"echo failed > " + testFile}
98+
wrapper := newResticWrapper("exit", false, profile, "1", nil, nil)
99+
err := wrapper.runProfile()
100+
assert.EqualError(t, err, "exit status 1")
101+
assert.NoFileExistsf(t, testFile, "the run-after script should not have been running")
102+
_ = os.Remove(testFile)
103+
}
104+
105+
func TestPostFailProfile(t *testing.T) {
106+
testFile := "TestPostFailProfile.txt"
107+
_ = os.Remove(testFile)
108+
profile := config.NewProfile("name")
109+
profile.RunAfterFail = []string{"echo failed > " + testFile}
110+
wrapper := newResticWrapper("exit", false, profile, "1", nil, nil)
111+
err := wrapper.runProfile()
112+
assert.EqualError(t, err, "exit status 1")
113+
assert.FileExistsf(t, testFile, "the run-after-fail script has not been running")
114+
_ = os.Remove(testFile)
115+
}

0 commit comments

Comments
 (0)