diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 79ec6ed9..8ca576f6 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -990,7 +990,16 @@ func (s *BackrestHandler) GeneratePairingToken(ctx context.Context, req *connect func sanitizeRepoFlags(repo *v1.Repo) { for i, flag := range repo.Flags { if strings.HasPrefix(flag, "--option=sftp.args=") { - repo.Flags[i] = strings.ReplaceAll(flag, "-i @", "-i ") + // Remove the "-i @" workaround since we now handle quoting properly + flag = strings.ReplaceAll(flag, "-i @", "-i ") + // Strip double quotes from sftp.args values that cause restic's + // pflag parser to fail with "bare \" in non-quoted-field". + // The UI wraps paths in double quotes (e.g. -i "/path/to/key"), + // but restic's --option flag parser cannot handle them. + // Paths passed as sftp.args don't need quoting since they are + // passed directly to sftp as individual arguments. + flag = strings.ReplaceAll(flag, "\"", "") + repo.Flags[i] = flag } } } diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index 78e42cae..ec7dc971 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -1123,3 +1123,52 @@ func getOperations(t *testing.T, log *oplog.OpLog) []*v1.Operation { } return operations } + +func TestSanitizeRepoFlags(t *testing.T) { + tests := []struct { + name string + flags []string + want []string + }{ + { + name: "strip double quotes from sftp.args", + flags: []string{`--option=sftp.args='-oBatchMode=yes -i "/root/.ssh/id_ed25519" -p 23 -oUserKnownHostsFile="/root/.ssh/known_hosts"'`}, + want: []string{`--option=sftp.args='-oBatchMode=yes -i /root/.ssh/id_ed25519 -p 23 -oUserKnownHostsFile=/root/.ssh/known_hosts'`}, + }, + { + name: "strip double quotes with -i @ workaround", + flags: []string{`--option=sftp.args='-oBatchMode=yes -i @/root/.ssh/id_ed25519"'`}, + want: []string{`--option=sftp.args='-oBatchMode=yes -i /root/.ssh/id_ed25519'`}, + }, + { + name: "no sftp.args flags unchanged", + flags: []string{`--option=some.other=value`}, + want: []string{`--option=some.other=value`}, + }, + { + name: "sftp.args without double quotes unchanged", + flags: []string{`--option=sftp.args=-oBatchMode=yes`}, + want: []string{`--option=sftp.args=-oBatchMode=yes`}, + }, + { + name: "mixed flags only sftp.args modified", + flags: []string{`--option=other=val`, `--option=sftp.args='-i "/path/key"'`}, + want: []string{`--option=other=val`, `--option=sftp.args=-i /path/key`}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &v1.Repo{Flags: tt.flags} + sanitizeRepoFlags(repo) + if len(repo.Flags) != len(tt.want) { + t.Fatalf("got %d flags, want %d", len(repo.Flags), len(tt.want)) + } + for i, got := range repo.Flags { + if got != tt.want[i] { + t.Errorf("flags[%d] = %q, want %q", i, got, tt.want[i]) + } + } + }) + } +}