mirror of
https://github.com/restic/restic.git
synced 2025-12-12 05:01:52 +00:00
termstatus: add stdin and inject into backup command
This commit is contained in:
@@ -182,7 +182,7 @@ func filterExisting(items []string, warnf func(msg string, args ...interface{}))
|
|||||||
// If filename is empty, readPatternsFromFile returns an empty slice.
|
// If filename is empty, readPatternsFromFile returns an empty slice.
|
||||||
// If filename is a dash (-), readPatternsFromFile will read the lines from the
|
// If filename is a dash (-), readPatternsFromFile will read the lines from the
|
||||||
// standard input.
|
// standard input.
|
||||||
func readLines(filename string) ([]string, error) {
|
func readLines(filename string, stdin io.ReadCloser) ([]string, error) {
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,7 @@ func readLines(filename string) ([]string, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if filename == "-" {
|
if filename == "-" {
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(stdin)
|
||||||
} else {
|
} else {
|
||||||
data, err = textfile.Read(filename)
|
data, err = textfile.Read(filename)
|
||||||
}
|
}
|
||||||
@@ -218,8 +218,8 @@ func readLines(filename string) ([]string, error) {
|
|||||||
// readFilenamesFromFileRaw reads a list of filenames from the given file,
|
// readFilenamesFromFileRaw reads a list of filenames from the given file,
|
||||||
// or stdin if filename is "-". Each filename is terminated by a zero byte,
|
// or stdin if filename is "-". Each filename is terminated by a zero byte,
|
||||||
// which is stripped off.
|
// which is stripped off.
|
||||||
func readFilenamesFromFileRaw(filename string) (names []string, err error) {
|
func readFilenamesFromFileRaw(filename string, stdin io.ReadCloser) (names []string, err error) {
|
||||||
f := os.Stdin
|
var f io.ReadCloser = stdin
|
||||||
if filename != "-" {
|
if filename != "-" {
|
||||||
if f, err = os.Open(filename); err != nil {
|
if f, err = os.Open(filename); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -378,13 +378,13 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// collectTargets returns a list of target files/dirs from several sources.
|
// collectTargets returns a list of target files/dirs from several sources.
|
||||||
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{})) (targets []string, err error) {
|
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{}), stdin io.ReadCloser) (targets []string, err error) {
|
||||||
if opts.Stdin || opts.StdinCommand {
|
if opts.Stdin || opts.StdinCommand {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range opts.FilesFrom {
|
for _, file := range opts.FilesFrom {
|
||||||
fromfile, err := readLines(file)
|
fromfile, err := readLines(file, stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -409,7 +409,7 @@ func collectTargets(opts BackupOptions, args []string, warnf func(msg string, ar
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range opts.FilesFromVerbatim {
|
for _, file := range opts.FilesFromVerbatim {
|
||||||
fromfile, err := readLines(file)
|
fromfile, err := readLines(file, stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -422,7 +422,7 @@ func collectTargets(opts BackupOptions, args []string, warnf func(msg string, ar
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range opts.FilesFromRaw {
|
for _, file := range opts.FilesFromRaw {
|
||||||
fromfile, err := readFilenamesFromFileRaw(file)
|
fromfile, err := readFilenamesFromFileRaw(file, stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -490,7 +490,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
targets, err := collectTargets(opts, args, msg.E)
|
targets, err := collectTargets(opts, args, msg.E, term.InputRaw())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -582,7 +582,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
progressPrinter.V("read data from stdin")
|
progressPrinter.V("read data from stdin")
|
||||||
}
|
}
|
||||||
filename := path.Join("/", opts.StdinFilename)
|
filename := path.Join("/", opts.StdinFilename)
|
||||||
var source io.ReadCloser = os.Stdin
|
var source io.ReadCloser = term.InputRaw()
|
||||||
if opts.StdinCommand {
|
if opts.StdinCommand {
|
||||||
source, err = fs.NewCommandReader(ctx, args, msg.E)
|
source, err = fs.NewCommandReader(ctx, args, msg.E)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func TestCollectTargets(t *testing.T) {
|
|||||||
FilesFromRaw: []string{f3.Name()},
|
FilesFromRaw: []string{f3.Name()},
|
||||||
}
|
}
|
||||||
|
|
||||||
targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}, t.Logf)
|
targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}, t.Logf, nil)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
sort.Strings(targets)
|
sort.Strings(targets)
|
||||||
rtest.Equals(t, expect, targets)
|
rtest.Equals(t, expect, targets)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func testRunInit(t testing.TB, opts GlobalOptions) {
|
|||||||
restic.TestSetLockTimeout(t, 0)
|
restic.TestSetLockTimeout(t, 0)
|
||||||
|
|
||||||
err := withTermStatus(opts, func(ctx context.Context, gopts GlobalOptions) error {
|
err := withTermStatus(opts, func(ctx context.Context, gopts GlobalOptions) error {
|
||||||
return runInit(ctx, InitOptions{}, opts, nil, gopts.term)
|
return runInit(ctx, InitOptions{}, gopts, nil, gopts.term)
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
t.Logf("repository initialized at %v", opts.Repo)
|
t.Logf("repository initialized at %v", opts.Repo)
|
||||||
@@ -54,10 +54,18 @@ func TestInitCopyChunkerParams(t *testing.T) {
|
|||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
repo, err := OpenRepository(context.TODO(), env.gopts, &progress.NoopPrinter{})
|
var repo *repository.Repository
|
||||||
|
err = withTermStatus(env.gopts, func(ctx context.Context, gopts GlobalOptions) error {
|
||||||
|
repo, err = OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||||
|
return err
|
||||||
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
otherRepo, err := OpenRepository(context.TODO(), env2.gopts, &progress.NoopPrinter{})
|
var otherRepo *repository.Repository
|
||||||
|
err = withTermStatus(env2.gopts, func(ctx context.Context, gopts GlobalOptions) error {
|
||||||
|
otherRepo, err = OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||||
|
return err
|
||||||
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial,
|
rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial,
|
||||||
|
|||||||
@@ -63,13 +63,16 @@ func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) {
|
|||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
repo, err := OpenRepository(context.TODO(), gopts, &progress.NoopPrinter{})
|
_ = withTermStatus(gopts, func(ctx context.Context, gopts GlobalOptions) error {
|
||||||
rtest.OK(t, err)
|
repo, err := OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||||
key, err := repository.SearchKey(context.TODO(), repo, testKeyNewPassword, 2, "")
|
rtest.OK(t, err)
|
||||||
rtest.OK(t, err)
|
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 2, "")
|
||||||
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Equals(t, "john", key.Username)
|
rtest.Equals(t, "john", key.Username)
|
||||||
rtest.Equals(t, "example.com", key.Hostname)
|
rtest.Equals(t, "example.com", key.Hostname)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
|
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
func testRunList(t testing.TB, opts GlobalOptions, tpe string) restic.IDs {
|
func testRunList(t testing.TB, opts GlobalOptions, tpe string) restic.IDs {
|
||||||
buf, err := withCaptureStdout(opts, func(opts GlobalOptions) error {
|
buf, err := withCaptureStdout(opts, func(opts GlobalOptions) error {
|
||||||
return withTermStatus(opts, func(ctx context.Context, gopts GlobalOptions) error {
|
return withTermStatus(opts, func(ctx context.Context, gopts GlobalOptions) error {
|
||||||
return runList(ctx, opts, []string{tpe}, gopts.term)
|
return runList(ctx, gopts, []string{tpe}, gopts.term)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string, printe
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if terminal.StdinIsTerminal() {
|
if opts.term.InputIsTerminal() {
|
||||||
password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt)
|
password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt)
|
||||||
} else {
|
} else {
|
||||||
printer.PT("reading repository password from stdin")
|
printer.PT("reading repository password from stdin")
|
||||||
@@ -286,7 +286,7 @@ func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if terminal.StdinIsTerminal() {
|
if gopts.term.InputIsTerminal() {
|
||||||
pw2, err := ReadPassword(ctx, gopts, prompt2, printer)
|
pw2, err := ReadPassword(ctx, gopts, prompt2, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -349,7 +349,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
passwordTriesLeft := 1
|
passwordTriesLeft := 1
|
||||||
if terminal.StdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword {
|
if opts.term.InputIsTerminal() && opts.password == "" && !opts.InsecureNoPassword {
|
||||||
passwordTriesLeft = 3
|
passwordTriesLeft = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ func withTermStatus(gopts GlobalOptions, callback func(ctx context.Context, gopt
|
|||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
term := termstatus.New(gopts.stdout, gopts.stderr, gopts.Quiet)
|
term := termstatus.New(os.Stdin, gopts.stdout, gopts.stderr, gopts.Quiet)
|
||||||
gopts.term = term
|
gopts.term = term
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ func main() {
|
|||||||
backends: collectBackends(),
|
backends: collectBackends(),
|
||||||
}
|
}
|
||||||
func() {
|
func() {
|
||||||
term, cancel := termstatus.Setup(os.Stdout, os.Stderr, globalOptions.Quiet)
|
term, cancel := termstatus.Setup(os.Stdin, os.Stdout, os.Stderr, globalOptions.Quiet)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
globalOptions.stdout, globalOptions.stderr = termstatus.WrapStdio(term)
|
globalOptions.stdout, globalOptions.stderr = termstatus.WrapStdio(term)
|
||||||
globalOptions.term = term
|
globalOptions.term = term
|
||||||
|
|||||||
@@ -131,12 +131,6 @@ func TestFillSecondaryGlobalOpts(t *testing.T) {
|
|||||||
PasswordCommand: "notEmpty",
|
PasswordCommand: "notEmpty",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// Test must fail as no password is given.
|
|
||||||
Opts: secondaryRepoOptions{
|
|
||||||
Repo: "backupDst",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// Test must fail as current and legacy options are mixed
|
// Test must fail as current and legacy options are mixed
|
||||||
Opts: secondaryRepoOptions{
|
Opts: secondaryRepoOptions{
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package terminal
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StdinIsTerminal() bool {
|
func InputIsTerminal(fd uintptr) bool {
|
||||||
return term.IsTerminal(int(os.Stdin.Fd()))
|
return term.IsTerminal(int(fd))
|
||||||
}
|
}
|
||||||
|
|
||||||
func OutputIsTerminal(fd uintptr) bool {
|
func OutputIsTerminal(fd uintptr) bool {
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ func (m *MockTerminal) CanUpdateStatus() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockTerminal) InputRaw() io.ReadCloser {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTerminal) InputIsTerminal() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockTerminal) OutputRaw() io.Writer {
|
func (m *MockTerminal) OutputRaw() io.Writer {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ type Terminal interface {
|
|||||||
SetStatus(lines []string)
|
SetStatus(lines []string)
|
||||||
// CanUpdateStatus returns true if the terminal can update the status lines.
|
// CanUpdateStatus returns true if the terminal can update the status lines.
|
||||||
CanUpdateStatus() bool
|
CanUpdateStatus() bool
|
||||||
|
InputRaw() io.ReadCloser
|
||||||
|
InputIsTerminal() bool
|
||||||
// OutputRaw returns the output writer. Should only be used if there is no
|
// OutputRaw returns the output writer. Should only be used if there is no
|
||||||
// other option. Must not be used in combination with Print, Error, SetStatus
|
// other option. Must not be used in combination with Print, Error, SetStatus
|
||||||
// or any other method that writes to the terminal.
|
// or any other method that writes to the terminal.
|
||||||
|
|||||||
@@ -17,14 +17,16 @@ var _ ui.Terminal = &Terminal{}
|
|||||||
// updated. When the output is redirected to a file, the status lines are not
|
// updated. When the output is redirected to a file, the status lines are not
|
||||||
// printed.
|
// printed.
|
||||||
type Terminal struct {
|
type Terminal struct {
|
||||||
|
rd io.ReadCloser
|
||||||
wr io.Writer
|
wr io.Writer
|
||||||
fd uintptr
|
fd uintptr
|
||||||
errWriter io.Writer
|
errWriter io.Writer
|
||||||
msg chan message
|
msg chan message
|
||||||
status chan status
|
status chan status
|
||||||
|
lastStatusLen int
|
||||||
|
inputIsTerminal bool
|
||||||
outputIsTerminal bool
|
outputIsTerminal bool
|
||||||
canUpdateStatus bool
|
canUpdateStatus bool
|
||||||
lastStatusLen int
|
|
||||||
|
|
||||||
// will be closed when the goroutine which runs Run() terminates, so it'll
|
// will be closed when the goroutine which runs Run() terminates, so it'll
|
||||||
// yield a default value immediately
|
// yield a default value immediately
|
||||||
@@ -56,12 +58,12 @@ type fder interface {
|
|||||||
// defer cancel()
|
// defer cancel()
|
||||||
// // do stuff
|
// // do stuff
|
||||||
// ```
|
// ```
|
||||||
func Setup(stdout, stderr io.Writer, quiet bool) (*Terminal, func()) {
|
func Setup(stdin io.ReadCloser, stdout, stderr io.Writer, quiet bool) (*Terminal, func()) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
// only shutdown once cancel is called to ensure that no output is lost
|
// only shutdown once cancel is called to ensure that no output is lost
|
||||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
term := New(stdout, stderr, quiet)
|
term := New(stdin, stdout, stderr, quiet)
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
@@ -82,8 +84,9 @@ func Setup(stdout, stderr io.Writer, quiet bool) (*Terminal, func()) {
|
|||||||
// normal output (via Print/Printf) are written to wr, error messages are
|
// normal output (via Print/Printf) are written to wr, error messages are
|
||||||
// written to errWriter. If disableStatus is set to true, no status messages
|
// written to errWriter. If disableStatus is set to true, no status messages
|
||||||
// are printed even if the terminal supports it.
|
// are printed even if the terminal supports it.
|
||||||
func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
|
func New(rd io.ReadCloser, wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
|
||||||
t := &Terminal{
|
t := &Terminal{
|
||||||
|
rd: rd,
|
||||||
wr: wr,
|
wr: wr,
|
||||||
errWriter: errWriter,
|
errWriter: errWriter,
|
||||||
msg: make(chan message),
|
msg: make(chan message),
|
||||||
@@ -95,6 +98,12 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d, ok := rd.(fder); ok {
|
||||||
|
if terminal.InputIsTerminal(d.Fd()) {
|
||||||
|
t.inputIsTerminal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if d, ok := wr.(fder); ok {
|
if d, ok := wr.(fder); ok {
|
||||||
if terminal.CanUpdateStatus(d.Fd()) {
|
if terminal.CanUpdateStatus(d.Fd()) {
|
||||||
// only use the fancy status code when we're running on a real terminal.
|
// only use the fancy status code when we're running on a real terminal.
|
||||||
@@ -111,6 +120,16 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InputIsTerminal returns whether the input is a terminal.
|
||||||
|
func (t *Terminal) InputIsTerminal() bool {
|
||||||
|
return t.inputIsTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputRaw returns the input reader.
|
||||||
|
func (t *Terminal) InputRaw() io.ReadCloser {
|
||||||
|
return t.rd
|
||||||
|
}
|
||||||
|
|
||||||
// CanUpdateStatus return whether the status output is updated in place.
|
// CanUpdateStatus return whether the status output is updated in place.
|
||||||
func (t *Terminal) CanUpdateStatus() bool {
|
func (t *Terminal) CanUpdateStatus() bool {
|
||||||
return t.canUpdateStatus
|
return t.canUpdateStatus
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
func TestSetStatus(t *testing.T) {
|
func TestSetStatus(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
term := New(&buf, io.Discard, false)
|
term := New(nil, &buf, io.Discard, false)
|
||||||
|
|
||||||
term.canUpdateStatus = true
|
term.canUpdateStatus = true
|
||||||
term.fd = ^uintptr(0)
|
term.fd = ^uintptr(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user