Skip to content

Commit e5647fc

Browse files
committed
Output: Support extended parsing and redirection
Also adds a status monitor when run in console mode
1 parent 0283615 commit e5647fc

29 files changed

+1340
-264
lines changed

config/flag.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,10 @@ func stringify(value reflect.Value, onlySimplyValues bool) ([]string, bool) {
205205
sort.Strings(flatMap)
206206
return flatMap, len(flatMap) > 0
207207

208-
case reflect.Interface:
208+
case reflect.Interface, reflect.Pointer:
209+
if value.IsNil() {
210+
return emptyStringArray, false
211+
}
209212
return stringify(value.Elem(), onlySimplyValues)
210213

211214
default:

config/flag_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func TestPointerValueShouldReturnErrorMessage(t *testing.T) {
1313
concrete := "test"
1414
value := &concrete
1515
argValue, _ := stringifyValueOf(value)
16-
assert.Equal(t, []string{"ERROR: unexpected type ptr"}, argValue)
16+
assert.Equal(t, []string{"test"}, argValue)
1717
}
1818

1919
func TestNilValueFlag(t *testing.T) {

config/info.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,11 +659,10 @@ func NewProfileInfoForRestic(resticVersion string, withDefaultOptions bool) Prof
659659
// Building initial set including generic sections (from data model)
660660
profileSet := propertySetFromType(infoTypes.profile)
661661
{
662-
genericSection := propertySetFromType(infoTypes.genericSection)
663662
for _, name := range infoTypes.genericSectionNames {
664663
pi := new(propertyInfo)
665664
pi.nested = &namedPropertySet{
666-
propertySet: genericSection,
665+
propertySet: propertySetFromType(infoTypes.genericSection),
667666
name: name,
668667
}
669668
profileSet.properties[name] = pi

config/info_customizer.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,31 @@ func init() {
119119
}
120120
})
121121

122+
// Profile: "stdout-hidden" default values
123+
redirectOutput, noRedirectOutput := &RedirectOutputSection{StdoutFile: []string{""}}, &RedirectOutputSection{}
124+
registerPropertyInfoCustomizer(func(sectionName, propertyName string, property accessibleProperty) {
125+
if propertyName == constants.ParameterStdoutHidden {
126+
property.basic().addDescriptionFilter(func(desc string) string {
127+
var def string
128+
if redirectOutput.IsOutputHidden(sectionName, false) == HideOutput {
129+
def = `Default is "true" when redirected`
130+
} else if noRedirectOutput.IsOutputHidden(sectionName, false) == HideOutput {
131+
def = `Default is "true"`
132+
} else if noRedirectOutput.IsOutputHidden(sectionName, true) == HideOutput {
133+
if sectionName == constants.CommandBackup {
134+
def = `Default is "true" when "extended-status" is set`
135+
} else {
136+
def = `Default is "true" when "json" is requested`
137+
}
138+
}
139+
if def != "" {
140+
desc = fmt.Sprintf("%s. %s", strings.TrimSuffix(strings.TrimSpace(desc), "."), def)
141+
}
142+
return desc
143+
})
144+
}
145+
})
146+
122147
// Profile: exclude "help" (--help flag doesn't need to be in the reference)
123148
ExcludeProfileProperty("*", "help")
124149
}

config/info_customizer_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,37 @@ func TestDeprecatedSection(t *testing.T) {
171171
require.True(t, set.PropertyInfo("schedule").IsDeprecated())
172172
}
173173

174+
func TestStdoutHiddenProperty(t *testing.T) {
175+
var testType = struct {
176+
RedirectOutputSection `mapstructure:",squash"`
177+
}{}
178+
179+
var tests = []struct {
180+
section string
181+
expected string
182+
}{
183+
{section: constants.CommandCat, expected: `Default is "true" when redirected`},
184+
{section: constants.CommandDump, expected: `Default is "true" when redirected`},
185+
{section: constants.CommandCopy, expected: ``},
186+
{section: constants.CommandBackup, expected: ``}, // Default is "true" when "extended-status" is set
187+
}
188+
189+
for i, test := range tests {
190+
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
191+
set := propertySetFromType(reflect.TypeOf(testType))
192+
require.NotNil(t, set.PropertyInfo(constants.ParameterStdoutHidden))
193+
194+
base := set.PropertyInfo(constants.ParameterStdoutHidden).Description()
195+
assert.NotEmpty(t, base)
196+
197+
customizeProperties(test.section, set.properties)
198+
assert.Equal(t,
199+
strings.TrimSuffix(strings.TrimSpace(fmt.Sprintf("%s. %s", base, test.expected)), "."),
200+
set.PropertyInfo(constants.ParameterStdoutHidden).Description())
201+
})
202+
}
203+
}
204+
174205
func TestHelpIsExcluded(t *testing.T) {
175206
assert.True(t, isExcluded("*", "help"))
176207
assert.False(t, isExcluded("*", "any-other"))

config/profile.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ type RunShellCommands interface {
4242
GetRunShellCommands() *RunShellCommandsSection
4343
}
4444

45+
type OutputHidden int
46+
47+
const (
48+
ShowOutput = OutputHidden(iota)
49+
HideJsonOutput
50+
HideOutput
51+
)
52+
53+
// RedirectOutput provides access to output redirection settings
54+
type RedirectOutput interface {
55+
IsOutputHidden(commandName string, jsonRequested bool) OutputHidden
56+
IsOutputRedirected() bool
57+
GetRedirectOutput() *RedirectOutputSection
58+
}
59+
4560
// OtherFlags provides access to dynamic commandline flags
4661
type OtherFlags interface {
4762
GetOtherFlags() map[string]any
@@ -106,6 +121,7 @@ type Profile struct {
106121
type GenericSection struct {
107122
OtherFlagsSection `mapstructure:",squash"`
108123
RunShellCommandsSection `mapstructure:",squash"`
124+
RedirectOutputSection `mapstructure:",squash"`
109125
}
110126

111127
func (g *GenericSection) IsEmpty() bool { return g == nil }
@@ -155,6 +171,7 @@ func (i *InitSection) getCommandFlags(profile *Profile) (flags *shell.Args) {
155171
type BackupSection struct {
156172
SectionWithScheduleAndMonitoring `mapstructure:",squash"`
157173
RunShellCommandsSection `mapstructure:",squash"`
174+
RedirectOutputSection `mapstructure:",squash"`
158175
unresolvedSource []string
159176
CheckBefore bool `mapstructure:"check-before" description:"Check the repository before starting the backup command"`
160177
CheckAfter bool `mapstructure:"check-after" description:"Check the repository after the backup command succeeded"`
@@ -165,7 +182,7 @@ type BackupSection struct {
165182
Iexclude []string `mapstructure:"iexclude" argument:"iexclude" argument-type:"no-glob"`
166183
ExcludeFile []string `mapstructure:"exclude-file" argument:"exclude-file"`
167184
FilesFrom []string `mapstructure:"files-from" argument:"files-from"`
168-
ExtendedStatus bool `mapstructure:"extended-status" argument:"json"`
185+
ExtendedStatus *bool `mapstructure:"extended-status" argument:"json"`
169186
NoErrorOnWarning bool `mapstructure:"no-error-on-warning" description:"Do not fail the backup when some files could not be read"`
170187
}
171188

@@ -176,6 +193,10 @@ func (b *BackupSection) resolve(p *Profile) {
176193
if len(b.StdinCommand) > 0 {
177194
b.UseStdin = true
178195
}
196+
// Enable ExtendedStatus if unset and required for full functionality
197+
if bools.IsUndefined(b.ExtendedStatus) && len(p.StatusFile)+len(p.PrometheusSaveToFile)+len(p.PrometheusPush) != 0 {
198+
b.ExtendedStatus = bools.True()
199+
}
179200
// Resolve source paths
180201
if b.unresolvedSource == nil {
181202
b.unresolvedSource = b.Source
@@ -247,6 +268,7 @@ func (s *ScheduleBaseSection) GetSchedule() *ScheduleBaseSection { return s }
247268
type CopySection struct {
248269
SectionWithScheduleAndMonitoring `mapstructure:",squash"`
249270
RunShellCommandsSection `mapstructure:",squash"`
271+
RedirectOutputSection `mapstructure:",squash"`
250272
Initialize bool `mapstructure:"initialize" description:"Initialize the secondary repository if missing"`
251273
InitializeCopyChunkerParams *bool `mapstructure:"initialize-copy-chunker-params" default:"true" description:"Copy chunker parameters when initializing the secondary repository"`
252274
Repository ConfidentialValue `mapstructure:"repository" description:"Destination repository to copy snapshots to"`
@@ -392,6 +414,37 @@ type SendMonitoringHeader struct {
392414
Value ConfidentialValue `mapstructure:"value" examples:"\"Bearer ...\";\"Basic ...\";\"no-cache\";\"attachment;; filename=stats.txt\";\"application/json\";\"text/plain\";\"text/xml\"" description:"Value of the header"`
393415
}
394416

417+
// RedirectOutputSection contains settings to redirect restic command output
418+
type RedirectOutputSection struct {
419+
StdoutHidden *bool `mapstructure:"stdout-hidden" description:"Toggles whether stdout is hidden in console & log"`
420+
StdoutFile []string `mapstructure:"stdout-file" description:"Redirect restic stdout to file(s)"`
421+
StdoutCommand []string `mapstructure:"stdout-command" description:"Pipe restic stdout to shell command(s)"`
422+
}
423+
424+
func (r *RedirectOutputSection) GetRedirectOutput() *RedirectOutputSection { return r }
425+
426+
func (r *RedirectOutputSection) IsOutputRedirected() bool {
427+
return r != nil &&
428+
(len(r.StdoutFile) > 0 || len(r.StdoutCommand) > 0)
429+
}
430+
431+
func (r *RedirectOutputSection) IsOutputHidden(commandName string, jsonRequested bool) (hidden OutputHidden) {
432+
if r != nil {
433+
if bools.IsTrue(r.StdoutHidden) {
434+
hidden = HideOutput
435+
} else if !bools.IsStrictlyFalse(r.StdoutHidden) {
436+
if commandName == constants.CommandDump || commandName == constants.CommandCat {
437+
if r.IsOutputRedirected() {
438+
hidden = HideOutput
439+
}
440+
} else if commandName == constants.CommandBackup && jsonRequested {
441+
hidden = HideJsonOutput
442+
}
443+
}
444+
}
445+
return
446+
}
447+
395448
// OtherFlagsSection contains additional restic command line flags
396449
type OtherFlagsSection struct {
397450
OtherFlags map[string]any `mapstructure:",remain"`

constants/command.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const (
1010
CommandSnapshots = "snapshots"
1111
CommandUnlock = "unlock"
1212
CommandMount = "mount"
13+
CommandCat = "cat"
1314
CommandCopy = "copy"
1415
CommandDump = "dump"
1516
CommandFind = "find"

constants/parameter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ const (
2222
ParameterPasswordFile = "password-file"
2323
ParameterPasswordCommand = "password-command"
2424
ParameterKeyHint = "key-hint"
25+
ParameterStdoutHidden = "stdout-hidden"
2526
)

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/creativeprojects/resticprofile/config"
1515
"github.com/creativeprojects/resticprofile/constants"
1616
"github.com/creativeprojects/resticprofile/filesearch"
17+
"github.com/creativeprojects/resticprofile/monitor/console"
1718
"github.com/creativeprojects/resticprofile/monitor/prom"
1819
"github.com/creativeprojects/resticprofile/monitor/status"
1920
"github.com/creativeprojects/resticprofile/preventsleep"
@@ -403,6 +404,7 @@ func runProfile(
403404
}
404405

405406
// add progress receivers if necessary
407+
wrapper.addProgress(console.NewProgress(profile))
406408
if profile.StatusFile != "" {
407409
wrapper.addProgress(status.NewProgress(profile, status.NewStatus(profile.StatusFile)))
408410
}

0 commit comments

Comments
 (0)