feat: render import progress bars in human-readable units (#581)

This commit is contained in:
Julian Sommer
2026-07-23 14:40:37 +10:00
committed by GitHub
parent 4803469037
commit 12085f029c
5 changed files with 72 additions and 9 deletions
+19 -1
View File
@@ -707,7 +707,25 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) {
// command, which would collapse the path to a relative config/ca.pem.
bakeCACertIntoImage(extractedImagePath, filepath.Join(baseDir, "config", "ca.pem"))
err = v_utils.ImportDiskImage(&s3Config, &vbConfig, extractedImagePath)
// Render the flush bar here rather than inside viperblock, which stays a
// pure storage library. The bar is built lazily on the first update so it is
// never drawn if the import fails before any bytes flush; viperblock
// throttles the callback to ≤101 invocations, so the human-readable title
// renders without the per-block render-frequency regression.
var flushBar *pterm.ProgressbarPrinter
var flushUpdate func(current uint64)
progress := func(current, total uint64) {
if flushBar == nil {
flushBar, flushUpdate = utils.NewByteProgressBar("Flushing image to storage", total)
}
flushUpdate(current)
}
err = v_utils.ImportDiskImage(&s3Config, &vbConfig, extractedImagePath, progress)
if flushBar != nil {
_, _ = flushBar.Stop()
}
if err != nil {
fmt.Fprintf(os.Stderr, "Could not import image to predastore: %v\n", err)
+1 -1
View File
@@ -22,7 +22,7 @@ require (
github.com/miekg/dns v1.1.72
github.com/mulgadc/northstar v1.13.0
github.com/mulgadc/predastore v1.13.1-0.20260722023246-c0a90d23b3a0
github.com/mulgadc/viperblock v1.13.1-0.20260723031009-8a2df5653734
github.com/mulgadc/viperblock v1.13.1-0.20260723043425-b78e24c374d5
github.com/nats-io/nats-server/v2 v2.14.3
github.com/nats-io/nats.go v1.52.0
github.com/opencontainers/runtime-spec v1.3.0
+2 -2
View File
@@ -1321,8 +1321,8 @@ github.com/mulgadc/northstar v1.13.0 h1:L9PyYZ9Klg7dv2kE5+DHOcgPHelaZRDIvnwBsbHs
github.com/mulgadc/northstar v1.13.0/go.mod h1:erQE/PzK9UMtgH8Y+uI5RdiboO/nqe1CUJbIpkae/FM=
github.com/mulgadc/predastore v1.13.1-0.20260722023246-c0a90d23b3a0 h1:29g+uPZ0LAANVA1IPnE/ogxJzJBJnBZvnTl51icp378=
github.com/mulgadc/predastore v1.13.1-0.20260722023246-c0a90d23b3a0/go.mod h1:38YSuk9zvYmbmgyM13WTkIQPJtvo2KmGJBYJul6NeAE=
github.com/mulgadc/viperblock v1.13.1-0.20260723031009-8a2df5653734 h1:BFR0RwNMxCwUONqaBvK1eo8wlLPcnAsaHnDon4jhg8A=
github.com/mulgadc/viperblock v1.13.1-0.20260723031009-8a2df5653734/go.mod h1:HMIDtq9NhuRYxmeAyZiqbYwoIpHF4wmMujk9PLW7Tzg=
github.com/mulgadc/viperblock v1.13.1-0.20260723043425-b78e24c374d5 h1:owinBKFK8AHM5XPtrVR27zhzakkqVOciLUjtvmxMvx4=
github.com/mulgadc/viperblock v1.13.1-0.20260723043425-b78e24c374d5/go.mod h1:ilr5G8RIkY441fZuRZeO4zPj9cZBsdUUPYjuXJ+cDRg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+8
View File
@@ -36,3 +36,11 @@ func SafeUint64ToInt64(v uint64) int64 {
}
return int64(v)
}
// SafeUint64ToInt converts uint64 to int, capping at math.MaxInt.
func SafeUint64ToInt(v uint64) int {
if v > math.MaxInt {
return math.MaxInt
}
return int(v)
}
+42 -5
View File
@@ -203,13 +203,21 @@ func DownloadFileWithProgress(url string, name string, filename string, timeout
cl := resp.ContentLength
if cl > 0 {
bar, _ := pterm.DefaultProgressbar.
WithTitle(fmt.Sprintf("Downloading %s", name)).
WithTotal(int(cl)).
Start()
total := SafeInt64ToUint64(cl)
bar, update := NewByteProgressBar(fmt.Sprintf("Downloading %s", name), total)
// io.Copy writes ~32 KiB per TeeReader call, so gate rendering on
// integer-percentage change to cap the bar at ≤101 renders instead of
// re-rendering tens of thousands of times.
var current uint64
lastPct := -1
reader := io.TeeReader(resp.Body, progressWriter(func(n int) {
bar.Add(n)
current += SafeIntToUint64(n)
pct := SafeUint64ToInt(current * 100 / total)
if pct > lastPct {
lastPct = pct
update(current)
}
}))
_, err = io.Copy(f, reader)
@@ -246,6 +254,35 @@ func (pw progressWriter) Write(p []byte) (int, error) {
return len(p), nil
}
// NewByteProgressBar starts a pterm progress bar that renders human-readable
// sizes in its title instead of raw byte counts, and returns an update func
// that performs one render per call. Callers must invoke update at a throttled
// cadence (integer-percentage change) — the render itself is expensive, so an
// unthrottled per-write call regresses throughput.
//
// pterm's elapsed-time display normally spawns a background goroutine that
// re-renders every second with no lock, tearing the line against our own
// low-frequency renders. Starting with it off means no timer is spawned; the
// flag is then set on the returned bar so pterm still appends its "| 9s"
// suffix, now emitted only on our (single) renders.
func NewByteProgressBar(title string, total uint64) (*pterm.ProgressbarPrinter, func(current uint64)) {
totalHuman := HumanBytes(total)
bar, _ := pterm.DefaultProgressbar.
WithTitle(title).
WithTotal(SafeUint64ToInt(total)).
WithShowCount(false). // hide raw ints; the size goes in the title
WithShowElapsedTime(false). // suppress the async re-render (see above)
Start()
bar.ShowElapsedTime = true // keep pterm's elapsed, rendered only by us
update := func(current uint64) {
bar.Current = SafeUint64ToInt(current) // drives fill + percentage
// UpdateTitle performs the single render for this step.
bar.UpdateTitle(fmt.Sprintf("%s — %s / %s", title, HumanBytes(current), totalHuman))
}
return bar, update
}
// HumanBytes formats a byte count using IEC binary suffixes (KiB, MiB, ...).
// Values below 1024 render as exact bytes.
func HumanBytes(b uint64) string {