-
-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathcommands.go
More file actions
443 lines (407 loc) · 15.1 KB
/
Copy pathcommands.go
File metadata and controls
443 lines (407 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
package main
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"maps"
"os"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"text/tabwriter"
"time"
"github.com/creativeprojects/clog"
"github.com/creativeprojects/resticprofile/batt"
"github.com/creativeprojects/resticprofile/config"
"github.com/creativeprojects/resticprofile/constants"
"github.com/creativeprojects/resticprofile/platform"
"github.com/creativeprojects/resticprofile/remote"
"github.com/creativeprojects/resticprofile/util/ansi"
"github.com/creativeprojects/resticprofile/win"
"github.com/distatus/battery"
)
var (
ownCommands = NewOwnCommands()
elevation sync.Once
)
func init() {
ownCommands.Register(getOwnCommands())
// own commands have no profile section, prevent their definition
for _, command := range ownCommands.All() {
config.ExcludeProfileSection(command.name)
}
}
func getOwnCommands() []ownCommand {
return []ownCommand{
// commands that don't need loading the configuration
{
name: "help",
description: "display help (use resticprofile help [command])",
longDescription: "The \"help\" command prints commandline help on resticprofile flags & commands (own and restic) including information on flags passed to restic from various profiles defined in the current configuration.\n\nHelp on a specific command is displayed by adding the command name as argument after \"help\", e.g. use \"resticprofile help version\" to get details on the \"version\" command.",
action: displayHelpCommand,
needConfiguration: false,
},
{
name: "version",
description: "display version (run in verbose mode for detailed information)",
longDescription: "The \"version\" command displays brief or detailed version information",
action: displayVersion,
needConfiguration: false,
flags: map[string]string{"-v, --verbose": "display detailed version information"},
},
{
name: "random-key",
description: "generate a cryptographically secure random key to use as a restic keyfile",
action: randomKey,
needConfiguration: false,
hide: true, // replaced by the generate command
},
{
name: "generate",
description: "generate resources such as random key, bash/zsh completion scripts, etc.",
longDescription: "The \"generate\" command is used to create various resources and print them to stdout",
action: generateCommand,
needConfiguration: false,
hide: false,
flags: map[string]string{
"--random-key [size]": "generate a cryptographically secure random key to use as a restic keyfile (size defaults to 1024 when omitted)",
"--config-reference [--version 0.15] [template]": "generate a config file reference from a go template (defaults to the built-in markdown template when omitted)",
"--json-schema [--version 0.15] [v1|v2]": "generate a JSON schema that validates resticprofile configuration files in YAML or JSON format",
"--bash-completion": "generate a shell completion script for bash",
"--zsh-completion": "generate a shell completion script for zsh",
"--fish-completion": "generate a shell completion script for fish",
},
},
// commands that need the configuration
{
name: "profiles",
description: "display profile names from the configuration file",
longDescription: "The \"profiles\" command prints brief information on all profiles and groups that are declared in the configuration file",
action: displayProfilesCommand,
needConfiguration: true,
},
{
name: "show",
description: "show all the details of the current profile or group",
longDescription: "The \"show\" command prints the effective configuration of the selected profile or group.\n\nThe effective profile or group configuration is built by loading all includes, applying inheritance, mixins, templates and variables and parsing the result.",
action: showProfileOrGroup,
needConfiguration: true,
},
{
name: "schedule",
description: "schedule jobs from a profile or group (or of all profiles and groups)",
longDescription: "The \"schedule\" command registers declared schedules of the selected profile or group (or of all profiles and groups) as scheduled jobs within the scheduling service of the operating system",
action: createSchedule,
needConfiguration: true,
hide: false,
flags: map[string]string{
"--no-start": "don't start the job after installing (systemd/launch only)",
"--start": "start the job after installing (systemd/launch only)",
"--reload": "force a systemctl daemon-reload after setting up the files (systemd only, available since v0.32.0)",
"--all": "add all scheduled jobs of all profiles and groups",
},
},
{
name: "unschedule",
description: "remove scheduled jobs of a profile or group (or of all profiles and groups)",
longDescription: "The \"unschedule\" command removes scheduled jobs from the scheduling service of the operating system. The command removes jobs for schedules declared in the selected profile or group (or of all profiles and groups)",
action: removeSchedule,
needConfiguration: true,
hide: false,
flags: map[string]string{"--all": "remove all scheduled jobs of all profiles and groups"},
},
{
name: "status",
description: "display the status of scheduled jobs of a profile or group (or of all profiles and groups)",
longDescription: "The \"status\" command prints all declared schedules of the selected profile or group (or of all profiles and groups) and shows the status of related scheduled jobs in the scheduling service of the operating system",
action: statusSchedule,
needConfiguration: true,
hide: false,
flags: map[string]string{"--all": "display the status of all scheduled jobs of all profiles and groups"},
},
{
name: "run-schedule",
description: "runs a scheduled job. This command should only be called by the scheduling service",
longDescription: "The \"run-schedule\" command loads the scheduled job configuration from the name in parameter and runs the restic command with the arguments defined in the profile or group. The name in parameter is <command>@<profile-or-group-name>.",
pre: preRunSchedule,
action: runSchedule,
needConfiguration: true,
hide: false,
hideInCompletion: true,
noProfile: true,
},
// hidden commands
{
name: "complete",
description: "create commandline completion results based on given args",
action: completeCommand,
needConfiguration: false,
hide: true,
},
{
name: "elevation",
description: "test windows elevated mode",
action: testElevationCommand,
needConfiguration: false,
hide: true,
},
{
name: "panic",
description: "(debug only) simulates a panic",
action: panicCommand,
needConfiguration: false,
hide: true,
},
{
name: "battery",
description: "check battery status",
action: batteryCommand,
needConfiguration: false,
hide: true,
},
{
name: "send",
description: "send a configuration profile to a remote client and execute a command",
action: sendProfileCommand,
needConfiguration: true,
noProfile: true,
hide: true,
experimental: true,
},
{
name: "serve",
description: "serve configuration profiles to remote clients",
action: serveCommand,
needConfiguration: true,
noProfile: true,
hide: true,
experimental: true,
},
}
}
func panicCommand(_ commandContext) error {
panic("you asked for it")
}
func completeCommand(ctx commandContext) error {
args := ctx.request.arguments
requester := "unknown"
requesterVersion := 0
debugCompletion(fmt.Sprintf("args: `%s`", strings.Join(args, "`, `")))
// Parse requester as first argument. Format "[kind]:v[version]", e.g. "bash:v1"
if len(args) > 0 {
matcher := regexp.MustCompile(`^(bash|zsh|fish):v(\d+)$`)
if matches := matcher.FindStringSubmatch(args[0]); matches != nil {
requester = matches[1]
if v, err := strconv.Atoi(matches[2]); err == nil {
requesterVersion = v
}
args = args[1:]
}
}
// Log when requester isn't specified.
if requester == "unknown" || requesterVersion < 1 {
clog.Warningf("Requester %q version %d not explicitly supported", requester, requesterVersion)
}
// Ensure newer completion scripts will not fail on outdated resticprofile
if requesterVersion > 9 {
return nil
}
// fish has always shown descriptions; zsh gained them in v2 of the protocol.
// Gating zsh on the version keeps an older "zsh:v1" completion script (which
// cannot parse descriptions) working against a newer resticprofile.
includeDescription := requester == "fish" || (requester == "zsh" && requesterVersion >= 2)
completer := NewCompleter(ctx.ownCommands.All(), DefaultFlagsLoader, includeDescription)
// zsh:v2+ forwards the resolved restic arguments instead of reconstructing them
// from the command line (which would wrongly include resticprofile's own flags).
completer.forwardResticArgs = requesterVersion >= 2
completions := completer.Complete(args)
if len(completions) > 0 {
for _, completion := range completions {
ctx.terminal.Println(completion)
}
debugCompletion(fmt.Sprintf("completions: `%s`", strings.Join(completions, "`, `")))
}
return nil
}
func debugCompletion(line string) {
debugFile := filepath.Clean(os.Getenv("RESTICPROFILE_DEBUG_COMPLETION"))
if debugFile != "" && debugFile != "." {
f, err := os.OpenFile(debugFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
defer f.Close()
fmt.Fprintln(f, line)
}
}
}
func showProfileOrGroup(ctx commandContext) error {
c := ctx.config
flags := ctx.flags
// Load global section
global, err := c.GetGlobalSection()
if err != nil {
return fmt.Errorf("cannot load global section: %w", err)
}
var profileOrGroup config.Schedulable
if c.HasProfile(flags.name) {
// Load profile
profile, cleanup, err := openProfile(c, flags.name)
defer cleanup()
if err != nil {
return fmt.Errorf("profile '%s': %w", flags.name, err)
}
if profile == nil {
return fmt.Errorf("cannot load profile '%s'", flags.name)
}
profileOrGroup = profile
} else if c.HasProfileGroup(flags.name) {
group, err := c.GetProfileGroup(flags.name)
if err != nil {
return fmt.Errorf("group '%s': %w", flags.name, err)
}
if group == nil {
return fmt.Errorf("cannot load group '%s'", flags.name)
}
profileOrGroup = group
} else {
return fmt.Errorf("profile or group '%s': %w", flags.name, config.ErrNotFound)
}
// Show global
err = config.ShowStruct(ctx.terminal, global, constants.SectionConfigurationGlobal)
if err != nil {
clog.Errorf("cannot show global section: %s", err.Error())
}
_, _ = ctx.terminal.Println()
// Show profile or group
err = config.ShowStruct(ctx.terminal, profileOrGroup, profileOrGroup.Kind()+" "+flags.name)
if err != nil {
clog.Errorf("cannot show profile or group '%s': %s", flags.name, err.Error())
}
_, _ = ctx.terminal.Println()
// Show schedules
showSchedules(ctx.terminal, slices.Collect(maps.Values(profileOrGroup.Schedules())))
if profile, ok := profileOrGroup.(*config.Profile); ok {
// Show deprecation notice
displayDeprecationNotices(profile)
}
// Show config issues
c.DisplayConfigurationIssues()
return nil
}
func showSchedules(output io.Writer, schedules []*config.Schedule) {
slices.SortFunc(schedules, config.CompareSchedules)
for _, schedule := range schedules {
err := config.ShowStruct(output, schedule.ScheduleConfig, fmt.Sprintf("schedule %s", schedule.ScheduleOrigin()))
if err != nil {
_, _ = fmt.Fprintln(output, err)
}
_, _ = fmt.Fprintln(output, "")
}
}
// randomKey simply display a base64'd random key to the console
func randomKey(ctx commandContext) error {
var err error
flags := ctx.flags
size := uint64(1024)
// flags.resticArgs contain the command and the rest of the command line
if len(flags.resticArgs) > 1 {
// second parameter should be an integer
size, err = strconv.ParseUint(flags.resticArgs[1], 10, 32)
if err != nil {
return fmt.Errorf("cannot parse the key size: %w", err)
}
if size < 1 {
return fmt.Errorf("invalid key size: %v", size)
}
}
buffer := make([]byte, size)
_, err = rand.Read(buffer)
if err != nil {
return err
}
encoder := base64.NewEncoder(base64.StdEncoding, ctx.terminal)
_, err = encoder.Write(buffer)
encoder.Close()
ctx.terminal.Println()
return err
}
func testElevationCommand(ctx commandContext) error {
if ctx.flags.isChild {
client := remote.NewClient(ctx.flags.parentPort)
ctx.terminal.Print("simple line", "\n")
ctx.terminal.Printf("value = %d\n", 11)
ctx.terminal.Println(ansi.Bold("in bold"))
clog.Info("log content")
err := client.Done()
if err != nil {
return err
}
return nil
}
return elevated()
}
func retryElevated(err error, flags commandLineFlags) error {
if err == nil {
return nil
}
// maybe can find a better way than searching for the word "denied"?
if platform.IsWindows() && !flags.isChild && strings.Contains(err.Error(), "denied") {
// we try only once, otherwise we return the original error
elevation.Do(func() {
clog.Info("restarting resticprofile in elevated mode...")
err = elevated()
})
if err != nil {
return err
}
return nil
}
return err
}
func elevated() error {
if !platform.IsWindows() {
return errors.New("only available on Windows platform")
}
done := make(chan any)
err := remote.StartServer(done)
if err != nil {
return err
}
err = win.RunElevated(remote.GetPort())
if err != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
remote.StopServer(ctx)
return err
}
// wait until the server is done
<-done
return nil
}
func batteryCommand(ctx commandContext) error {
all, err := batt.Batteries()
if err != nil {
if errors.Is(err, battery.ErrFatal{}) {
return fmt.Errorf("error loading battery information: %w", err)
}
clog.Errorf("error loading battery information: %s", err)
}
if len(all) == 0 {
clog.Info("no battery detected")
return nil
}
ctx.terminal.Println()
w := tabwriter.NewWriter(ctx.terminal, 2, 2, 2, ' ', tabwriter.AlignRight)
fmt.Fprintf(w, "Battery\tStatus\tCurrent capacity\tFull capacity\tDesign Capacity\tCharge Rate\tVoltage\t\n")
for index, batt := range all {
fmt.Fprintf(w, "#%d\t%s\t%.2f mWh\t%.2f mWh\t%.2f mWh\t%.2f mWh\t%.2f V\t\n", index, batt.State, batt.Current, batt.Full, batt.Design, batt.ChargeRate, batt.Voltage)
}
w.Flush()
ctx.terminal.Println()
return nil
}