From dc6dfd8b2caab3224590a943f66d0dbceb7f88c9 Mon Sep 17 00:00:00 2001 From: Alexander Renz Date: Wed, 14 Jan 2026 16:07:04 +0100 Subject: [PATCH] v3.42.31: Add schollz/progressbar for visual progress display - Visual progress bars for cloud uploads/downloads - Byte transfer display, speed, ETA prediction - Color-coded Unicode block progress - Checksum verification with progress bar for large files - Spinner for indeterminate operations (unknown size) - New types: NewSchollzBar(), NewSchollzBarItems(), NewSchollzSpinner() - Progress Writer() method for io.Copy integration --- CHANGELOG.md | 18 ++++ bin/README.md | 6 +- go.mod | 3 + go.sum | 6 ++ internal/backup/engine.go | 18 ++-- internal/progress/progress.go | 160 +++++++++++++++++++++++++++++ internal/restore/cloud_download.go | 64 ++++++++++-- internal/restore/preflight.go | 16 +-- main.go | 2 +- 9 files changed, 267 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc42b4e..aaf44b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to dbbackup will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.42.31] - 2026-01-14 "Visual Progress Bars" + +### Added - schollz/progressbar for Enhanced Progress Display +- **Visual progress bars** for cloud uploads/downloads with: + - Byte transfer display (e.g., `245 MB / 1.2 GB`) + - Transfer speed (e.g., `45 MB/s`) + - ETA prediction + - Color-coded progress with Unicode blocks +- **Checksum verification progress** - visual progress while calculating SHA-256 +- **Spinner for indeterminate operations** - Braille-style spinner when size unknown +- New progress types: `NewSchollzBar()`, `NewSchollzBarItems()`, `NewSchollzSpinner()` +- Progress bar `Writer()` method for io.Copy integration + +### Changed +- Cloud download shows real-time byte progress instead of 10% log messages +- Cloud upload shows visual progress bar instead of debug logs +- Checksum verification shows progress for large files + ## [3.42.30] - 2026-01-09 "Better Error Aggregation" ### Added - go-multierror for Cluster Restore Errors diff --git a/bin/README.md b/bin/README.md index 65e9e32..1c29f37 100644 --- a/bin/README.md +++ b/bin/README.md @@ -3,9 +3,9 @@ This directory contains pre-compiled binaries for the DB Backup Tool across multiple platforms and architectures. ## Build Information -- **Version**: 3.42.10 -- **Build Time**: 2026-01-14_14:49:15_UTC -- **Git Commit**: 8c85d85 +- **Version**: 3.42.30 +- **Build Time**: 2026-01-14_14:59:20_UTC +- **Git Commit**: 7b4ab76 ## Recent Updates (v1.1.0) - ✅ Fixed TUI progress display with line-by-line output diff --git a/go.mod b/go.mod index d814584..b2af32d 100755 --- a/go.mod +++ b/go.mod @@ -86,12 +86,14 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/schollz/progressbar/v3 v3.19.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect @@ -111,6 +113,7 @@ require ( golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect diff --git a/go.sum b/go.sum index c2019f1..170764a 100755 --- a/go.sum +++ b/go.sum @@ -177,6 +177,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -196,6 +198,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= +github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -260,6 +264,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= diff --git a/internal/backup/engine.go b/internal/backup/engine.go index e51e46e..3130e48 100755 --- a/internal/backup/engine.go +++ b/internal/backup/engine.go @@ -1242,23 +1242,29 @@ func (e *Engine) uploadToCloud(ctx context.Context, backupFile string, tracker * filename := filepath.Base(backupFile) e.log.Info("Uploading backup to cloud", "file", filename, "size", cloud.FormatSize(info.Size())) - // Progress callback - var lastPercent int + // Create schollz progressbar for visual upload progress + bar := progress.NewSchollzBar(info.Size(), fmt.Sprintf("Uploading %s", filename)) + + // Progress callback with schollz progressbar + var lastBytes int64 progressCallback := func(transferred, total int64) { - percent := int(float64(transferred) / float64(total) * 100) - if percent != lastPercent && percent%10 == 0 { - e.log.Debug("Upload progress", "percent", percent, "transferred", cloud.FormatSize(transferred), "total", cloud.FormatSize(total)) - lastPercent = percent + delta := transferred - lastBytes + if delta > 0 { + _ = bar.Add64(delta) } + lastBytes = transferred } // Upload to cloud err = backend.Upload(ctx, backupFile, filename, progressCallback) if err != nil { + bar.Fail("Upload failed") uploadStep.Fail(fmt.Errorf("cloud upload failed: %w", err)) return err } + _ = bar.Finish() + // Also upload metadata file metaFile := backupFile + ".meta.json" if _, err := os.Stat(metaFile); err == nil { diff --git a/internal/progress/progress.go b/internal/progress/progress.go index e7f18b4..387a285 100755 --- a/internal/progress/progress.go +++ b/internal/progress/progress.go @@ -6,6 +6,8 @@ import ( "os" "strings" "time" + + "github.com/schollz/progressbar/v3" ) // Indicator represents a progress indicator interface @@ -440,6 +442,8 @@ func NewIndicator(interactive bool, indicatorType string) Indicator { return NewDots() case "bar": return NewProgressBar(100) // Default to 100 steps + case "schollz": + return NewSchollzBarItems(100, "Progress") case "line": return NewLineByLine() case "light": @@ -463,3 +467,159 @@ func (n *NullIndicator) Complete(message string) {} func (n *NullIndicator) Fail(message string) {} func (n *NullIndicator) Stop() {} func (n *NullIndicator) SetEstimator(estimator *ETAEstimator) {} + +// SchollzBar wraps schollz/progressbar for enhanced progress display +// Ideal for byte-based operations like archive extraction and file transfers +type SchollzBar struct { + bar *progressbar.ProgressBar + message string + total int64 + estimator *ETAEstimator +} + +// NewSchollzBar creates a new schollz progressbar with byte-based progress +func NewSchollzBar(total int64, description string) *SchollzBar { + bar := progressbar.NewOptions64( + total, + progressbar.OptionEnableColorCodes(true), + progressbar.OptionShowBytes(true), + progressbar.OptionSetWidth(40), + progressbar.OptionSetDescription(description), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "[green]█[reset]", + SaucerHead: "[green]▌[reset]", + SaucerPadding: "░", + BarStart: "[", + BarEnd: "]", + }), + progressbar.OptionShowCount(), + progressbar.OptionSetPredictTime(true), + progressbar.OptionFullWidth(), + progressbar.OptionClearOnFinish(), + ) + return &SchollzBar{ + bar: bar, + message: description, + total: total, + } +} + +// NewSchollzBarItems creates a progressbar for item counts (not bytes) +func NewSchollzBarItems(total int, description string) *SchollzBar { + bar := progressbar.NewOptions( + total, + progressbar.OptionEnableColorCodes(true), + progressbar.OptionShowCount(), + progressbar.OptionSetWidth(40), + progressbar.OptionSetDescription(description), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "[cyan]█[reset]", + SaucerHead: "[cyan]▌[reset]", + SaucerPadding: "░", + BarStart: "[", + BarEnd: "]", + }), + progressbar.OptionSetPredictTime(true), + progressbar.OptionFullWidth(), + progressbar.OptionClearOnFinish(), + ) + return &SchollzBar{ + bar: bar, + message: description, + total: int64(total), + } +} + +// NewSchollzSpinner creates an indeterminate spinner for unknown-length operations +func NewSchollzSpinner(description string) *SchollzBar { + bar := progressbar.NewOptions( + -1, // Indeterminate + progressbar.OptionEnableColorCodes(true), + progressbar.OptionSetWidth(40), + progressbar.OptionSetDescription(description), + progressbar.OptionSpinnerType(14), // Braille spinner + progressbar.OptionFullWidth(), + ) + return &SchollzBar{ + bar: bar, + message: description, + total: -1, + } +} + +// Start initializes the progress bar (Indicator interface) +func (s *SchollzBar) Start(message string) { + s.message = message + s.bar.Describe(message) +} + +// Update updates the description (Indicator interface) +func (s *SchollzBar) Update(message string) { + s.message = message + s.bar.Describe(message) +} + +// Add adds bytes/items to the progress +func (s *SchollzBar) Add(n int) error { + return s.bar.Add(n) +} + +// Add64 adds bytes to the progress (for large files) +func (s *SchollzBar) Add64(n int64) error { + return s.bar.Add64(n) +} + +// Set sets the current progress value +func (s *SchollzBar) Set(n int) error { + return s.bar.Set(n) +} + +// Set64 sets the current progress value (for large files) +func (s *SchollzBar) Set64(n int64) error { + return s.bar.Set64(n) +} + +// ChangeMax updates the maximum value +func (s *SchollzBar) ChangeMax(max int) { + s.bar.ChangeMax(max) + s.total = int64(max) +} + +// ChangeMax64 updates the maximum value (for large files) +func (s *SchollzBar) ChangeMax64(max int64) { + s.bar.ChangeMax64(max) + s.total = max +} + +// Complete finishes with success (Indicator interface) +func (s *SchollzBar) Complete(message string) { + _ = s.bar.Finish() + fmt.Printf("\n[green][OK][reset] %s\n", message) +} + +// Fail finishes with failure (Indicator interface) +func (s *SchollzBar) Fail(message string) { + _ = s.bar.Clear() + fmt.Printf("\n[red][FAIL][reset] %s\n", message) +} + +// Stop stops the progress bar (Indicator interface) +func (s *SchollzBar) Stop() { + _ = s.bar.Clear() +} + +// SetEstimator is a no-op (schollz has built-in ETA) +func (s *SchollzBar) SetEstimator(estimator *ETAEstimator) { + s.estimator = estimator +} + +// Writer returns an io.Writer that updates progress as data is written +// Useful for wrapping readers/writers in copy operations +func (s *SchollzBar) Writer() io.Writer { + return s.bar +} + +// Finish marks the progress as complete +func (s *SchollzBar) Finish() error { + return s.bar.Finish() +} diff --git a/internal/restore/cloud_download.go b/internal/restore/cloud_download.go index 48c2212..d9500a9 100644 --- a/internal/restore/cloud_download.go +++ b/internal/restore/cloud_download.go @@ -12,6 +12,7 @@ import ( "dbbackup/internal/cloud" "dbbackup/internal/logger" "dbbackup/internal/metadata" + "dbbackup/internal/progress" ) // CloudDownloader handles downloading backups from cloud storage @@ -73,25 +74,43 @@ func (d *CloudDownloader) Download(ctx context.Context, remotePath string, opts size = 0 // Continue anyway } - // Progress callback - var lastPercent int + // Create schollz progressbar for visual download progress + var bar *progress.SchollzBar + if size > 0 { + bar = progress.NewSchollzBar(size, fmt.Sprintf("Downloading %s", filename)) + } else { + bar = progress.NewSchollzSpinner(fmt.Sprintf("Downloading %s", filename)) + } + + // Progress callback with schollz progressbar + var lastBytes int64 progressCallback := func(transferred, total int64) { - if total > 0 { - percent := int(float64(transferred) / float64(total) * 100) - if percent != lastPercent && percent%10 == 0 { - d.log.Info("Download progress", "percent", percent, "transferred", cloud.FormatSize(transferred), "total", cloud.FormatSize(total)) - lastPercent = percent + if bar != nil { + // Update progress bar with delta + delta := transferred - lastBytes + if delta > 0 { + _ = bar.Add64(delta) } + lastBytes = transferred } } // Download file if err := d.backend.Download(ctx, remotePath, localPath, progressCallback); err != nil { + if bar != nil { + bar.Fail("Download failed") + } // Cleanup on failure os.RemoveAll(tempSubDir) return nil, fmt.Errorf("download failed: %w", err) } + if bar != nil { + _ = bar.Finish() + } + + d.log.Info("Download completed", "size", cloud.FormatSize(size)) + result := &DownloadResult{ LocalPath: localPath, RemotePath: remotePath, @@ -115,7 +134,7 @@ func (d *CloudDownloader) Download(ctx context.Context, remotePath string, opts // Verify checksum if requested if opts.VerifyChecksum { d.log.Info("Verifying checksum...") - checksum, err := calculateSHA256(localPath) + checksum, err := calculateSHA256WithProgress(localPath) if err != nil { // Cleanup on verification failure os.RemoveAll(tempSubDir) @@ -186,6 +205,35 @@ func calculateSHA256(filePath string) (string, error) { return hex.EncodeToString(hash.Sum(nil)), nil } +// calculateSHA256WithProgress calculates SHA-256 with visual progress bar +func calculateSHA256WithProgress(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + // Get file size for progress bar + stat, err := file.Stat() + if err != nil { + return "", err + } + + bar := progress.NewSchollzBar(stat.Size(), "Verifying checksum") + hash := sha256.New() + + // Create a multi-writer to update both hash and progress + writer := io.MultiWriter(hash, bar.Writer()) + + if _, err := io.Copy(writer, file); err != nil { + bar.Fail("Verification failed") + return "", err + } + + _ = bar.Finish() + return hex.EncodeToString(hash.Sum(nil)), nil +} + // DownloadFromCloudURI is a convenience function to download from a cloud URI func DownloadFromCloudURI(ctx context.Context, uri string, opts DownloadOptions) (*DownloadResult, error) { // Parse URI diff --git a/internal/restore/preflight.go b/internal/restore/preflight.go index 0e1c1f1..4bea943 100644 --- a/internal/restore/preflight.go +++ b/internal/restore/preflight.go @@ -35,15 +35,15 @@ type PreflightResult struct { // LinuxChecks contains Linux kernel/system checks type LinuxChecks struct { - ShmMax int64 // /proc/sys/kernel/shmmax - ShmAll int64 // /proc/sys/kernel/shmall - MemTotal uint64 // Total RAM in bytes - MemAvailable uint64 // Available RAM in bytes + ShmMax int64 // /proc/sys/kernel/shmmax + ShmAll int64 // /proc/sys/kernel/shmall + MemTotal uint64 // Total RAM in bytes + MemAvailable uint64 // Available RAM in bytes MemUsedPercent float64 // Memory usage percentage - ShmMaxOK bool // Is shmmax sufficient? - ShmAllOK bool // Is shmall sufficient? - MemAvailableOK bool // Is available RAM sufficient? - IsLinux bool // Are we running on Linux? + ShmMaxOK bool // Is shmmax sufficient? + ShmAllOK bool // Is shmall sufficient? + MemAvailableOK bool // Is available RAM sufficient? + IsLinux bool // Are we running on Linux? } // PostgreSQLChecks contains PostgreSQL configuration checks diff --git a/main.go b/main.go index 5ad5254..69ec90a 100755 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( // Build information (set by ldflags) var ( - version = "3.42.30" + version = "3.42.31" buildTime = "unknown" gitCommit = "unknown" )