Skip to content

Commit affe7ba

Browse files
committed
feat: improve push progress output with single-line refresh and transfer rate
Signed-off-by: jwcesign <jwcesign@gmail.com>
1 parent fc0e4c0 commit affe7ba

File tree

1 file changed

+139
-1
lines changed

1 file changed

+139
-1
lines changed

pkg/publish/default.go

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import (
2020
"fmt"
2121
"log"
2222
"net/http"
23+
"os"
2324
"path"
2425
"runtime"
2526
"strings"
27+
"time"
2628

2729
"github.com/google/go-containerregistry/pkg/authn"
2830
"github.com/google/go-containerregistry/pkg/name"
@@ -33,6 +35,7 @@ import (
3335
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
3436
"github.com/sigstore/cosign/v2/pkg/oci/walk"
3537
"golang.org/x/sync/errgroup"
38+
"golang.org/x/term"
3639

3740
"github.com/google/ko/pkg/build"
3841
)
@@ -47,6 +50,7 @@ type defalt struct {
4750
jobs int
4851

4952
pusher *remote.Pusher
53+
ropt []remote.Option
5054
oopt []ociremote.Option
5155
}
5256

@@ -115,6 +119,7 @@ func (do *defaultOpener) Open() (Interface, error) {
115119
insecure: do.insecure,
116120
jobs: do.jobs,
117121
pusher: pusher,
122+
ropt: do.ropt,
118123
oopt: oopt,
119124
}, nil
120125
}
@@ -156,7 +161,7 @@ func (d *defalt) pushResult(ctx context.Context, tag name.Tag, br build.Result)
156161
g.SetLimit(d.jobs)
157162

158163
g.Go(func() error {
159-
return d.pusher.Push(ctx, tag, br)
164+
return d.pushWithProgress(ctx, tag, br)
160165
})
161166

162167
// writePeripherals implements walk.Fn
@@ -222,6 +227,139 @@ func (d *defalt) pushResult(ctx context.Context, tag name.Tag, br build.Result)
222227
return g.Wait()
223228
}
224229

230+
func (d *defalt) pushWithProgress(ctx context.Context, ref name.Reference, target remote.Taggable) error {
231+
updates := make(chan v1.Update, 128)
232+
options := append([]remote.Option{}, d.ropt...)
233+
options = append(options, remote.WithProgress(updates), remote.WithContext(ctx))
234+
235+
errCh := make(chan error, 1)
236+
go func() {
237+
errCh <- remote.Push(ref, target, options...)
238+
}()
239+
240+
start := time.Now()
241+
lastPrint := start
242+
lastComplete := int64(0)
243+
total := int64(0)
244+
complete := int64(0)
245+
var pushErr error
246+
updatesOpen := true
247+
pushDone := false
248+
ticker := time.NewTicker(time.Second)
249+
defer ticker.Stop()
250+
inlineProgress := term.IsTerminal(int(os.Stderr.Fd()))
251+
printedInline := false
252+
lastInlineLen := 0
253+
254+
for updatesOpen || !pushDone {
255+
select {
256+
case <-ctx.Done():
257+
if inlineProgress && printedInline {
258+
fmt.Fprintln(os.Stderr)
259+
}
260+
return ctx.Err()
261+
case update, ok := <-updates:
262+
if !ok {
263+
updatesOpen = false
264+
continue
265+
}
266+
if update.Total > 0 {
267+
total = update.Total
268+
}
269+
if update.Complete > complete {
270+
complete = update.Complete
271+
}
272+
case pushErr = <-errCh:
273+
pushDone = true
274+
case now := <-ticker.C:
275+
if complete == 0 || complete == lastComplete {
276+
continue
277+
}
278+
rate := bytesPerSecond(complete-lastComplete, now.Sub(lastPrint))
279+
if inlineProgress {
280+
line := progressLineWithRef(inlineRefName(ref), complete, total, rate)
281+
lastInlineLen = printInlineProgress(line, lastInlineLen)
282+
printedInline = true
283+
} else {
284+
logProgress(ref, complete, total, rate)
285+
}
286+
lastComplete = complete
287+
lastPrint = now
288+
}
289+
}
290+
291+
if complete > 0 || total > 0 {
292+
elapsedRate := bytesPerSecond(complete, time.Since(start))
293+
if inlineProgress {
294+
line := progressLineWithRef(inlineRefName(ref), complete, total, elapsedRate)
295+
printInlineProgress(line, lastInlineLen)
296+
fmt.Fprintln(os.Stderr)
297+
} else {
298+
logProgress(ref, complete, total, elapsedRate)
299+
}
300+
} else if inlineProgress && printedInline {
301+
fmt.Fprintln(os.Stderr)
302+
}
303+
return pushErr
304+
}
305+
306+
func logProgress(ref name.Reference, complete, total int64, rate float64) {
307+
log.Printf("%s", progressLineWithRef(ref.Name(), complete, total, rate))
308+
}
309+
310+
func progressLineWithRef(refName string, complete, total int64, rate float64) string {
311+
if total > 0 {
312+
return fmt.Sprintf("Push progress %s: %.1f%% (%s/%s) at %s",
313+
refName,
314+
100*float64(complete)/float64(total),
315+
formatMiB(complete),
316+
formatMiB(total),
317+
formatRate(rate))
318+
}
319+
return fmt.Sprintf("Push progress %s: %s uploaded at %s",
320+
refName,
321+
formatMiB(complete),
322+
formatRate(rate))
323+
}
324+
325+
func printInlineProgress(line string, previousLen int) int {
326+
// Use CR + spaces to keep progress on a single line without ANSI reliance.
327+
padding := ""
328+
if previousLen > len(line) {
329+
padding = strings.Repeat(" ", previousLen-len(line))
330+
}
331+
fmt.Fprintf(os.Stderr, "\r%s%s", line, padding)
332+
return len(line)
333+
}
334+
335+
func inlineRefName(ref name.Reference) string {
336+
refName := ref.Name()
337+
// Keep inline output short to avoid terminal line wrapping.
338+
if slash := strings.LastIndex(refName, "/"); slash >= 0 && slash+1 < len(refName) {
339+
refName = refName[slash+1:]
340+
}
341+
const maxInlineRef = 64
342+
if len(refName) > maxInlineRef {
343+
refName = "..." + refName[len(refName)-(maxInlineRef-3):]
344+
}
345+
return refName
346+
}
347+
348+
func bytesPerSecond(bytes int64, d time.Duration) float64 {
349+
if d <= 0 {
350+
return 0
351+
}
352+
return float64(bytes) / d.Seconds()
353+
}
354+
355+
func formatMiB(bytes int64) string {
356+
return fmt.Sprintf("%.2fMiB", float64(bytes)/(1024*1024))
357+
}
358+
359+
func formatRate(bytesPerSecond float64) string {
360+
return fmt.Sprintf("%.2fMiB/s", bytesPerSecond/(1024*1024))
361+
}
362+
225363
// Publish implements publish.Interface
226364
func (d *defalt) Publish(ctx context.Context, br build.Result, s string) (name.Reference, error) {
227365
s = strings.TrimPrefix(s, build.StrictScheme)

0 commit comments

Comments
 (0)