diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac199c83..7d5a2c01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,9 +42,9 @@ jobs: node-version: "22" - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 with: - version: v1.64.8 + version: v2.5.0 args: --timeout=30m - name: Test diff --git a/.golangci.yml b/.golangci.yml index f7c624f8..c82587b2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,28 +1,89 @@ +version: "2" linters: enable: - asasalint - asciicheck - bidichk - bodyclose + - canonicalheader + - containedctx - contextcheck + - copyloopvar + - decorder + - durationcheck + # - err113 + - errcheck + - errchkjson - errname + # - errorlint + - exptostd + - fatcontext + # - forcetypeassert - gocheckcompilerdirectives + - gochecksumtype + # - gocritic + # - godoclint + - goheader + - gomoddirectives - gosec - # - maintidx + - govet + - grouper + - iface + - importas + - inamedparam + - ineffassign + # - intrange + # - ireturn + - loggercheck + - makezero + - mirror - misspell + - nilerr + - nilnesserr - nilnil - - noctx + # - nlreturn + # - noctx - nolintlint + - nosprintfhostport + # - perfsprint + - prealloc - predeclared + - promlinter - reassign + # - revive + - rowserrcheck - sloglint - spancheck + - staticcheck + - testableexamples + # - testifylint + - thelper - unconvert - unparam + - unused - usestdlibvars - -linters-settings: - gosec: - excludes: - - G204 # Audit the use of command execution - - G404 # Insecure random number source (rand) + # - usetesting + - wastedassign + settings: + gosec: + excludes: + - G204 # Audit use of command execution + - G404 # Insecure random number source (rand) + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.vscode/settings.json b/.vscode/settings.json index 3af104a0..c978fa81 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,6 @@ "go.lintTool": "golangci-lint", "githubPullRequests.ignoredPullRequestBranches": [ "master" - ] + ], + "makefile.configureOnOpen": false } \ No newline at end of file diff --git a/Makefile b/Makefile index 93240909..bc847dc6 100644 --- a/Makefile +++ b/Makefile @@ -94,11 +94,11 @@ $(GOBIN)/github-markdown-toc.go: verify $(GOBIN)/eget $(GOBIN)/mockery: verify $(GOBIN)/eget @echo "[*] $@" - "$(GOBIN)/eget" vektra/mockery --upgrade-only --to '$(GOBIN)' + "$(GOBIN)/eget" vektra/mockery --tag v3.5.5 --upgrade-only --to '$(GOBIN)' $(GOBIN)/golangci-lint: verify $(GOBIN)/eget @echo "[*] $@" - "$(GOBIN)/eget" golangci/golangci-lint --tag v1.64.8 --asset=tar.gz --upgrade-only --to '$(GOBIN)' + "$(GOBIN)/eget" golangci/golangci-lint --tag v2.5.0 --asset=tar.gz --upgrade-only --to '$(GOBIN)' $(GOBIN)/hugo: $(GOBIN)/eget @echo "[*] $@" diff --git a/batt/battery.go b/batt/battery.go index 6c2d612b..786dc549 100644 --- a/batt/battery.go +++ b/batt/battery.go @@ -78,9 +78,10 @@ func detectRunningOnBattery(batteries []battery.Battery) bool { pluggedIn := false discharging := false for _, bat := range batteries { - if bat.State.Raw == battery.Discharging || bat.State.Raw == battery.Empty { + switch bat.State.Raw { + case battery.Discharging, battery.Empty: discharging = true - } else if bat.State.Raw == battery.Charging || bat.State.Raw == battery.Full || bat.State.Raw == battery.Idle || bat.State.Raw == battery.Unknown { + case battery.Charging, battery.Full, battery.Idle, battery.Unknown: pluggedIn = true } } diff --git a/commands_display.go b/commands_display.go index 6c3f9657..15d13f0e 100644 --- a/commands_display.go +++ b/commands_display.go @@ -367,7 +367,7 @@ func newLineLengthWriter(writer io.Writer, maxLineLength int) *lineLengthWriter } func (l *lineLengthWriter) Write(p []byte) (n int, err error) { - written := 0 + var written int inAnsi := false offset := l.lineLength lineLength := func() int { return l.lineLength - l.ansiLength } diff --git a/commands_generate.go b/commands_generate.go index 4b56d83e..c032928c 100644 --- a/commands_generate.go +++ b/commands_generate.go @@ -206,6 +206,7 @@ func generateJsonSchema(output io.Writer, args []string) (err error) { // SectionInfoData is used as data for go templates that render profile section references type SectionInfoData struct { templates.DefaultData + Section config.SectionInfo Weight int } diff --git a/complete_test.go b/complete_test.go index 12354723..5ec906bc 100644 --- a/complete_test.go +++ b/complete_test.go @@ -111,6 +111,7 @@ func TestCompleter(t *testing.T) { testValues := func(flagName string, expected []string) func(t *testing.T) { return func(t *testing.T) { + t.Helper() t.Run("ReturnsAllValues", func(t *testing.T) { actual := completer.Complete(newArgs(fmt.Sprintf("--%s", flagName), "")) diff --git a/config/config.go b/config/config.go index aa7a92f0..02c213af 100644 --- a/config/config.go +++ b/config/config.go @@ -247,7 +247,7 @@ func (c *Config) applyMixinsToProfile(profileName string) error { } func (c *Config) applyMatchingMixinsOnce(matcher func(useKey string) bool) error { - var matchingUses []map[string][]*mixinUse + matchingUses := make([]map[string][]*mixinUse, 0, len(c.mixinUses)) for _, allUses := range c.mixinUses { usesToApply := make(map[string][]*mixinUse) @@ -756,7 +756,7 @@ func traceConfig(profileName, name string, replace bool, config *bytes.Buffer) { gutter = "%4d: " } for i := 0; i < len(lines); i++ { - output.WriteString(fmt.Sprintf(gutter, i+1)) + fmt.Fprintf(output, gutter, i+1) output.Write(lines[i]) output.WriteString("\n") } diff --git a/config/config_test.go b/config/config_test.go index 4ac352ab..7b19bdb9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -201,6 +201,7 @@ profile: func TestBoolPointer(t *testing.T) { fixtures := []struct { testTemplate + continueOnError maybe.Bool }{ { diff --git a/config/group.go b/config/group.go index 91e20123..c0c6306e 100644 --- a/config/group.go +++ b/config/group.go @@ -46,7 +46,7 @@ func (g *Group) Schedules() map[string]*Schedule { } // SchedulableCommands returns the list of commands that can be scheduled (whether they have schedules or not) -func (g Group) SchedulableCommands() []string { +func (g *Group) SchedulableCommands() []string { // once the deprecated retention schedule is removed, we can use the list from profiles // return NewProfile(g.config, "").SchedulableCommands() return []string{ diff --git a/config/info.go b/config/info.go index 29d46baa..4c3ba13d 100644 --- a/config/info.go +++ b/config/info.go @@ -125,6 +125,7 @@ type NumericRange struct { // profileInfo implements ProfileInfo type profileInfo struct { propertySet + sections map[string]SectionInfo } @@ -137,6 +138,7 @@ func (p *profileInfo) Sections() []string { // sectionInfo implements SectionInfo type sectionInfo struct { namedPropertySet + command restic.CommandIf } @@ -172,6 +174,7 @@ func (p *propertySet) IsAllOptions() bool { // namedPropertySet extends propertySet with Named type namedPropertySet struct { propertySet + name, description string } @@ -181,7 +184,7 @@ func (p *namedPropertySet) Description() string { return p.description } // accessibleProperty provides package local access to basicPropertyInfo and the backing struct field (when available) type accessibleProperty interface { // sectionField returns or sets the field that declares the PropertySet that this property is in, or nil if unknown. - sectionField(*reflect.StructField) *reflect.StructField + sectionField(f *reflect.StructField) *reflect.StructField // field returns the field that declares this property, or nil if the property is not based on a field field() *reflect.StructField // basic returns the mutable basicPropertyInfo (is never nil) @@ -278,6 +281,7 @@ func (b *basicPropertyInfo) resetTypeInfo() *basicPropertyInfo { // propertyInfo implements PropertyInfo type propertyInfo struct { basicPropertyInfo + description string defaults []string originField *reflect.StructField @@ -302,6 +306,7 @@ func (p *propertyInfo) sectionField(f *reflect.StructField) *reflect.StructField // resticPropertyInfo implements PropertyInfo for properties derived from restic.Option type resticPropertyInfo struct { basicPropertyInfo + originField *reflect.StructField optionFlag restic.Option skipDefaultValue bool diff --git a/config/info_customizer.go b/config/info_customizer.go index 2fd61c44..3f34453d 100644 --- a/config/info_customizer.go +++ b/config/info_customizer.go @@ -82,14 +82,15 @@ func init() { note = fmt.Sprintf(`Boolean true is replaced with the %ss from section "backup".`, propertyName) } - if sectionName == constants.CommandBackup { + switch sectionName { + case constants.CommandBackup: if propertyName != constants.ParameterHost { info.examples = info.examples[1:] // remove "true" from examples of backup section note = `Boolean true is unsupported in section "backup".` } else { note += suffixDefaultTrueV2 } - } else if sectionName == constants.SectionConfigurationRetention { + case constants.SectionConfigurationRetention: if propertyName == constants.ParameterHost { note = `Boolean true is replaced with the hostname that applies in section "backup".` } diff --git a/config/jsonschema/model.go b/config/jsonschema/model.go index 52cefe32..55b0d9d4 100644 --- a/config/jsonschema/model.go +++ b/config/jsonschema/model.go @@ -213,6 +213,7 @@ func (s *schemaTypeWithoutBase) describe(title, description string) { type schemaTypeList struct { schemaTypeWithoutBase + OneOf []SchemaType `json:"oneOf,omitempty"` AnyOf []SchemaType `json:"anyOf,omitempty"` } @@ -256,6 +257,7 @@ func newSchemaTypeList(anyType bool, types ...SchemaType) *schemaTypeList { type schemaReference struct { schemaTypeWithoutBase + Ref string `json:"$ref"` } @@ -265,6 +267,7 @@ func newSchemaBool() *schemaTypeBase { type schemaObject struct { schemaTypeBase + AdditionalProperties any `json:"additionalProperties,omitempty"` PatternProperties map[string]SchemaType `json:"patternProperties,omitempty"` Properties map[string]SchemaType `json:"properties,omitempty"` @@ -315,6 +318,7 @@ func (s *schemaObject) verify() (err error) { type schemaArray struct { schemaTypeBase + Items SchemaType `json:"items"` MinItems uint64 `json:"minItems"` MaxItems *uint64 `json:"maxItems,omitempty"` @@ -341,6 +345,7 @@ func (a *schemaArray) verify() (err error) { type schemaNumber struct { schemaTypeBase + MultipleOf *float64 `json:"multipleOf,omitempty"` Minimum *float64 `json:"minimum,omitempty"` Maximum *float64 `json:"maximum,omitempty"` @@ -380,6 +385,7 @@ var validFormatNames = []stringFormat{ type schemaString struct { schemaTypeBase + MinLength uint64 `json:"minLength"` MaxLength *uint64 `json:"maxLength,omitempty"` ContentEncoding string `json:"contentEncoding,omitempty"` diff --git a/config/jsonschema/schema_test.go b/config/jsonschema/schema_test.go index 4e428405..61d0ae78 100644 --- a/config/jsonschema/schema_test.go +++ b/config/jsonschema/schema_test.go @@ -4,6 +4,7 @@ package jsonschema import ( "bytes" + "context" "fmt" "io/fs" "maps" @@ -52,7 +53,10 @@ func npmRunner(t *testing.T) npmRunnerFunc { t.Helper() t.Log(args) if npmExecutable != "" { - cmd := exec.Command(npmExecutable, args...) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + cmd := exec.CommandContext(ctx, npmExecutable, args...) cmd.Dir = path.Join(".", ".node-env") _ = os.MkdirAll(cmd.Dir, 0755) diff --git a/config/profile.go b/config/profile.go index 533545bd..e7e9a39f 100644 --- a/config/profile.go +++ b/config/profile.go @@ -67,43 +67,44 @@ type resolver interface { type Profile struct { RunShellCommandsSection `mapstructure:",squash"` OtherFlagsSection `mapstructure:",squash"` - config *Config - resticVersion *semver.Version - Name string - Description string `mapstructure:"description" description:"Describes the profile"` - BaseDir string `mapstructure:"base-dir" description:"Sets the working directory for this profile. The profile will fail when the working directory cannot be changed. Leave empty to use the current directory instead"` - Quiet bool `mapstructure:"quiet" argument:"quiet"` - Verbose int `mapstructure:"verbose" argument:"verbose"` - KeyHint string `mapstructure:"key-hint" argument:"key-hint"` - Repository ConfidentialValue `mapstructure:"repository" argument:"repo"` - RepositoryFile string `mapstructure:"repository-file" argument:"repository-file"` - PasswordFile string `mapstructure:"password-file" argument:"password-file"` - PasswordCommand string `mapstructure:"password-command" argument:"password-command"` - CacheDir string `mapstructure:"cache-dir" argument:"cache-dir"` - CACert string `mapstructure:"cacert" argument:"cacert"` - TLSClientCert string `mapstructure:"tls-client-cert" argument:"tls-client-cert"` - Initialize bool `mapstructure:"initialize" default:"" description:"Initialize the restic repository if missing"` - Inherit string `mapstructure:"inherit" show:"noshow" description:"Name of the profile to inherit all of the settings from"` - Lock string `mapstructure:"lock" description:"Path to the lock file to use with resticprofile locks"` - ForceLock bool `mapstructure:"force-inactive-lock" description:"Allows to lock when the existing lock is considered stale"` - StreamError []StreamErrorSection `mapstructure:"stream-error" description:"Run shell command(s) when a pattern matches the stderr of restic"` - StatusFile string `mapstructure:"status-file" description:"Path to the status file to update with a summary of last restic command result"` - PrometheusSaveToFile string `mapstructure:"prometheus-save-to-file" description:"Path to the prometheus metrics file to update with a summary of the last restic command result"` - PrometheusPush string `mapstructure:"prometheus-push" format:"uri" description:"URL of the prometheus push gateway to send the summary of the last restic command result to"` - PrometheusPushJob string `mapstructure:"prometheus-push-job" description:"Prometheus push gateway job name. $command placeholder is replaced with restic command"` - PrometheusPushFormat string `mapstructure:"prometheus-push-format" default:"text" enum:"text;protobuf" description:"Prometheus push gateway request format"` - PrometheusLabels map[string]string `mapstructure:"prometheus-labels" description:"Additional prometheus labels to set"` - SystemdDropInFiles []string `mapstructure:"systemd-drop-in-files" default:"" description:"Files containing systemd drop-in (override) files - see https://creativeprojects.github.io/resticprofile/schedules/systemd/"` - Environment map[string]ConfidentialValue `mapstructure:"env" description:"Additional environment variables to set in any child process. Inline env variables take precedence over dotenv files declared with \"env-file\"."` - EnvironmentFiles []string `mapstructure:"env-file" description:"Additional dotenv files to load and set as environment in any child process"` - Init *InitSection `mapstructure:"init"` - Backup *BackupSection `mapstructure:"backup"` - Retention *RetentionSection `mapstructure:"retention" command:"forget"` - Check *GenericSectionWithSchedule `mapstructure:"check"` - Prune *GenericSectionWithSchedule `mapstructure:"prune"` - Forget *GenericSectionWithSchedule `mapstructure:"forget"` - Copy *CopySection `mapstructure:"copy"` - OtherSections map[string]*GenericSection `show:",remain"` + + config *Config + resticVersion *semver.Version + Name string + Description string `mapstructure:"description" description:"Describes the profile"` + BaseDir string `mapstructure:"base-dir" description:"Sets the working directory for this profile. The profile will fail when the working directory cannot be changed. Leave empty to use the current directory instead"` + Quiet bool `mapstructure:"quiet" argument:"quiet"` + Verbose int `mapstructure:"verbose" argument:"verbose"` + KeyHint string `mapstructure:"key-hint" argument:"key-hint"` + Repository ConfidentialValue `mapstructure:"repository" argument:"repo"` + RepositoryFile string `mapstructure:"repository-file" argument:"repository-file"` + PasswordFile string `mapstructure:"password-file" argument:"password-file"` + PasswordCommand string `mapstructure:"password-command" argument:"password-command"` + CacheDir string `mapstructure:"cache-dir" argument:"cache-dir"` + CACert string `mapstructure:"cacert" argument:"cacert"` + TLSClientCert string `mapstructure:"tls-client-cert" argument:"tls-client-cert"` + Initialize bool `mapstructure:"initialize" default:"" description:"Initialize the restic repository if missing"` + Inherit string `mapstructure:"inherit" show:"noshow" description:"Name of the profile to inherit all of the settings from"` + Lock string `mapstructure:"lock" description:"Path to the lock file to use with resticprofile locks"` + ForceLock bool `mapstructure:"force-inactive-lock" description:"Allows to lock when the existing lock is considered stale"` + StreamError []StreamErrorSection `mapstructure:"stream-error" description:"Run shell command(s) when a pattern matches the stderr of restic"` + StatusFile string `mapstructure:"status-file" description:"Path to the status file to update with a summary of last restic command result"` + PrometheusSaveToFile string `mapstructure:"prometheus-save-to-file" description:"Path to the prometheus metrics file to update with a summary of the last restic command result"` + PrometheusPush string `mapstructure:"prometheus-push" format:"uri" description:"URL of the prometheus push gateway to send the summary of the last restic command result to"` + PrometheusPushJob string `mapstructure:"prometheus-push-job" description:"Prometheus push gateway job name. $command placeholder is replaced with restic command"` + PrometheusPushFormat string `mapstructure:"prometheus-push-format" default:"text" enum:"text;protobuf" description:"Prometheus push gateway request format"` + PrometheusLabels map[string]string `mapstructure:"prometheus-labels" description:"Additional prometheus labels to set"` + SystemdDropInFiles []string `mapstructure:"systemd-drop-in-files" default:"" description:"Files containing systemd drop-in (override) files - see https://creativeprojects.github.io/resticprofile/schedules/systemd/"` + Environment map[string]ConfidentialValue `mapstructure:"env" description:"Additional environment variables to set in any child process. Inline env variables take precedence over dotenv files declared with \"env-file\"."` + EnvironmentFiles []string `mapstructure:"env-file" description:"Additional dotenv files to load and set as environment in any child process"` + Init *InitSection `mapstructure:"init"` + Backup *BackupSection `mapstructure:"backup"` + Retention *RetentionSection `mapstructure:"retention" command:"forget"` + Check *GenericSectionWithSchedule `mapstructure:"check"` + Prune *GenericSectionWithSchedule `mapstructure:"prune"` + Forget *GenericSectionWithSchedule `mapstructure:"forget"` + Copy *CopySection `mapstructure:"copy"` + OtherSections map[string]*GenericSection `show:",remain"` } // GenericSection is used for all restic commands that are not covered in specific section types @@ -121,7 +122,8 @@ func (g *GenericSection) IsEmpty() bool { return g == nil } // InitSection contains the specific configuration to the 'init' command type InitSection struct { - GenericSection `mapstructure:",squash"` + GenericSection `mapstructure:",squash"` + CopyChunkerParams bool `mapstructure:"copy-chunker-params" argument:"copy-chunker-params"` FromKeyHint string `mapstructure:"from-key-hint" argument:"from-key-hint"` FromRepository ConfidentialValue `mapstructure:"from-repository" argument:"from-repo"` @@ -172,23 +174,24 @@ func (i *InitSection) getCommandFlags(profile *Profile) (flags *shell.Args) { // BackupSection contains the specific configuration to the 'backup' command type BackupSection struct { GenericSectionWithSchedule `mapstructure:",squash"` - unresolvedSource []string - CheckBefore bool `mapstructure:"check-before" description:"Check the repository before starting the backup command"` - CheckAfter bool `mapstructure:"check-after" description:"Check the repository after the backup command succeeded"` - UseStdin bool `mapstructure:"stdin" argument:"stdin"` - StdinCommand []string `mapstructure:"stdin-command" description:"Shell command(s) that generate content to redirect into the stdin of restic. When set, the flag \"stdin\" is always set to \"true\"."` - SourceRelative bool `mapstructure:"source-relative" description:"Enable backup with relative source paths. This will change the working directory of the \"restic backup\" command to \"source-base\", and will not expand \"source\" to an absolute path."` - SourceBase string `mapstructure:"source-base" examples:"/;$PWD;C:\\;%cd%" description:"The base path to resolve relative backup paths against. Defaults to current directory if unset or empty (see also \"base-dir\" in profile)"` - Source []string `mapstructure:"source" examples:"/opt/;/home/user/;C:\\Users\\User\\Documents" description:"The paths to backup"` - Exclude []string `mapstructure:"exclude" argument:"exclude" argument-type:"no-glob"` - Iexclude []string `mapstructure:"iexclude" argument:"iexclude" argument-type:"no-glob"` - ExcludeFile []string `mapstructure:"exclude-file" argument:"exclude-file"` - IexcludeFile []string `mapstructure:"iexclude-file" argument:"iexclude-file"` - FilesFrom []string `mapstructure:"files-from" argument:"files-from"` - FilesFromRaw []string `mapstructure:"files-from-raw" argument:"files-from-raw"` - FilesFromVerbatim []string `mapstructure:"files-from-verbatim" argument:"files-from-verbatim"` - ExtendedStatus bool `mapstructure:"extended-status" argument:"json"` - NoErrorOnWarning bool `mapstructure:"no-error-on-warning" description:"Do not fail the backup when some files could not be read"` + + unresolvedSource []string + CheckBefore bool `mapstructure:"check-before" description:"Check the repository before starting the backup command"` + CheckAfter bool `mapstructure:"check-after" description:"Check the repository after the backup command succeeded"` + UseStdin bool `mapstructure:"stdin" argument:"stdin"` + StdinCommand []string `mapstructure:"stdin-command" description:"Shell command(s) that generate content to redirect into the stdin of restic. When set, the flag \"stdin\" is always set to \"true\"."` + SourceRelative bool `mapstructure:"source-relative" description:"Enable backup with relative source paths. This will change the working directory of the \"restic backup\" command to \"source-base\", and will not expand \"source\" to an absolute path."` + SourceBase string `mapstructure:"source-base" examples:"/;$PWD;C:\\;%cd%" description:"The base path to resolve relative backup paths against. Defaults to current directory if unset or empty (see also \"base-dir\" in profile)"` + Source []string `mapstructure:"source" examples:"/opt/;/home/user/;C:\\Users\\User\\Documents" description:"The paths to backup"` + Exclude []string `mapstructure:"exclude" argument:"exclude" argument-type:"no-glob"` + Iexclude []string `mapstructure:"iexclude" argument:"iexclude" argument-type:"no-glob"` + ExcludeFile []string `mapstructure:"exclude-file" argument:"exclude-file"` + IexcludeFile []string `mapstructure:"iexclude-file" argument:"iexclude-file"` + FilesFrom []string `mapstructure:"files-from" argument:"files-from"` + FilesFromRaw []string `mapstructure:"files-from-raw" argument:"files-from-raw"` + FilesFromVerbatim []string `mapstructure:"files-from-verbatim" argument:"files-from-verbatim"` + ExtendedStatus bool `mapstructure:"extended-status" argument:"json"` + NoErrorOnWarning bool `mapstructure:"no-error-on-warning" description:"Do not fail the backup when some files could not be read"` } func (s *BackupSection) IsEmpty() bool { return s == nil } @@ -241,8 +244,9 @@ func (s *BackupSection) setRootPath(p *Profile, rootPath string) { type RetentionSection struct { ScheduleBaseSection `mapstructure:",squash" deprecated:"0.11.0"` OtherFlagsSection `mapstructure:",squash"` - BeforeBackup maybe.Bool `mapstructure:"before-backup" description:"Apply retention before starting the backup command"` - AfterBackup maybe.Bool `mapstructure:"after-backup" description:"Apply retention after the backup command succeeded. Defaults to true in configuration format v2 if any \"keep-*\" flag is set and \"before-backup\" is unset"` + + BeforeBackup maybe.Bool `mapstructure:"before-backup" description:"Apply retention before starting the backup command"` + AfterBackup maybe.Bool `mapstructure:"after-backup" description:"Apply retention after the backup command succeeded. Defaults to true in configuration format v2 if any \"keep-*\" flag is set and \"before-backup\" is unset"` } func (r *RetentionSection) IsEmpty() bool { return r == nil } @@ -348,7 +352,8 @@ func (s *ScheduleBaseSection) getScheduleConfig(p *Profile, command string) *Sch // CopySection contains the source or destination parameters for a copy command type CopySection struct { - GenericSectionWithSchedule `mapstructure:",squash"` + GenericSectionWithSchedule `mapstructure:",squash"` + Initialize bool `mapstructure:"initialize" description:"Initialize the secondary repository if missing"` InitializeCopyChunkerParams maybe.Bool `mapstructure:"initialize-copy-chunker-params" default:"true" description:"Copy chunker parameters when initializing the secondary repository"` FromRepository ConfidentialValue `mapstructure:"from-repository" argument:"from-repo" description:"Source repository to copy snapshots from"` diff --git a/config/schedule.go b/config/schedule.go index be8ede51..65cfc3fe 100644 --- a/config/schedule.go +++ b/config/schedule.go @@ -168,9 +168,10 @@ func ScheduleOrigin(name, command string, kind ...ScheduleOriginType) (s Schedul // ScheduleConfig is the user configuration of a specific schedule bound to a command in a profile or group. type ScheduleConfig struct { - normalized bool - origin ScheduleConfigOrigin `show:"noshow"` - Schedules []string `mapstructure:"at" examples:"hourly;daily;weekly;monthly;10:00,14:00,18:00,22:00;Wed,Fri 17:48;*-*-15 02:45;Mon..Fri 00:30" description:"Set the times at which the scheduled command is run (times are specified in systemd timer format)"` + normalized bool + origin ScheduleConfigOrigin `show:"noshow"` + Schedules []string `mapstructure:"at" examples:"hourly;daily;weekly;monthly;10:00,14:00,18:00,22:00;Wed,Fri 17:48;*-*-15 02:45;Mon..Fri 00:30" description:"Set the times at which the scheduled command is run (times are specified in systemd timer format)"` + ScheduleBaseConfig `mapstructure:",squash"` } @@ -268,6 +269,7 @@ type Schedulable interface { // Schedule is the configuration used in profiles and groups for passing the user config to the scheduler system. type Schedule struct { ScheduleConfig + ConfigFile string `show:"noshow"` Environment []string `show:"noshow"` Flags map[string]string `show:"noshow"` diff --git a/config/show_test.go b/config/show_test.go index 86b354f1..9d23efbb 100644 --- a/config/show_test.go +++ b/config/show_test.go @@ -47,7 +47,8 @@ type testPointer struct { type testEmbedded struct { EmbeddedStruct `mapstructure:",squash"` - InlineValue int `mapstructure:"inline"` + + InlineValue int `mapstructure:"inline"` } type EmbeddedStruct struct { diff --git a/config/template.go b/config/template.go index af81fe97..8c46d030 100644 --- a/config/template.go +++ b/config/template.go @@ -14,6 +14,7 @@ import ( // TemplateData contain the variables fed to a config template type TemplateData struct { templates.DefaultData + Profile ProfileTemplateData Schedule ScheduleTemplateData ConfigDir string @@ -53,6 +54,7 @@ func newTemplateData(configFile, profileName, scheduleName string) TemplateData // TemplateInfoData is used as data for go templates that render config references type TemplateInfoData struct { templates.DefaultData + Global, Group PropertySet Profile ProfileInfo KnownResticVersions []string diff --git a/constants/global.go b/constants/global.go index 2ce3703e..ce6c0a8d 100644 --- a/constants/global.go +++ b/constants/global.go @@ -2,8 +2,6 @@ package constants import ( "time" - - "github.com/creativeprojects/resticprofile/priority" ) // Scheduler type @@ -16,18 +14,6 @@ const ( SchedulerOSDefault = "" ) -var ( - // PriorityValues is the map between the name and the value - PriorityValues = map[string]int{ - "idle": priority.Idle, - "background": priority.Background, - "low": priority.Low, - "normal": priority.Normal, - "high": priority.High, - "highest": priority.Highest, - } -) - // Limits for restic lock handling (stale locks and retry on lock failure) const ( MinResticLockRetryDelay = 15 * time.Second diff --git a/constants/testing.go b/constants/testing.go new file mode 100644 index 00000000..699209e8 --- /dev/null +++ b/constants/testing.go @@ -0,0 +1,7 @@ +package constants + +import "time" + +const ( + DefaultTestTimeout = 2 * time.Minute +) diff --git a/crond/crontab_test.go b/crond/crontab_test.go index 4dfac189..190f5b51 100644 --- a/crond/crontab_test.go +++ b/crond/crontab_test.go @@ -3,6 +3,7 @@ package crond import ( + "context" "fmt" "os" "os/exec" @@ -12,6 +13,7 @@ import ( "testing" "github.com/creativeprojects/resticprofile/calendar" + "github.com/creativeprojects/resticprofile/constants" "github.com/creativeprojects/resticprofile/platform" "github.com/spf13/afero" "github.com/stretchr/testify/assert" @@ -308,10 +310,13 @@ PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin } func TestUseCrontabBinary(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultTestTimeout) + defer cancel() + binary := filepath.Join(t.TempDir(), platform.Executable("crontab")) defer func() { _ = os.Remove(binary) }() - cmd := exec.Command("go", "build", "-buildvcs=false", "-o", binary, "./mock") + cmd := exec.CommandContext(ctx, "go", "build", "-buildvcs=false", "-o", binary, "./mock") require.NoError(t, cmd.Run()) crontab := NewCrontab(nil) diff --git a/filesearch/filesearch.go b/filesearch/filesearch.go index b4ec9486..7dbd7ed2 100644 --- a/filesearch/filesearch.go +++ b/filesearch/filesearch.go @@ -80,9 +80,10 @@ func NewFinder() Finder { // FindConfigurationFile returns the path of the configuration file // If the file doesn't have an extension, it will search for all possible extensions func (f Finder) FindConfigurationFile(configFile string) (string, error) { - found := "" + var found, displayFile string + extension := filepath.Ext(configFile) - displayFile := "" + if extension != "" { displayFile = fmt.Sprintf("'%s'", configFile) // Search only once through the paths diff --git a/integration_test.go b/integration_test.go index 9eb38254..5e8aea48 100644 --- a/integration_test.go +++ b/integration_test.go @@ -23,7 +23,7 @@ func TestFromConfigFileToCommandLine(t *testing.T) { // we can use the same files to test a glob pattern globFiles := "\"" + strings.Join(files, "\" \"") + "\"" - globFilesOnWindows := strings.Replace(globFiles, `\`, `\\`, -1) + globFilesOnWindows := strings.ReplaceAll(globFiles, `\`, `\\`) integrationData := []struct { profileName string diff --git a/lock/lock.go b/lock/lock.go index da600ba6..2805aca0 100644 --- a/lock/lock.go +++ b/lock/lock.go @@ -88,7 +88,7 @@ func (l *Lock) SetPID(pid int32) { return } // just add the PID on a newline - _, _ = l.file.WriteString(fmt.Sprintf("\n%d", pid)) + _, _ = fmt.Fprintf(l.file, "\n%d", pid) } // HasLocked check this instance (and only this one) has locked the file @@ -142,7 +142,7 @@ func (l *Lock) lock() bool { now := time.Now().Format(time.RFC850) // No error checking... it's not a big deal if we cannot write that - _, _ = l.file.WriteString(fmt.Sprintf("%s on %s from %s", username, now, hostname)) + _, _ = fmt.Fprintf(l.file, "%s on %s from %s", username, now, hostname) return true } diff --git a/lock/lock_test.go b/lock/lock_test.go index 47a25599..43ada7c4 100644 --- a/lock/lock_test.go +++ b/lock/lock_test.go @@ -2,6 +2,7 @@ package lock import ( "bytes" + "context" "fmt" "os" "os/exec" @@ -12,6 +13,7 @@ import ( "testing" "time" + "github.com/creativeprojects/resticprofile/constants" "github.com/creativeprojects/resticprofile/platform" "github.com/creativeprojects/resticprofile/shell" "github.com/shirou/gopsutil/v3/process" @@ -26,6 +28,9 @@ var ( func TestMain(m *testing.M) { // using an anonymous function to handle defer statements before os.Exit() exitCode := func() int { + ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultTestTimeout) + defer cancel() + tempDir, err := os.MkdirTemp("", "resticprofile-lock") if err != nil { fmt.Fprintf(os.Stderr, "cannot create temp dir: %v\n", err) @@ -36,7 +41,7 @@ func TestMain(m *testing.M) { helperBinary = filepath.Join(tempDir, platform.Executable("locktest")) - cmd := exec.Command("go", "build", "-buildvcs=false", "-o", helperBinary, "./test") + cmd := exec.CommandContext(ctx, "go", "build", "-buildvcs=false", "-o", helperBinary, "./test") if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error building helper binary: %s\n", err) return 1 diff --git a/main.go b/main.go index 40b91b74..d778a53e 100644 --- a/main.go +++ b/main.go @@ -305,7 +305,7 @@ func setPriority(nice int, class string) error { var err error if class != "" { - if classID, ok := constants.PriorityValues[strings.ToLower(class)]; ok { + if classID, ok := priority.Values[strings.ToLower(class)]; ok { err = priority.SetClass(classID) if err != nil { return err diff --git a/main_test.go b/main_test.go index 84e5b997..a21f538f 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" "os/exec" @@ -23,6 +24,9 @@ var ( func TestMain(m *testing.M) { // using an anonymous function to handle defer statements before os.Exit() exitCode := func() int { + ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultTestTimeout) + defer cancel() + tempDir, err := os.MkdirTemp("", "resticprofile-main") if err != nil { fmt.Fprintf(os.Stderr, "cannot create temp dir: %v\n", err) @@ -35,7 +39,7 @@ func TestMain(m *testing.M) { if platform.IsWindows() { mockBinary += ".exe" } - cmdMock := exec.Command("go", "build", "-buildvcs=false", "-o", mockBinary, "./shell/mock") + cmdMock := exec.CommandContext(ctx, "go", "build", "-buildvcs=false", "-o", mockBinary, "./shell/mock") if output, err := cmdMock.CombinedOutput(); err != nil { fmt.Fprintf(os.Stderr, "Error building shell/mock binary: %s\nCommand output: %s\n", err, string(output)) return 1 @@ -45,7 +49,7 @@ func TestMain(m *testing.M) { if platform.IsWindows() { echoBinary += ".exe" } - cmdEcho := exec.Command("go", "build", "-buildvcs=false", "-o", echoBinary, "./shell/echo") + cmdEcho := exec.CommandContext(ctx, "go", "build", "-buildvcs=false", "-o", echoBinary, "./shell/echo") if output, err := cmdEcho.CombinedOutput(); err != nil { fmt.Fprintf(os.Stderr, "Error building shell/echo binary: %s\nCommand output: %s\n", err, string(output)) return 1 diff --git a/monitor/hook/context.go b/monitor/hook/context.go index 51e3353e..45048ead 100644 --- a/monitor/hook/context.go +++ b/monitor/hook/context.go @@ -4,6 +4,7 @@ import "github.com/creativeprojects/resticprofile/util/templates" type Context struct { templates.DefaultData + ProfileName string ProfileCommand string Error ErrorContext diff --git a/monitor/status/profile.go b/monitor/status/profile.go index 24dd66bd..b0c0c7f5 100644 --- a/monitor/status/profile.go +++ b/monitor/status/profile.go @@ -30,6 +30,7 @@ type CommandStatus struct { // BackupStatus contains the last backup status type BackupStatus struct { CommandStatus + FilesNew int `json:"files_new"` FilesChanged int `json:"files_changed"` FilesUnmodified int `json:"files_unmodified"` diff --git a/own_command_error_test.go b/own_command_error_test.go index 6d87957a..22d6264c 100644 --- a/own_command_error_test.go +++ b/own_command_error_test.go @@ -8,7 +8,7 @@ import ( ) func TestOwnCommandError(t *testing.T) { - var wrap error = errors.New("wrap") + wrap := errors.New("wrap") var err error = newOwnCommandError(wrap, 10) assert.Equal(t, "wrap", err.Error()) diff --git a/own_commands.go b/own_commands.go index ff23beae..de565a9f 100644 --- a/own_commands.go +++ b/own_commands.go @@ -10,6 +10,7 @@ import ( // commandContext is the context for running a command. type commandContext struct { Context + ownCommands *OwnCommands } diff --git a/priority/priority.go b/priority/priority.go index 425394f2..a5de7d33 100644 --- a/priority/priority.go +++ b/priority/priority.go @@ -10,3 +10,15 @@ const ( High = -10 Highest = -20 ) + +var ( + // Values is the map between the name and the value + Values = map[string]int{ + "idle": Idle, + "background": Background, + "low": Low, + "normal": Normal, + "high": High, + "highest": Highest, + } +) diff --git a/priority/prority_test.go b/priority/prority_test.go index c241679f..82e603c9 100644 --- a/priority/prority_test.go +++ b/priority/prority_test.go @@ -2,11 +2,13 @@ package priority import ( "bytes" + "context" "io" "os/exec" "testing" "time" + "github.com/creativeprojects/resticprofile/constants" "github.com/creativeprojects/resticprofile/platform" "github.com/stretchr/testify/assert" ) @@ -71,7 +73,10 @@ func TestStartProcessWithPriority(t *testing.T) { } func runChildProcess() (string, error) { - cmd := exec.Command("go", "run", "./check") + ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultTestTimeout) + defer cancel() + + cmd := exec.CommandContext(ctx, "go", "run", "./check") buffer := &bytes.Buffer{} cmd.Stdout = buffer cmd.Stderr = buffer diff --git a/restic/commands.go b/restic/commands.go index 66290ea1..b622ff30 100644 --- a/restic/commands.go +++ b/restic/commands.go @@ -96,6 +96,7 @@ func (c *command) sortOptions() { type commandAtVersion struct { command + includeRemoved bool actualVersion *semver.Version } diff --git a/restic/downloader_test.go b/restic/downloader_test.go index 0cb38240..eab923e5 100644 --- a/restic/downloader_test.go +++ b/restic/downloader_test.go @@ -24,7 +24,6 @@ func TestDownloadBinary(t *testing.T) { } for _, version := range versions { - version := version t.Run(version, func(t *testing.T) { t.Parallel() executable := platform.Executable(filepath.Join(t.TempDir(), "restic")) diff --git a/restic/generator/main.go b/restic/generator/main.go index cb17669f..6b97fac4 100644 --- a/restic/generator/main.go +++ b/restic/generator/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "fmt" "log" @@ -9,6 +10,7 @@ import ( "path" "path/filepath" "strconv" + "time" "github.com/creativeprojects/resticprofile/restic" ) @@ -60,6 +62,9 @@ func generate() (err error) { } func install(includeManual bool) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + executable := path.Join(installDir, restic.Executable) // Install if required @@ -90,7 +95,7 @@ func install(includeManual bool) error { manPage := path.Join(manualDir, "restic.1") if _, err := os.Stat(manPage); os.IsNotExist(err) { fmt.Println("creating man pages: " + manualDir) - cmd := exec.Command(executable, "generate", "--man", manualDir) + cmd := exec.CommandContext(ctx, executable, "generate", "--man", manualDir) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err = cmd.Run(); err != nil { diff --git a/schedule/handler_systemd.go b/schedule/handler_systemd.go index 68327a3e..0c352e1e 100644 --- a/schedule/handler_systemd.go +++ b/schedule/handler_systemd.go @@ -113,7 +113,7 @@ func (h *HandlerSystemd) DisplayStatus(profileName string) error { } if err != nil || status == "" || strings.Contains(status, "0 timers") { // fail silently - return nil + return nil //nolint:nilerr } fmt.Fprintf(term.GetOutput(), "\nTimers summary\n===============\n%s\n", status) return nil @@ -142,11 +142,11 @@ func (h *HandlerSystemd) CreateJob(job *Config, schedules []*calendar.Event, per clog.Infof("removing existing unit with different permission") err := h.disableJob(job, otherUnitType, timerFile) if err != nil { - return fmt.Errorf("cannot stop or disable existing unit before scheduling with different permission. You might want to retry using sudo.") + return fmt.Errorf("cannot stop or disable existing unit before scheduling with different permission, you might want to retry using sudo") } err = h.removeJobFiles(job, otherUnitType, timerFile, systemd.GetServiceFile(job.ProfileName, job.CommandName)) if err != nil { - return fmt.Errorf("cannot remove existing unit before scheduling with different permission. You might want to retry using sudo.") + return fmt.Errorf("cannot remove existing unit before scheduling with different permission, you might want to retry using sudo") } } } @@ -250,7 +250,7 @@ func (h *HandlerSystemd) removeJobFiles(job *Config, unitType systemd.UnitType, if unitType == systemd.UserUnit { systemdPath, err = unit.GetUserDir() if err != nil { - return nil + return nil //nolint:nilerr } } diff --git a/schedule/handler_windows.go b/schedule/handler_windows.go index be3fa282..363fdf27 100644 --- a/schedule/handler_windows.go +++ b/schedule/handler_windows.go @@ -51,9 +51,10 @@ func (h *HandlerWindows) DisplayStatus(profileName string) error { func (h *HandlerWindows) CreateJob(job *Config, schedules []*calendar.Event, permission Permission) error { // default permission will be system perm := schtasks.SystemAccount - if permission == PermissionUserBackground { + switch permission { + case PermissionUserBackground: perm = schtasks.UserAccount - } else if permission == PermissionUserLoggedOn { + case PermissionUserLoggedOn: perm = schtasks.UserLoggedOnAccount } diff --git a/shell/command_test.go b/shell/command_test.go index 42bf96d5..1849414b 100644 --- a/shell/command_test.go +++ b/shell/command_test.go @@ -2,6 +2,7 @@ package shell import ( "bytes" + "context" "fmt" "io" "os" @@ -14,6 +15,7 @@ import ( "testing" "time" + "github.com/creativeprojects/resticprofile/constants" "github.com/creativeprojects/resticprofile/platform" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,6 +28,9 @@ var ( func TestMain(m *testing.M) { // using an anonymous function to handle defer statements before os.Exit() exitCode := func() int { + ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultTestTimeout) + defer cancel() + tempDir, err := os.MkdirTemp("", "resticprofile-shell") if err != nil { fmt.Fprintf(os.Stderr, "cannot create temp dir: %v\n", err) @@ -38,7 +43,7 @@ func TestMain(m *testing.M) { if platform.IsWindows() { mockBinary += ".exe" } - cmd := exec.Command("go", "build", "-buildvcs=false", "-o", mockBinary, "./mock") + cmd := exec.CommandContext(ctx, "go", "build", "-buildvcs=false", "-o", mockBinary, "./mock") if output, err := cmd.CombinedOutput(); err != nil { fmt.Fprintf(os.Stderr, "Error building mock binary: %s\nCommand output: %s\n", err, string(output)) return 1 diff --git a/systemd/generate.go b/systemd/generate.go index b03ace9b..badf6b2e 100644 --- a/systemd/generate.go +++ b/systemd/generate.go @@ -74,6 +74,7 @@ const ( // templateInfo to create systemd unit type templateInfo struct { templates.DefaultData + JobDescription string TimerDescription string WorkingDirectory string diff --git a/util/dotenv.go b/util/dotenv.go index 26fe6ec2..a5db8145 100644 --- a/util/dotenv.go +++ b/util/dotenv.go @@ -70,7 +70,7 @@ func (f *EnvironmentFile) Name() string { return f.filename } // Valid returns true as the loaded values are in-sync with underlying dotenv file func (f *EnvironmentFile) Valid() bool { if s, err := os.Stat(f.filename); err == nil && !s.IsDir() { - return f.fileInfo.Size() == s.Size() && f.fileInfo.ModTime() == s.ModTime() + return f.fileInfo.Size() == s.Size() && f.fileInfo.ModTime().Equal(s.ModTime()) } return false }