Skip to content

Commit 58ecde0

Browse files
Merge branch run-after-fail
1 parent cf808b0 commit 58ecde0

File tree

7 files changed

+177
-28
lines changed

7 files changed

+177
-28
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ coverage:
7272
clean:
7373
$(GOCLEAN)
7474
rm -rf $(BINARY) $(BINARY_DARWIN) $(BINARY_LINUX) $(BINARY_PI) $(BINARY_WINDOWS) $(COVERAGE_FILE) restic_*_linux_amd64* ${BUILD}restic* dist/*
75+
restic cache --cleanup
7576

7677
test-docker:
7778
docker run --rm -v "${GOPATH}":/go -w /go/src/creativeprojects/resticprofile golang:${GO_VERSION} $(GOTEST) -v $(TESTS)

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ For the rest of the documentation, I'll be mostly showing examples using the TOM
4848
* [Other unixes (Linux and BSD)](#other-unixes-linux-and-bsd)
4949
* [Windows](#windows)
5050
* [Path resolution in configuration](#path-resolution-in-configuration)
51+
* [Run commands before, after success or after failure](#run-commands-before-after-success-or-after-failure)
52+
* [run before and after order during a backup](#run-before-and-after-order-during-a-backup)
5153
* [Using resticprofile](#using-resticprofile)
5254
* [Command line reference](#command-line-reference)
5355
* [Minimum memory required](#minimum-memory-required)
@@ -72,8 +74,6 @@ For the rest of the documentation, I'll be mostly showing examples using the TOM
7274
* [Special case of schedule\-permission=user with sudo](#special-case-of-schedule-permissionuser-with-sudo)
7375
* [Daemon](#daemon)
7476

75-
76-
7777
## Requirements
7878

7979
Since version 0.6.0, resticprofile no longer needs python installed on your machine. It is distributed as an executable (same as restic).
@@ -458,6 +458,44 @@ resticprofile will search for your configuration file in these folders:
458458

459459
All files path in the configuration are resolved from the configuration path. The big **exception** being `source` in `backup` section where it's resolved from the current path where you started resticprofile.
460460

461+
## Run commands before, after success or after failure
462+
463+
resticprofile has 2 places where you can run commands around restic:
464+
465+
- commands that will run before and after every restic command (snapshots, backup, check, forget, prune, mount, etc.). These are placed at the root of each profile.
466+
- commands that will only run before and after a backup: these are placed in the backup section of your profiles.
467+
468+
Here's an example of all the external commands that you can run during the execution of a profile:
469+
470+
```yaml
471+
documents:
472+
inherit: default
473+
run-before: "echo == run-before profile $PROFILE_NAME command $PROFILE_COMMAND"
474+
run-after: "echo == run-after profile $PROFILE_NAME command $PROFILE_COMMAND"
475+
run-after-fail: "echo == Error in profile $PROFILE_NAME command $PROFILE_COMMAND: $ERROR"
476+
backup:
477+
run-before: "echo === run-before backup profile $PROFILE_NAME command $PROFILE_COMMAND"
478+
run-after: "echo === run-after backup profile $PROFILE_NAME command $PROFILE_COMMAND"
479+
source: ~/Documents
480+
```
481+
482+
`run-before`, `run-after` and `run-after-fail` can be a string, or an array of strings if you need to run more than one command
483+
484+
A few environment variables will be set before running these commands:
485+
- `PROFILE_NAME`
486+
- `PROFILE_COMMAND`: backup, check, forget, etc.
487+
488+
Additionally for the `run-after-fail` commands, the `ERROR` environment variable will be set to the latest error message.
489+
490+
### run before and after order during a backup
491+
492+
The commands will be running in this order **during a backup**:
493+
- `run-before` from the profile - if error, go to `run-after-fail`
494+
- `run-before` from the backup section - if error, go to `run-after-fail`
495+
- run the restic backup (with check and retention if configured) - if error, go to `run-after-fail`
496+
- `run-after` from the backup section - if error, go to `run-after-fail`
497+
- `run-after` from the profile - if error, go to `run-after-fail`
498+
461499
## Using resticprofile
462500

463501
Here are a few examples how to run resticprofile (using the main example configuration file)

config/profile.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ func (p *Profile) GetCommandFlags(command string) map[string][]string {
172172

173173
// GetRetentionFlags returns the flags specific to the "forget" command being run as part of a backup
174174
func (p *Profile) GetRetentionFlags() map[string][]string {
175+
// if there was no "other" flags, the map could be un-initialized
176+
if p.Retention.OtherFlags == nil {
177+
p.Retention.OtherFlags = make(map[string]interface{})
178+
}
179+
175180
flags := p.GetCommonFlags()
176181
// Special case of retention: we do copy the "source" from "backup" as "path" if it hasn't been redefined in "retention"
177182
if _, found := p.Retention.OtherFlags[constants.ParameterPath]; !found {

examples/linux.yaml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,19 @@ default:
1010
documents:
1111
inherit: default
1212
initialize: true
13+
run-before: "echo == run-before profile $PROFILE_NAME command $PROFILE_COMMAND"
14+
run-after: "echo == run-after profile $PROFILE_NAME command $PROFILE_COMMAND"
15+
run-after-fail: "echo == Error in profile $PROFILE_NAME command $PROFILE_COMMAND: $ERROR"
1316
backup:
17+
run-before: "echo === run-before backup profile $PROFILE_NAME command $PROFILE_COMMAND"
18+
run-after: "echo === run-after backup profile $PROFILE_NAME command $PROFILE_COMMAND"
1419
tag: documents
1520
source: ~/Documents
16-
schedule:
17-
- "*:00,30" # every 15 minutes
18-
- "*:15,45" # both combined together
21+
schedule: "*:00,15,30,45" # every 15 minutes
22+
schedule-permission: user
23+
check-before: true
24+
retention:
25+
before-backup: true
1926
snapshots:
2027
tag: documents
2128

shell/command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func getShellCommand(command string, args []string) (string, []string, error) {
9090
if err != nil {
9191
return "", nil, fmt.Errorf("cannot find shell executable (cmd.exe) in path")
9292
}
93-
// cmd.exe accepts that all arguments are sent one bye one
93+
// cmd.exe accepts that all arguments are sent one by one
9494
args := append([]string{"/C", command}, removeQuotes(args)...)
9595
return shell, args, nil
9696
}

wrapper.go

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ func (r *resticWrapper) runProfile() error {
123123

124124
return nil
125125
},
126-
func() {
127-
_ = r.runProfilePostFailCommand()
126+
// on failure
127+
func(err error) {
128+
_ = r.runProfilePostFailCommand(err)
128129
},
129130
)
130131
})
@@ -140,30 +141,45 @@ func (r *resticWrapper) runInitialize() error {
140141
rCommand := r.prepareCommand(constants.CommandInit, args)
141142
// don't display any error
142143
rCommand.stderr = nil
143-
return runShellCommand(rCommand)
144+
err := runShellCommand(rCommand)
145+
if err != nil {
146+
return fmt.Errorf("repository initialization on profile '%s': %w", r.profile.Name, err)
147+
}
148+
return nil
144149
}
145150

146151
func (r *resticWrapper) runCheck() error {
147152
clog.Infof("profile '%s': checking repository consistency", r.profile.Name)
148153
args := convertIntoArgs(r.profile.GetCommandFlags(constants.CommandCheck))
149154
rCommand := r.prepareCommand(constants.CommandCheck, args)
150-
return runShellCommand(rCommand)
155+
err := runShellCommand(rCommand)
156+
if err != nil {
157+
return fmt.Errorf("backup check on profile '%s': %w", r.profile.Name, err)
158+
}
159+
return nil
151160
}
152161

153162
func (r *resticWrapper) runRetention() error {
154163
clog.Infof("profile '%s': cleaning up repository using retention information", r.profile.Name)
155164
args := convertIntoArgs(r.profile.GetRetentionFlags())
156165
rCommand := r.prepareCommand(constants.CommandForget, args)
157-
return runShellCommand(rCommand)
166+
err := runShellCommand(rCommand)
167+
if err != nil {
168+
return fmt.Errorf("backup retention on profile '%s': %w", r.profile.Name, err)
169+
}
170+
return nil
158171
}
159172

160173
func (r *resticWrapper) runCommand(command string) error {
161174
clog.Infof("profile '%s': starting '%s'", r.profile.Name, command)
162175
args := convertIntoArgs(r.profile.GetCommandFlags(command))
163176
rCommand := r.prepareCommand(command, args)
164177
err := runShellCommand(rCommand)
178+
if err != nil {
179+
return fmt.Errorf("%s on profile '%s': %w", r.command, r.profile.Name, err)
180+
}
165181
clog.Infof("profile '%s': finished '%s'", r.profile.Name, command)
166-
return err
182+
return nil
167183
}
168184

169185
func (r *resticWrapper) prepareCommand(command string, args []string) shellCommandDefinition {
@@ -202,13 +218,18 @@ func (r *resticWrapper) runPreCommand(command string) error {
202218
if r.profile.Backup == nil || r.profile.Backup.RunBefore == nil || len(r.profile.Backup.RunBefore) == 0 {
203219
return nil
204220
}
221+
env := append(os.Environ(), r.getEnvironment()...)
222+
env = append(env, r.getProfileEnvironment()...)
223+
205224
for i, preCommand := range r.profile.Backup.RunBefore {
206225
clog.Debugf("starting pre-backup command %d/%d", i+1, len(r.profile.Backup.RunBefore))
207-
env := append(os.Environ(), r.getEnvironment()...)
208226
rCommand := newShellCommand(preCommand, nil, env, r.dryRun, r.sigChan)
227+
// stdout are stderr are coming from the default terminal (in case they're redirected)
228+
rCommand.stdout = term.GetOutput()
229+
rCommand.stderr = term.GetErrorOutput()
209230
err := runShellCommand(rCommand)
210231
if err != nil {
211-
return err
232+
return fmt.Errorf("run-before backup on profile '%s': %w", r.profile.Name, err)
212233
}
213234
}
214235
return nil
@@ -222,13 +243,18 @@ func (r *resticWrapper) runPostCommand(command string) error {
222243
if r.profile.Backup == nil || r.profile.Backup.RunAfter == nil || len(r.profile.Backup.RunAfter) == 0 {
223244
return nil
224245
}
246+
env := append(os.Environ(), r.getEnvironment()...)
247+
env = append(env, r.getProfileEnvironment()...)
248+
225249
for i, postCommand := range r.profile.Backup.RunAfter {
226250
clog.Debugf("starting post-backup command %d/%d", i+1, len(r.profile.Backup.RunAfter))
227-
env := append(os.Environ(), r.getEnvironment()...)
228251
rCommand := newShellCommand(postCommand, nil, env, r.dryRun, r.sigChan)
252+
// stdout are stderr are coming from the default terminal (in case they're redirected)
253+
rCommand.stdout = term.GetOutput()
254+
rCommand.stderr = term.GetErrorOutput()
229255
err := runShellCommand(rCommand)
230256
if err != nil {
231-
return err
257+
return fmt.Errorf("run-after backup on profile '%s': %w", r.profile.Name, err)
232258
}
233259
}
234260
return nil
@@ -238,13 +264,18 @@ func (r *resticWrapper) runProfilePreCommand() error {
238264
if r.profile.RunBefore == nil || len(r.profile.RunBefore) == 0 {
239265
return nil
240266
}
267+
env := append(os.Environ(), r.getEnvironment()...)
268+
env = append(env, r.getProfileEnvironment()...)
269+
241270
for i, preCommand := range r.profile.RunBefore {
242271
clog.Debugf("starting 'run-before' profile command %d/%d", i+1, len(r.profile.RunBefore))
243-
env := append(os.Environ(), r.getEnvironment()...)
244272
rCommand := newShellCommand(preCommand, nil, env, r.dryRun, r.sigChan)
273+
// stdout are stderr are coming from the default terminal (in case they're redirected)
274+
rCommand.stdout = term.GetOutput()
275+
rCommand.stderr = term.GetErrorOutput()
245276
err := runShellCommand(rCommand)
246277
if err != nil {
247-
return err
278+
return fmt.Errorf("run-before on profile '%s': %w", r.profile.Name, err)
248279
}
249280
}
250281
return nil
@@ -254,26 +285,37 @@ func (r *resticWrapper) runProfilePostCommand() error {
254285
if r.profile.RunAfter == nil || len(r.profile.RunAfter) == 0 {
255286
return nil
256287
}
288+
env := append(os.Environ(), r.getEnvironment()...)
289+
env = append(env, r.getProfileEnvironment()...)
290+
257291
for i, postCommand := range r.profile.RunAfter {
258292
clog.Debugf("starting 'run-after' profile command %d/%d", i+1, len(r.profile.RunAfter))
259-
env := append(os.Environ(), r.getEnvironment()...)
260293
rCommand := newShellCommand(postCommand, nil, env, r.dryRun, r.sigChan)
294+
// stdout are stderr are coming from the default terminal (in case they're redirected)
295+
rCommand.stdout = term.GetOutput()
296+
rCommand.stderr = term.GetErrorOutput()
261297
err := runShellCommand(rCommand)
262298
if err != nil {
263-
return err
299+
return fmt.Errorf("run-after on profile '%s': %w", r.profile.Name, err)
264300
}
265301
}
266302
return nil
267303
}
268304

269-
func (r *resticWrapper) runProfilePostFailCommand() error {
305+
func (r *resticWrapper) runProfilePostFailCommand(fail error) error {
270306
if r.profile.RunAfterFail == nil || len(r.profile.RunAfterFail) == 0 {
271307
return nil
272308
}
309+
env := append(os.Environ(), r.getEnvironment()...)
310+
env = append(env, r.getProfileEnvironment()...)
311+
env = append(env, fmt.Sprintf("ERROR=%s", fail.Error()))
312+
273313
for i, postCommand := range r.profile.RunAfterFail {
274314
clog.Debugf("starting 'run-after-fail' profile command %d/%d", i+1, len(r.profile.RunAfterFail))
275-
env := append(os.Environ(), r.getEnvironment()...)
276315
rCommand := newShellCommand(postCommand, nil, env, r.dryRun, r.sigChan)
316+
// stdout are stderr are coming from the default terminal (in case they're redirected)
317+
rCommand.stdout = term.GetOutput()
318+
rCommand.stderr = term.GetErrorOutput()
277319
err := runShellCommand(rCommand)
278320
if err != nil {
279321
return err
@@ -282,6 +324,7 @@ func (r *resticWrapper) runProfilePostFailCommand() error {
282324
return nil
283325
}
284326

327+
// getEnvironment returns the environment variables defined in the profile configuration
285328
func (r *resticWrapper) getEnvironment() []string {
286329
if r.profile.Environment == nil || len(r.profile.Environment) == 0 {
287330
return nil
@@ -298,6 +341,15 @@ func (r *resticWrapper) getEnvironment() []string {
298341
return env
299342
}
300343

344+
// getProfileEnvironment returns some environment variables about the current profile
345+
// (name and command for now)
346+
func (r *resticWrapper) getProfileEnvironment() []string {
347+
return []string{
348+
fmt.Sprintf("PROFILE_NAME=%s", r.profile.Name),
349+
fmt.Sprintf("PROFILE_COMMAND=%s", r.command),
350+
}
351+
}
352+
301353
func convertIntoArgs(flags map[string][]string) []string {
302354
args := make([]string, 0)
303355

@@ -361,10 +413,10 @@ func lockRun(filename string, run func() error) error {
361413
}
362414

363415
// runOnFailure will run the onFailure function if an error occurred in the run function
364-
func runOnFailure(run func() error, onFailure func()) error {
416+
func runOnFailure(run func() error, onFailure func(error)) error {
365417
err := run()
366418
if err != nil {
367-
onFailure()
419+
onFailure(err)
368420
}
369421
return err
370422
}

0 commit comments

Comments
 (0)