diff --git a/cmd/spinifex/cmd/admin.go b/cmd/spinifex/cmd/admin.go index d4e2f5cbb..10b0dff31 100644 --- a/cmd/spinifex/cmd/admin.go +++ b/cmd/spinifex/cmd/admin.go @@ -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) diff --git a/go.mod b/go.mod index 3fa6e7664..7af8cba6c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 7f0888a55..704010017 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/spinifex/utils/conversion.go b/spinifex/utils/conversion.go index d488b7198..ecc5d446c 100644 --- a/spinifex/utils/conversion.go +++ b/spinifex/utils/conversion.go @@ -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) +} diff --git a/spinifex/utils/utils.go b/spinifex/utils/utils.go index 1e567e4ea..26b4bc402 100644 --- a/spinifex/utils/utils.go +++ b/spinifex/utils/utils.go @@ -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 {