diff --git a/internal/vaultik/restore.go b/internal/vaultik/restore.go index 0a8da62..acc6f8a 100644 --- a/internal/vaultik/restore.go +++ b/internal/vaultik/restore.go @@ -126,24 +126,26 @@ func (v *Vaultik) Restore(opts *RestoreOptions) error { humanize.Bytes(uint64(totalBytes)), ) - // Create progress bar if output is a terminal + // Create progress bar if stderr is a terminal + isTTY := isTerminal(v.Stderr) var bar *progressbar.ProgressBar - if isTerminal() { + if isTTY { bar = progressbar.NewOptions64( totalBytes, progressbar.OptionSetDescription("Restoring"), - progressbar.OptionSetWriter(os.Stderr), + progressbar.OptionSetWriter(v.Stderr), progressbar.OptionShowBytes(true), progressbar.OptionShowCount(), progressbar.OptionSetWidth(40), progressbar.OptionThrottle(100*time.Millisecond), progressbar.OptionOnCompletion(func() { - fmt.Fprint(os.Stderr, "\n") + fmt.Fprint(v.Stderr, "\n") }), progressbar.OptionSetRenderBlankState(true), ) } + filesProcessed := 0 for _, file := range files { if v.ctx.Err() != nil { return v.ctx.Err() @@ -151,17 +153,33 @@ func (v *Vaultik) Restore(opts *RestoreOptions) error { if err := v.restoreFile(v.ctx, repos, file, opts.TargetDir, identity, chunkToBlobMap, blobCache, result); err != nil { log.Error("Failed to restore file", "path", file.Path, "error", err) + filesProcessed++ // Update progress bar even on failure if bar != nil { _ = bar.Add64(file.Size) } + // Periodic structured log for non-terminal contexts (headless/CI) + if !isTTY && filesProcessed%100 == 0 { + log.Info("Restore progress", + "files", fmt.Sprintf("%d/%d", filesProcessed, len(files)), + "bytes_restored", humanize.Bytes(uint64(result.BytesRestored)), + ) + } continue } + filesProcessed++ // Update progress bar if bar != nil { _ = bar.Add64(file.Size) } + // Periodic structured log for non-terminal contexts (headless/CI) + if !isTTY && (filesProcessed%100 == 0 || filesProcessed == len(files)) { + log.Info("Restore progress", + "files", fmt.Sprintf("%d/%d", filesProcessed, len(files)), + "bytes_restored", humanize.Bytes(uint64(result.BytesRestored)), + ) + } } if bar != nil { @@ -602,7 +620,7 @@ func (v *Vaultik) verifyRestoredFiles( // Create progress bar if output is a terminal var bar *progressbar.ProgressBar - if isTerminal() { + if isTerminal(v.Stderr) { bar = progressbar.NewOptions64( totalBytes, progressbar.OptionSetDescription("Verifying"), @@ -710,7 +728,11 @@ func (v *Vaultik) verifyFile( return bytesVerified, nil } -// isTerminal returns true if stdout is a terminal -func isTerminal() bool { - return term.IsTerminal(int(os.Stdout.Fd())) +// isTerminal returns true if the given writer is connected to a terminal. +// Returns false if the writer does not expose a file descriptor (e.g. in tests). +func isTerminal(w io.Writer) bool { + if f, ok := w.(*os.File); ok { + return term.IsTerminal(int(f.Fd())) + } + return false }