Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc6dfd8b2c | |||
| 7b4ab76313 |
38
CHANGELOG.md
38
CHANGELOG.md
@@ -5,6 +5,44 @@ 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
|
||||
- **Enhanced error reporting** - Now shows ALL database failures, not just a count
|
||||
- Uses `hashicorp/go-multierror` for proper error aggregation
|
||||
- Each failed database error is preserved with full context
|
||||
- Bullet-pointed error output for readability:
|
||||
```
|
||||
cluster restore completed with 3 failures:
|
||||
3 database(s) failed:
|
||||
• db1: restore failed: max_locks_per_transaction exceeded
|
||||
• db2: restore failed: connection refused
|
||||
• db3: failed to create database: permission denied
|
||||
```
|
||||
|
||||
### Changed
|
||||
- Replaced string slice error collection with proper `*multierror.Error`
|
||||
- Thread-safe error aggregation with dedicated mutex
|
||||
- Improved error wrapping with `%w` for error chain preservation
|
||||
|
||||
## [3.42.10] - 2026-01-08 "Code Quality"
|
||||
|
||||
### Fixed - Code Quality Issues
|
||||
|
||||
@@ -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
|
||||
|
||||
5
go.mod
5
go.mod
@@ -75,6 +75,8 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -84,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
|
||||
@@ -109,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
|
||||
|
||||
10
go.sum
10
go.sum
@@ -149,6 +149,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAV
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
@@ -173,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=
|
||||
@@ -192,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=
|
||||
@@ -256,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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"dbbackup/internal/progress"
|
||||
"dbbackup/internal/security"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver
|
||||
)
|
||||
|
||||
@@ -961,7 +962,8 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error {
|
||||
}()
|
||||
}
|
||||
|
||||
var failedDBs []string
|
||||
var restoreErrors *multierror.Error
|
||||
var restoreErrorsMu sync.Mutex
|
||||
totalDBs := 0
|
||||
|
||||
// Count total databases
|
||||
@@ -995,7 +997,6 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error {
|
||||
}
|
||||
|
||||
var successCount, failCount int32
|
||||
var failedDBsMu sync.Mutex
|
||||
var mu sync.Mutex // Protect shared resources (progress, logger)
|
||||
|
||||
// Create semaphore to limit concurrency
|
||||
@@ -1050,9 +1051,9 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error {
|
||||
// STEP 2: Create fresh database
|
||||
if err := e.ensureDatabaseExists(ctx, dbName); err != nil {
|
||||
e.log.Error("Failed to create database", "name", dbName, "error", err)
|
||||
failedDBsMu.Lock()
|
||||
failedDBs = append(failedDBs, fmt.Sprintf("%s: failed to create database: %v", dbName, err))
|
||||
failedDBsMu.Unlock()
|
||||
restoreErrorsMu.Lock()
|
||||
restoreErrors = multierror.Append(restoreErrors, fmt.Errorf("%s: failed to create database: %w", dbName, err))
|
||||
restoreErrorsMu.Unlock()
|
||||
atomic.AddInt32(&failCount, 1)
|
||||
return
|
||||
}
|
||||
@@ -1095,10 +1096,10 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error {
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
failedDBsMu.Lock()
|
||||
restoreErrorsMu.Lock()
|
||||
// Include more context in the error message
|
||||
failedDBs = append(failedDBs, fmt.Sprintf("%s: restore failed: %v", dbName, restoreErr))
|
||||
failedDBsMu.Unlock()
|
||||
restoreErrors = multierror.Append(restoreErrors, fmt.Errorf("%s: restore failed: %w", dbName, restoreErr))
|
||||
restoreErrorsMu.Unlock()
|
||||
atomic.AddInt32(&failCount, 1)
|
||||
return
|
||||
}
|
||||
@@ -1116,7 +1117,17 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error {
|
||||
failCountFinal := int(atomic.LoadInt32(&failCount))
|
||||
|
||||
if failCountFinal > 0 {
|
||||
failedList := strings.Join(failedDBs, "\n ")
|
||||
// Format multi-error with detailed output
|
||||
restoreErrors.ErrorFormat = func(errs []error) string {
|
||||
if len(errs) == 1 {
|
||||
return errs[0].Error()
|
||||
}
|
||||
points := make([]string, len(errs))
|
||||
for i, err := range errs {
|
||||
points[i] = fmt.Sprintf(" • %s", err.Error())
|
||||
}
|
||||
return fmt.Sprintf("%d database(s) failed:\n%s", len(errs), strings.Join(points, "\n"))
|
||||
}
|
||||
|
||||
// Log summary
|
||||
e.log.Info("Cluster restore completed with failures",
|
||||
@@ -1127,7 +1138,7 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error {
|
||||
e.progress.Fail(fmt.Sprintf("Cluster restore: %d succeeded, %d failed out of %d total", successCountFinal, failCountFinal, totalDBs))
|
||||
operation.Complete(fmt.Sprintf("Partial restore: %d/%d databases succeeded", successCountFinal, totalDBs))
|
||||
|
||||
return fmt.Errorf("cluster restore completed with %d failures:\n %s", failCountFinal, failedList)
|
||||
return fmt.Errorf("cluster restore completed with %d failures:\n%s", failCountFinal, restoreErrors.Error())
|
||||
}
|
||||
|
||||
e.progress.Complete(fmt.Sprintf("Cluster restored successfully: %d databases", successCountFinal))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user