Skip to content

Commit e5058f9

Browse files
committed
Allow standard copy in addition to the "special case"
resticprofile implements a "special case" for the copy command, which does not use the "from-"-prefix and reverses the copy direction. With this change the standard direction is used when the "from" prefix is explicitly used.
1 parent 77b1f30 commit e5058f9

File tree

3 files changed

+221
-65
lines changed

3 files changed

+221
-65
lines changed

config/profile.go

Lines changed: 107 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -344,96 +344,147 @@ func (s *ScheduleBaseSection) getScheduleConfig(p *Profile, command string) *Sch
344344
return s.scheduleConfig
345345
}
346346

347-
// CopySection contains the destination parameters for a copy command
347+
// CopySection contains the source or destination parameters for a copy command
348348
type CopySection struct {
349349
GenericSectionWithSchedule `mapstructure:",squash"`
350350
Initialize bool `mapstructure:"initialize" description:"Initialize the secondary repository if missing"`
351351
InitializeCopyChunkerParams maybe.Bool `mapstructure:"initialize-copy-chunker-params" default:"true" description:"Copy chunker parameters when initializing the secondary repository"`
352-
Repository ConfidentialValue `mapstructure:"repository" description:"Destination repository to copy snapshots to"`
353-
RepositoryFile string `mapstructure:"repository-file" description:"File from which to read the destination repository location to copy snapshots to"`
354-
PasswordFile string `mapstructure:"password-file" description:"File to read the destination repository password from"`
355-
PasswordCommand string `mapstructure:"password-command" description:"Shell command to obtain the destination repository password from"`
356-
KeyHint string `mapstructure:"key-hint" description:"Key ID of key to try decrypting the destination repository first"`
352+
FromRepository ConfidentialValue `mapstructure:"from-repo" argument:"from-repo" description:"Destination repository to copy snapshots from"`
353+
FromRepositoryFile string `mapstructure:"from-repository-file" argument:"from-repository-file" description:"File from which to read the source repository location to copy snapshots from"`
354+
FromPasswordFile string `mapstructure:"from-password-file" argument:"from-password-file" description:"File to read the source repository password from"`
355+
FromPasswordCommand string `mapstructure:"from-password-command" argument:"from-password-command" description:"Shell command to obtain the source repository password from"`
356+
FromKeyHint string `mapstructure:"from-key-hint" argument:"from-key-hint" description:"Key ID of key to try decrypting the source repository first"`
357357
Snapshots []string `mapstructure:"snapshot" description:"Snapshot IDs to copy (if empty, all snapshots are copied)"`
358+
ToRepository ConfidentialValue `mapstructure:"repository" description:"Destination repository to copy snapshots to"`
359+
ToRepositoryFile string `mapstructure:"repository-file" description:"File from which to read the destination repository location to copy snapshots to"`
360+
ToPasswordFile string `mapstructure:"password-file" description:"File to read the destination repository password from"`
361+
ToPasswordCommand string `mapstructure:"password-command" description:"Shell command to obtain the destination repository password from"`
362+
ToKeyHint string `mapstructure:"key-hint" description:"Key ID of key to try decrypting the destination repository first"`
358363
}
359364

360365
func (s *CopySection) IsEmpty() bool { return s == nil }
361366

367+
func (s *CopySection) IsCopyTo() bool { return s.ToRepository.HasValue() || s.ToRepositoryFile != "" }
368+
362369
func (c *CopySection) resolve(p *Profile) {
363370
c.ScheduleBaseSection.resolve(p)
364371

365-
c.Repository.setValue(fixPath(c.Repository.Value(), expandEnv, expandUserHome))
372+
c.ToRepository.setValue(fixPath(c.ToRepository.Value(), expandEnv, expandUserHome))
366373
}
367374

368375
func (c *CopySection) setRootPath(p *Profile, rootPath string) {
369376
c.GenericSectionWithSchedule.setRootPath(p, rootPath)
370377

371-
c.PasswordFile = fixPath(c.PasswordFile, expandEnv, expandUserHome, absolutePrefix(rootPath))
372-
c.RepositoryFile = fixPath(c.RepositoryFile, expandEnv, expandUserHome, absolutePrefix(rootPath))
378+
c.ToPasswordFile = fixPath(c.ToPasswordFile, expandEnv, expandUserHome, absolutePrefix(rootPath))
379+
c.ToRepositoryFile = fixPath(c.ToRepositoryFile, expandEnv, expandUserHome, absolutePrefix(rootPath))
373380
}
374381

375382
func (s *CopySection) getInitFlags(profile *Profile) *shell.Args {
376383
var init *InitSection
377384

378-
if s.InitializeCopyChunkerParams.IsTrueOrUndefined() {
379-
// Source repo for CopyChunkerParams
380-
init = &InitSection{
381-
CopyChunkerParams: true,
382-
FromKeyHint: profile.KeyHint,
383-
FromRepository: profile.Repository,
384-
FromRepositoryFile: profile.RepositoryFile,
385-
FromPasswordFile: profile.PasswordFile,
386-
FromPasswordCommand: profile.PasswordCommand,
387-
}
388-
init.OtherFlags = profile.OtherFlags
389-
} else {
390-
init = new(InitSection)
391-
}
385+
if s.IsCopyTo() {
386+
if s.InitializeCopyChunkerParams.IsTrueOrUndefined() {
387+
// Source repo for CopyChunkerParams
388+
init = &InitSection{
389+
CopyChunkerParams: true,
390+
FromKeyHint: profile.KeyHint,
391+
FromRepository: profile.Repository,
392+
FromRepositoryFile: profile.RepositoryFile,
393+
FromPasswordFile: profile.PasswordFile,
394+
FromPasswordCommand: profile.PasswordCommand,
395+
}
396+
init.OtherFlags = profile.OtherFlags
397+
} else {
398+
init = new(InitSection)
399+
}
392400

393-
// Repo that should be initialized
394-
ip := *profile
395-
ip.KeyHint = s.KeyHint
396-
ip.Repository = s.Repository
397-
ip.RepositoryFile = s.RepositoryFile
398-
ip.PasswordFile = s.PasswordFile
399-
ip.PasswordCommand = s.PasswordCommand
400-
ip.OtherFlags = s.OtherFlags
401+
// Repo that should be initialized
402+
ip := *profile
403+
ip.KeyHint = s.ToKeyHint
404+
ip.Repository = s.ToRepository
405+
ip.RepositoryFile = s.ToRepositoryFile
406+
ip.PasswordFile = s.ToPasswordFile
407+
ip.PasswordCommand = s.ToPasswordCommand
408+
ip.OtherFlags = s.OtherFlags
409+
return init.getCommandFlags(&ip)
410+
} else {
411+
if s.InitializeCopyChunkerParams.IsTrueOrUndefined() {
412+
// Source repo for CopyChunkerParams
413+
init = &InitSection{
414+
CopyChunkerParams: true,
415+
FromKeyHint: s.FromKeyHint,
416+
FromRepository: s.FromRepository,
417+
FromRepositoryFile: s.FromRepositoryFile,
418+
FromPasswordFile: s.FromPasswordFile,
419+
FromPasswordCommand: s.FromPasswordCommand,
420+
}
421+
init.OtherFlags = profile.OtherFlags
422+
} else {
423+
init = new(InitSection)
424+
}
401425

402-
return init.getCommandFlags(&ip)
426+
// Repo that should be initialized
427+
return init.getCommandFlags(profile)
428+
}
403429
}
404430

405431
func (s *CopySection) getCommandFlags(profile *Profile) (flags *shell.Args) {
406-
repositoryArgs := map[string]string{
407-
constants.ParameterRepository: s.Repository.Value(),
408-
constants.ParameterRepositoryFile: s.RepositoryFile,
409-
constants.ParameterPasswordFile: s.PasswordFile,
410-
constants.ParameterPasswordCommand: s.PasswordCommand,
411-
constants.ParameterKeyHint: s.KeyHint,
412-
}
432+
if s.IsCopyTo() {
433+
repositoryArgs := map[string]string{
434+
constants.ParameterRepository: s.ToRepository.Value(),
435+
constants.ParameterRepositoryFile: s.ToRepositoryFile,
436+
constants.ParameterPasswordFile: s.ToPasswordFile,
437+
constants.ParameterPasswordCommand: s.ToPasswordCommand,
438+
constants.ParameterKeyHint: s.ToKeyHint,
439+
}
413440

414-
// Handle confidential repo in flags
415-
restore := profile.replaceWithRepositoryFile(&s.Repository, &s.RepositoryFile, "-to")
416-
defer restore()
441+
// Handle confidential repo in flags
442+
restore := profile.replaceWithRepositoryFile(&s.ToRepository, &s.ToRepositoryFile, "-to")
443+
defer restore()
417444

418-
flags = profile.GetCommonFlags()
419-
addArgsFromStruct(flags, s)
420-
addArgsFromOtherFlags(flags, profile, s)
445+
flags = profile.GetCommonFlags()
446+
addArgsFromStruct(flags, s)
447+
addArgsFromOtherFlags(flags, profile, s)
421448

422-
if v := profile.resticVersion; v == nil || v.LessThan(resticVersion14) {
423-
// restic < 0.14: repo2, password-file2, etc. is the destination, repo, password-file, etc. the source
424-
for name, value := range repositoryArgs {
425-
if len(value) > 0 {
426-
flags.AddFlag(fmt.Sprintf("%s2", name), shell.NewArg(value, shell.ArgConfigEscape))
449+
if v := profile.resticVersion; v == nil || v.LessThan(resticVersion14) {
450+
// restic < 0.14: repo2, password-file2, etc. is the destination, repo, password-file, etc. the source
451+
for name, value := range repositoryArgs {
452+
if len(value) > 0 {
453+
flags.AddFlag(fmt.Sprintf("%s2", name), shell.NewArg(value, shell.ArgConfigEscape))
454+
}
455+
}
456+
} else {
457+
// restic >= 0.14: from-repo, from-password-file, etc. is the source, repo, password-file, etc. the destination
458+
for name := range maps.Keys(repositoryArgs) {
459+
flags.Rename(name, fmt.Sprintf("from-%s", name))
460+
}
461+
for name, value := range repositoryArgs {
462+
if len(value) > 0 {
463+
flags.AddFlag(name, shell.NewArg(value, shell.ArgConfigEscape))
464+
}
427465
}
428466
}
429467
} else {
430-
// restic >= 0.14: from-repo, from-password-file, etc. is the source, repo, password-file, etc. the destination
431-
for name := range maps.Keys(repositoryArgs) {
432-
flags.Rename(name, fmt.Sprintf("from-%s", name))
468+
legacyArgs := map[string]string{
469+
"from-repo": "repo2",
470+
"from-repository-file": "repository-file2",
471+
"from-password-file": "password-file2",
472+
"from-password-command": "password-command2",
473+
"from-key-hint": "key-hint2",
433474
}
434-
for name, value := range repositoryArgs {
435-
if len(value) > 0 {
436-
flags.AddFlag(name, shell.NewArg(value, shell.ArgConfigEscape))
475+
476+
// Handle confidential repo in flags
477+
restore := profile.replaceWithRepositoryFile(&s.FromRepository, &s.FromRepositoryFile, "-from")
478+
defer restore()
479+
480+
flags = profile.GetCommonFlags()
481+
addArgsFromStruct(flags, s)
482+
addArgsFromOtherFlags(flags, profile, s)
483+
484+
if v := profile.resticVersion; v == nil || v.LessThan(resticVersion14) {
485+
// restic < 0.14: from-repo => repo2, from-password-file => password-file2, etc.
486+
for name, legacyName := range legacyArgs {
487+
flags.Rename(name, legacyName)
437488
}
438489
}
439490
}

config/profile_test.go

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ func TestEnvironmentInProfileRepo(t *testing.T) {
313313
profile.ResolveConfiguration()
314314
assert.Equal(t, repoPath, filepath.ToSlash(profile.Repository.Value()))
315315
assert.Equal(t, repoPath, filepath.ToSlash(profile.Init.FromRepository.Value()))
316-
assert.Equal(t, repoPath, filepath.ToSlash(profile.Copy.Repository.Value()))
316+
assert.Equal(t, repoPath, filepath.ToSlash(profile.Copy.ToRepository.Value()))
317317

318318
profile.SetRootPath("any")
319319
assert.Equal(t, repoPath+".key", filepath.ToSlash(profile.PasswordFile))
@@ -433,7 +433,7 @@ from-password-file = "key"
433433
assert.ElementsMatch(t, []string{"/wd/include-verbatim"}, profile.Backup.FilesFromVerbatim)
434434
assert.ElementsMatch(t, []string{"exclude"}, profile.Backup.Exclude)
435435
assert.ElementsMatch(t, []string{"iexclude"}, profile.Backup.Iexclude)
436-
assert.Equal(t, "/wd/key", profile.Copy.PasswordFile)
436+
assert.Equal(t, "/wd/key", profile.Copy.ToPasswordFile)
437437
assert.Equal(t, []string{"/wd/key"}, profile.OtherSections[constants.CommandDump].OtherFlags["password-file"])
438438
assert.Equal(t, "/wd/key", profile.Init.FromPasswordFile)
439439
assert.Equal(t, "/wd/key", profile.Init.FromRepositoryFile)
@@ -1503,11 +1503,116 @@ func TestGetInitStructFields(t *testing.T) {
15031503

15041504
func TestGetCopyStructFields(t *testing.T) {
15051505
copySection := &CopySection{
1506-
Repository: NewConfidentialValue("dest-repo"),
1507-
RepositoryFile: "dest-repo-file",
1508-
PasswordFile: "dest-pw-file",
1509-
PasswordCommand: "dest-pw-command",
1510-
KeyHint: "dest-key-hint",
1506+
FromRepository: NewConfidentialValue("src-repo"),
1507+
FromRepositoryFile: "src-repo-file",
1508+
FromPasswordFile: "src-pw-file",
1509+
FromPasswordCommand: "src-pw-command",
1510+
FromKeyHint: "src-key-hint",
1511+
}
1512+
1513+
copySection.OtherFlags = map[string]any{"option": "opt=src"}
1514+
1515+
profile := NewProfile(nil, "")
1516+
profile.Repository = NewConfidentialValue("dest-repo")
1517+
profile.RepositoryFile = "dest-repo-file"
1518+
profile.PasswordFile = "dest-pw-file"
1519+
profile.PasswordCommand = "dest-pw-command"
1520+
profile.KeyHint = "dest-key-hint"
1521+
1522+
profile.OtherFlags = map[string]any{"option": "opt=dest"}
1523+
1524+
t.Run("restic<14", func(t *testing.T) {
1525+
require.NoError(t, profile.SetResticVersion(""))
1526+
1527+
// copy
1528+
assert.Equal(t, map[string][]string{
1529+
"key-hint2": {"src-key-hint"},
1530+
"repo2": {"src-repo"},
1531+
"repository-file2": {"src-repo-file"},
1532+
"password-file2": {"src-pw-file"},
1533+
"password-command2": {"src-pw-command"},
1534+
1535+
"option": {"opt=src"}, // TODO: flags should be partitioned (both options are required)
1536+
1537+
"key-hint": {"dest-key-hint"},
1538+
"repo": {"dest-repo"},
1539+
"repository-file": {"dest-repo-file"},
1540+
"password-file": {"dest-pw-file"},
1541+
"password-command": {"dest-pw-command"},
1542+
}, copySection.getCommandFlags(profile).ToMap())
1543+
1544+
// init
1545+
assert.Equal(t, map[string][]string{
1546+
"copy-chunker-params": {},
1547+
"key-hint2": {"src-key-hint"},
1548+
"repo2": {"src-repo"},
1549+
"repository-file2": {"src-repo-file"},
1550+
"password-file2": {"src-pw-file"},
1551+
"password-command2": {"src-pw-command"},
1552+
1553+
"option": {"opt=dest"}, // TODO: flags should be partitioned (both options are required)
1554+
1555+
"key-hint": {"dest-key-hint"},
1556+
"repo": {"dest-repo"},
1557+
"repository-file": {"dest-repo-file"},
1558+
"password-file": {"dest-pw-file"},
1559+
"password-command": {"dest-pw-command"},
1560+
}, copySection.getInitFlags(profile).ToMap())
1561+
})
1562+
1563+
t.Run("restic>=14", func(t *testing.T) {
1564+
require.NoError(t, profile.SetResticVersion(resticVersion14.Original()))
1565+
1566+
// copy
1567+
assert.Equal(t, map[string][]string{
1568+
"from-key-hint": {"src-key-hint"},
1569+
"from-repo": {"src-repo"},
1570+
"from-repository-file": {"src-repo-file"},
1571+
"from-password-file": {"src-pw-file"},
1572+
"from-password-command": {"src-pw-command"},
1573+
1574+
"option": {"opt=src"}, // TODO: flags should be partitioned (both options are required)
1575+
1576+
"key-hint": {"dest-key-hint"},
1577+
"repo": {"dest-repo"},
1578+
"repository-file": {"dest-repo-file"},
1579+
"password-file": {"dest-pw-file"},
1580+
"password-command": {"dest-pw-command"},
1581+
}, copySection.getCommandFlags(profile).ToMap())
1582+
1583+
// init
1584+
assert.Equal(t, map[string][]string{
1585+
"copy-chunker-params": {},
1586+
"from-key-hint": {"src-key-hint"},
1587+
"from-repo": {"src-repo"},
1588+
"from-repository-file": {"src-repo-file"},
1589+
"from-password-file": {"src-pw-file"},
1590+
"from-password-command": {"src-pw-command"},
1591+
1592+
"option": {"opt=dest"}, // TODO: flags should be partitioned (both options are required)
1593+
1594+
"key-hint": {"dest-key-hint"},
1595+
"repo": {"dest-repo"},
1596+
"repository-file": {"dest-repo-file"},
1597+
"password-file": {"dest-pw-file"},
1598+
"password-command": {"dest-pw-command"},
1599+
}, copySection.getInitFlags(profile).ToMap())
1600+
})
1601+
1602+
t.Run("get-init-flags-from-profile", func(t *testing.T) {
1603+
assert.Nil(t, profile.GetCopyInitializeFlags())
1604+
profile.Copy = copySection
1605+
assert.Equal(t, copySection.getInitFlags(profile).GetAll(), profile.GetCopyInitializeFlags().GetAll())
1606+
})
1607+
}
1608+
1609+
func TestGetCopyToStructFields(t *testing.T) {
1610+
copySection := &CopySection{
1611+
ToRepository: NewConfidentialValue("dest-repo"),
1612+
ToRepositoryFile: "dest-repo-file",
1613+
ToPasswordFile: "dest-pw-file",
1614+
ToPasswordCommand: "dest-pw-command",
1615+
ToKeyHint: "dest-key-hint",
15111616
}
15121617

15131618
copySection.OtherFlags = map[string]any{"option": "opt=dest"}

wrapper_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,8 +1630,8 @@ func TestRunInitCopyCommand(t *testing.T) {
16301630
PasswordFile: "password_origin",
16311631
Copy: &config.CopySection{
16321632
InitializeCopyChunkerParams: copyChunkerParams,
1633-
Repository: config.NewConfidentialValue("repo_copy"),
1634-
PasswordFile: "password_copy",
1633+
ToRepository: config.NewConfidentialValue("repo_copy"),
1634+
ToPasswordFile: "password_copy",
16351635
},
16361636
}
16371637
require.NoError(t, p.SetResticVersion(resticVersion))

0 commit comments

Comments
 (0)