mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
feat: render import progress bars in human-readable units (#581)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user