Skip to content

Commit 1fa7d48

Browse files
Add support for copy command (#73)
* add copy section (with schedules) * docs: add section on copy command
1 parent ff2fc1f commit 1fa7d48

File tree

11 files changed

+184
-16
lines changed

11 files changed

+184
-16
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"cSpell.words": [
3+
"crond",
4+
"ionice",
35
"launchd",
46
"restic",
57
"resticprofile",

README.md

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ With resticprofile:
3131
* You can generate a simple status file to send to some monitoring software and make sure your backups are running fine
3232
* You can use a template syntax in your configuration file
3333
* You can generate scheduled tasks using *crond*
34-
* **[new for v0.12.0]** Get backup statistics in your status file
34+
* Get backup statistics in your status file
3535
* **[new for v0.14.0]** Automatically clear up [stale locks](#locks)
3636
* **[new for v0.15.0]** Export a **prometheus** file after a backup, or send the report to a push gateway automatically
37+
* **[new for v0.16.0]** Full support for the copy command (with scheduling)
3738

3839
The configuration file accepts various formats:
3940
* [TOML](https://github.com/toml-lang/toml) : configuration file with extension _.toml_ and _.conf_ to keep compatibility with versions before 0.6.0
@@ -65,7 +66,8 @@ For the rest of the documentation, I'll be showing examples using different form
6566
* [Simple YAML configuration](#simple-yaml-configuration)
6667
* [More complex configuration in TOML](#more-complex-configuration-in-toml)
6768
* [TOML configuration example for Windows](#toml-configuration-example-for-windows)
68-
* [Use stdin in configuration](#use-stdin-in-configuration)
69+
* [Use stdin in configuration](#use-stdin-in-configuration)
70+
* [Special case for the copy command section](#special-case-for-the-copy-command-section)
6971
* [Configuration paths](#configuration-paths)
7072
* [macOS X](#macos-x)
7173
* [Other unixes (Linux and BSD)](#other-unixes-linux-and-bsd)
@@ -212,7 +214,7 @@ Installation using Ansible is not supported out of the box yet, but since I'm us
212214

213215
## Installation from source
214216

215-
You can download the source code and recompile it, it's actually very easy! all you need to have on your machine is:
217+
You can download the source code and compile it, it's actually very easy! all you need to have on your machine is:
216218
- `git`
217219
- [go compiler](https://golang.org/dl/)
218220
- `GNU Make` which is installed by default on many unix boxes. On debian based distributions (Ubuntu included) the package is called `build-essential`.
@@ -555,7 +557,7 @@ run-after = "echo All Done!"
555557
no-error-on-warning = true
556558
```
557559

558-
### Use stdin in configuration
560+
## Use stdin in configuration
559561

560562
Simple example sending a file via stdin
561563

@@ -572,6 +574,37 @@ tag = [ 'stdin' ]
572574

573575
```
574576

577+
## Special case for the `copy` command section
578+
579+
The copy command needs two repository (and quite likely 2 different set of keys). You can configure a `copy` section like this:
580+
581+
```toml
582+
[default]
583+
initialize = false
584+
repository = "/backup/original"
585+
password-file = "key"
586+
587+
[default.copy]
588+
initialize = true
589+
repository = "/backup/copy"
590+
password-file = "other_key"
591+
```
592+
593+
You will note that the secondary repository doesn't need to have a `2` behind its flags (`repository2`, `password-file2`, etc.). It's because the flags are well separated in the configuration.
594+
595+
Here's the same configuration in YAML format:
596+
597+
```yaml
598+
default:
599+
initialize: false
600+
repository: "/backup/original"
601+
password-file: key
602+
copy:
603+
initialize: true
604+
repository: "/backup/copy"
605+
password-file: other_key
606+
```
607+
575608
# Configuration paths
576609
577610
The default name for the configuration file is `profiles`, without an extension.
@@ -957,13 +990,14 @@ global:
957990

958991
Each profile can be scheduled independently (groups are not available for scheduling yet).
959992

960-
These 4 profile sections are accepting a schedule configuration:
993+
These 5 profile sections are accepting a schedule configuration:
961994
- backup
962995
- check
963996
- forget (version 0.11.0)
964997
- prune (version 0.11.0)
998+
- copy (version 0.16.0)
965999

966-
which mean you can schedule `backup`, `forget`, `prune` and `check` independently (I recommend to use a local `lock` in this case).
1000+
which mean you can schedule `backup`, `forget`, `prune`, `check` and `copy` independently (I recommend to use a local `lock` in this case).
9671001

9681002
## retention schedule is deprecated
9691003
**Important**:
@@ -2075,6 +2109,7 @@ Flags passed to the restic command line
20752109
* **password-file**: string
20762110
* **quiet**: true / false
20772111
* **repository**: string **(will be passed as 'repo' to the command line)**
2112+
* **repository-file**: string
20782113
* **tls-client-cert**: string
20792114
* **verbose**: true / false OR integer
20802115

@@ -2223,6 +2258,24 @@ Flags passed to the restic command line
22232258
* **snapshot-template**: string
22242259
* **tag**: string OR list of strings
22252260

2261+
`[profile.copy]`
2262+
2263+
Flags used by resticprofile only
2264+
2265+
* **initialize**: true / false
2266+
2267+
2268+
Flags passed to the restic command line
2269+
2270+
* **key-hint**: string
2271+
* **password-command**: command
2272+
* **password-file**: string
2273+
* **path**: string OR list of strings
2274+
* **repository**: repository
2275+
* **repository-file**: string
2276+
* **tag**: string OR list of strings
2277+
2278+
22262279
# Appendix
22272280

22282281
As an example, here's a similar configuration file in YAML:

config/confidential.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func (c *ConfidentialValue) hideSubmatches(pattern *regexp.Regexp) {
4040
return
4141
}
4242

43-
if matches := pattern.FindStringSubmatchIndex(c.confidential); matches != nil && len(matches) > 2 {
43+
if matches := pattern.FindStringSubmatchIndex(c.confidential); len(matches) > 2 {
4444
c.public = c.confidential
4545

4646
for i := len(matches) - 2; i > 1; i -= 2 {
@@ -121,11 +121,9 @@ func getAllConfidentialValues(profile *Profile) []*ConfidentialValue {
121121
}
122122

123123
func convertToNonConfidential(confidentials []*ConfidentialValue, value string) string {
124-
if confidentials != nil {
125-
for _, c := range confidentials {
126-
if c != nil && c.IsConfidential() && value == c.Value() {
127-
return c.String()
128-
}
124+
for _, c := range confidentials {
125+
if c != nil && c.IsConfidential() && value == c.Value() {
126+
return c.String()
129127
}
130128
}
131129
return value

config/profile.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Profile struct {
1818
Quiet bool `mapstructure:"quiet" argument:"quiet"`
1919
Verbose bool `mapstructure:"verbose" argument:"verbose"`
2020
Repository ConfidentialValue `mapstructure:"repository" argument:"repo"`
21+
RepositoryFile string `mapstructure:"repository-file" argument:"repository-file"`
2122
PasswordFile string `mapstructure:"password-file" argument:"password-file"`
2223
CacheDir string `mapstructure:"cache-dir" argument:"cache-dir"`
2324
CACert string `mapstructure:"cacert" argument:"cacert"`
@@ -43,6 +44,7 @@ type Profile struct {
4344
Snapshots map[string]interface{} `mapstructure:"snapshots"`
4445
Forget *OtherSectionWithSchedule `mapstructure:"forget"`
4546
Mount map[string]interface{} `mapstructure:"mount"`
47+
Copy *CopySection `mapstructure:"copy"`
4648
}
4749

4850
// BackupSection contains the specific configuration to the 'backup' command
@@ -90,6 +92,18 @@ type ScheduleSection struct {
9092
ScheduleLockWait time.Duration `mapstructure:"schedule-lock-wait"`
9193
}
9294

95+
// CopySection contains the destination parameters for a copy command
96+
type CopySection struct {
97+
Initialize bool `mapstructure:"initialize"`
98+
Repository ConfidentialValue `mapstructure:"repository" argument:"repo2"`
99+
RepositoryFile string `mapstructure:"repository-file" argument:"repository-file2"`
100+
PasswordFile string `mapstructure:"password-file" argument:"password-file2"`
101+
PasswordCommand string `mapstructure:"password-command" argument:"password-command2"`
102+
KeyHint string `mapstructure:"key-hint" argument:"key-hint2"`
103+
ScheduleSection `mapstructure:",squash"`
104+
OtherFlags map[string]interface{} `mapstructure:",remain"`
105+
}
106+
93107
// NewProfile instantiates a new blank profile
94108
func NewProfile(c *Config, name string) *Profile {
95109
return &Profile{
@@ -108,6 +122,7 @@ func (p *Profile) SetRootPath(rootPath string) {
108122

109123
p.Lock = fixPath(p.Lock, expandEnv, absolutePrefix(rootPath))
110124
p.PasswordFile = fixPath(p.PasswordFile, expandEnv, absolutePrefix(rootPath))
125+
p.RepositoryFile = fixPath(p.RepositoryFile, expandEnv, absolutePrefix(rootPath))
111126
p.CacheDir = fixPath(p.CacheDir, expandEnv, absolutePrefix(rootPath))
112127
p.CACert = fixPath(p.CACert, expandEnv, absolutePrefix(rootPath))
113128
p.TLSClientCert = fixPath(p.TLSClientCert, expandEnv, absolutePrefix(rootPath))
@@ -137,6 +152,11 @@ func (p *Profile) SetRootPath(rootPath string) {
137152
p.Backup.Iexclude = fixPaths(p.Backup.Iexclude, expandEnv)
138153
}
139154
}
155+
156+
if p.Copy != nil {
157+
p.Copy.PasswordFile = fixPath(p.Copy.PasswordFile, expandEnv, absolutePrefix(rootPath))
158+
p.Copy.RepositoryFile = fixPath(p.Copy.RepositoryFile, expandEnv, absolutePrefix(rootPath))
159+
}
140160
}
141161

142162
// SetHost will replace any host value from a boolean to the hostname
@@ -205,6 +225,14 @@ func (p *Profile) GetCommandFlags(command string) *shell.Args {
205225
if p.Mount != nil {
206226
flags = addOtherArgs(flags, p.Mount)
207227
}
228+
229+
case constants.CommandCopy:
230+
if p.Copy != nil {
231+
flags = convertStructToArgs(*p.Copy, flags)
232+
if p.Copy.OtherFlags != nil {
233+
flags = addOtherArgs(flags, p.Copy.OtherFlags)
234+
}
235+
}
208236
}
209237

210238
return flags
@@ -329,6 +357,7 @@ func (p *Profile) allSchedulableSections() map[string]interface{} {
329357
constants.CommandCheck: p.Check,
330358
constants.CommandForget: p.Forget,
331359
constants.CommandPrune: p.Prune,
360+
constants.CommandCopy: p.Copy,
332361
}
333362
}
334363

@@ -339,6 +368,8 @@ func getScheduleSection(section interface{}) *ScheduleSection {
339368
return &v.ScheduleSection
340369
case *RetentionSection:
341370
return &v.ScheduleSection
371+
case *CopySection:
372+
return &v.ScheduleSection
342373
case *OtherSectionWithSchedule:
343374
return &v.ScheduleSection
344375
}

config/profile_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ exclude-file = "exclude"
223223
files-from = "include"
224224
exclude = "exclude"
225225
iexclude = "iexclude"
226+
[profile.copy]
227+
password-file = "key"
226228
`
227229
profile, err := getProfile("toml", testConfig, "profile")
228230
if err != nil {
@@ -240,6 +242,7 @@ iexclude = "iexclude"
240242
assert.ElementsMatch(t, []string{"/wd/include"}, profile.Backup.FilesFrom)
241243
assert.ElementsMatch(t, []string{"exclude"}, profile.Backup.Exclude)
242244
assert.ElementsMatch(t, []string{"iexclude"}, profile.Backup.Iexclude)
245+
assert.Equal(t, "/wd/key", profile.Copy.PasswordFile)
243246
}
244247

245248
func TestHostInProfile(t *testing.T) {
@@ -556,7 +559,7 @@ initialize = true
556559
}
557560

558561
sections := NewProfile(nil, "").SchedulableCommands()
559-
assert.Len(sections, 5)
562+
assert.Len(sections, 6)
560563

561564
for _, command := range sections {
562565
// Check that schedule is supported
@@ -711,6 +714,8 @@ other-flag-forget = true
711714
other-flag-prune = true
712715
[profile.mount]
713716
other-flag-mount = true
717+
[profile.copy]
718+
other-flag-copy = true
714719
`},
715720
{"json", `
716721
{
@@ -722,7 +727,8 @@ other-flag-mount = true
722727
"check": {"other-flag-check": true},
723728
"forget": {"other-flag-forget": true},
724729
"prune": {"other-flag-prune": true},
725-
"mount": {"other-flag-mount": true}
730+
"mount": {"other-flag-mount": true},
731+
"copy": {"other-flag-copy": true}
726732
}
727733
}`},
728734
{"yaml", `---
@@ -742,6 +748,8 @@ profile:
742748
other-flag-prune: true
743749
mount:
744750
other-flag-mount: true
751+
copy:
752+
other-flag-copy: true
745753
`},
746754
{"hcl", `
747755
"profile" = {
@@ -767,6 +775,9 @@ profile:
767775
mount = {
768776
other-flag-mount = true
769777
}
778+
copy = {
779+
other-flag-copy = true
780+
}
770781
}
771782
`},
772783
}
@@ -786,6 +797,7 @@ profile:
786797
require.NotNil(t, profile.Mount)
787798
require.NotNil(t, profile.Prune)
788799
require.NotNil(t, profile.Snapshots)
800+
require.NotNil(t, profile.Copy)
789801

790802
flags := profile.GetCommonFlags()
791803
assert.Equal(t, 1, len(flags.ToMap()))
@@ -831,6 +843,12 @@ profile:
831843
assert.ElementsMatch(t, []string{"1"}, flags.ToMap()["other-flag"])
832844
_, found = flags.ToMap()["other-flag-mount"]
833845
assert.True(t, found)
846+
847+
flags = profile.GetCommandFlags("copy")
848+
assert.Equal(t, 2, len(flags.ToMap()))
849+
assert.ElementsMatch(t, []string{"1"}, flags.ToMap()["other-flag"])
850+
_, found = flags.ToMap()["other-flag-copy"]
851+
assert.True(t, found)
834852
})
835853
}
836854
}

constants/command.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ const (
1010
CommandSnapshots = "snapshots"
1111
CommandUnlock = "unlock"
1212
CommandMount = "mount"
13+
CommandCopy = "copy"
1314
)

examples/dev.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ default:
2525
password-file: key
2626
repository: "/Volumes/RAMDisk/{{ .Profile.Name }}"
2727
lock: "/Volumes/RAMDisk/resticprofile-{{ .Profile.Name }}.lock"
28+
copy:
29+
password-file: key
30+
repository: "/Volumes/RAMDisk/{{ .Profile.Name }}-copy"
2831

2932
space:
3033
description: Repository contains space
@@ -105,12 +108,16 @@ self:
105108
- "echo restic stderr = ${RESTIC_STDERR}"
106109
check:
107110
schedule:
108-
- "*:15,45"
111+
- "*:15"
109112
retention:
110113
after-backup: true
111114
forget:
112115
schedule: "weekly"
113116
schedule-priority: standard
117+
copy:
118+
initialize: true
119+
schedule:
120+
- "*:45"
114121

115122
prom:
116123
force-inactive-lock: true

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626

2727
// These fields are populated by the goreleaser build
2828
var (
29-
version = "0.15.0-dev"
29+
version = "0.16.0-dev"
3030
commit = ""
3131
date = ""
3232
builtBy = ""

0 commit comments

Comments
 (0)