From b088f2b4c31aa6644ff57ffad5fffb5b2fe82164 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 12 Sep 2023 07:05:51 +0000 Subject: [PATCH 01/98] feat: support progressed output in blob push and blob get Signed-off-by: Billy Zha --- cmd/oras/internal/display/console/console.go | 84 ++++++++++ cmd/oras/internal/display/progress/manager.go | 140 ++++++++++++++++ cmd/oras/internal/display/progress/mark.go | 31 ++++ cmd/oras/internal/display/progress/status.go | 156 ++++++++++++++++++ cmd/oras/internal/display/track/reader.go | 99 +++++++++++ cmd/oras/internal/option/common.go | 19 ++- cmd/oras/root/blob/fetch.go | 64 ++++--- cmd/oras/root/blob/push.go | 51 ++++-- go.mod | 3 + go.sum | 7 + 10 files changed, 615 insertions(+), 39 deletions(-) create mode 100644 cmd/oras/internal/display/console/console.go create mode 100644 cmd/oras/internal/display/progress/manager.go create mode 100644 cmd/oras/internal/display/progress/mark.go create mode 100644 cmd/oras/internal/display/progress/status.go create mode 100644 cmd/oras/internal/display/track/reader.go diff --git a/cmd/oras/internal/display/console/console.go b/cmd/oras/internal/display/console/console.go new file mode 100644 index 000000000..be1be4fe6 --- /dev/null +++ b/cmd/oras/internal/display/console/console.go @@ -0,0 +1,84 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package console + +import ( + "fmt" + "os" + + "github.com/containerd/console" + "github.com/morikuni/aec" +) + +// Console is a wrapper around containerd's console.Console and ANSI escape +// codes. +type Console struct { + console.Console +} + +// Size returns the width and height of the console. +// If the console size cannot be determined, returns a default value of 80x10. +func (c *Console) Size() (width, height int) { + width = 80 + height = 10 + size, err := c.Console.Size() + if err == nil && size.Height > 0 && size.Width > 0 { + width = int(size.Width) + height = int(size.Height) + } + return +} + +// GetConsole returns a Console from a file. +func GetConsole(f *os.File) (*Console, error) { + c, err := console.ConsoleFromFile(f) + if err != nil { + return nil, err + } + return &Console{c}, nil +} + +// Save saves the current cursor position. +func (c *Console) Save() { + fmt.Fprint(c, aec.Hide) + // cannot use aec.Save since DEC has better compatilibity than SCO + fmt.Fprint(c, "\0337") +} + +// NewRow allocates a horizontal space to the output area with scroll if needed. +func (c *Console) NewRow() { + // cannot use aec.Restore since DEC has better compatilibity than SCO + fmt.Fprint(c, "\0338") + fmt.Fprint(c, "\n") + fmt.Fprint(c, "\0337") +} + +// OutputTo outputs a string to a specific line. +func (c *Console) OutputTo(upCnt uint, str string) { + fmt.Fprint(c, "\0338") + fmt.Fprint(c, aec.PreviousLine(upCnt)) + fmt.Fprint(c, str+" ") + fmt.Fprint(c, aec.EraseLine(aec.EraseModes.Tail)) +} + +// Restore restores the saved cursor position. +func (c *Console) Restore() { + // cannot use aec.Restore since DEC has better compatilibity than SCO + fmt.Fprint(c, "\0338") + fmt.Fprint(c, aec.Column(0)) + fmt.Fprint(c, aec.EraseLine(aec.EraseModes.All)) + fmt.Fprint(c, aec.Show) +} diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go new file mode 100644 index 000000000..64cca42a7 --- /dev/null +++ b/cmd/oras/internal/display/progress/manager.go @@ -0,0 +1,140 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package progress + +import ( + "os" + "sync" + "time" + + "oras.land/oras/cmd/oras/internal/display/console" +) + +const BUFFER_SIZE = 20 + +// Status is print message channel +type Status chan<- *status + +// Manager is progress view master +type Manager interface { + Add() Status + StopAndWait() +} + +const ( + bufFlushDuration = 100 * time.Millisecond +) + +type manager struct { + statuses []*status + + done chan struct{} + renderTick *time.Ticker + c *console.Console + updating sync.WaitGroup + sync.WaitGroup + mu sync.Mutex + close sync.Once +} + +// NewManager initialized a new progress manager. +func NewManager() (Manager, error) { + var m manager + var err error + + m.c, err = console.GetConsole(os.Stderr) + if err != nil { + return nil, err + } + m.done = make(chan struct{}) + m.renderTick = time.NewTicker(bufFlushDuration) + m.start() + return &m, nil +} + +func (m *manager) start() { + m.renderTick.Reset(bufFlushDuration) + m.c.Save() + go func() { + for { + m.render() + select { + case <-m.done: + return + case <-m.renderTick.C: + } + } + }() +} + +func (m *manager) render() { + m.mu.Lock() + defer m.mu.Unlock() + // todo: update size in another routine + width, height := m.c.Size() + len := len(m.statuses) * 2 + offset := 0 + if len > height { + // skip statuses that cannot be rendered + offset = len - height + } + + for ; offset < len; offset += 2 { + status, progress := m.statuses[offset/2].String(width) + m.c.OutputTo(uint(len-offset), status) + m.c.OutputTo(uint(len-offset-1), progress) + } +} + +// Add appends a new status with 2-line space for rendering. +func (m *manager) Add() Status { + m.mu.Lock() + defer m.mu.Unlock() + id := len(m.statuses) + m.statuses = append(m.statuses, nil) + defer m.c.NewRow() + defer m.c.NewRow() + return m.newStatus(id) +} + +func (m *manager) newStatus(id int) Status { + ch := make(chan *status, BUFFER_SIZE) + m.updating.Add(1) + go m.update(ch, id) + return ch +} + +func (m *manager) update(ch chan *status, id int) { + defer m.updating.Done() + for s := range ch { + m.statuses[id] = m.statuses[id].Update(s) + } +} + +// StopAndWait stops all status and waits for updating and rendering. +func (m *manager) StopAndWait() { + // 1. stop periodic render + m.renderTick.Stop() + close(m.done) + defer m.close.Do(func() { + // 4. restore cursor, mark done + m.c.Restore() + }) + // 2. wait for all model update done + m.updating.Wait() + // 3. render last model + m.render() +} diff --git a/cmd/oras/internal/display/progress/mark.go b/cmd/oras/internal/display/progress/mark.go new file mode 100644 index 000000000..ce6988a96 --- /dev/null +++ b/cmd/oras/internal/display/progress/mark.go @@ -0,0 +1,31 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package progress + +var ( + spinner = []rune("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") + spinnerLen = len(spinner) + spinnerPos = 0 +) + +// GetMark returns the rune of status mark. +func GetMark(s *status) rune { + if s.done { + return '√' + } + spinnerPos = (spinnerPos + 1) % spinnerLen + return spinner[spinnerPos/2] +} diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go new file mode 100644 index 000000000..5d6d89b06 --- /dev/null +++ b/cmd/oras/internal/display/progress/status.go @@ -0,0 +1,156 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package progress + +import ( + "fmt" + "strings" + "time" + "unicode/utf8" + + "github.com/dustin/go-humanize" + "github.com/morikuni/aec" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +const BarMaxLength = 40 + +// status is used as message to update progress view. +type status struct { + done bool + prompt string + descriptor ocispec.Descriptor + offset int64 + startTime *time.Time + endTime *time.Time +} + +// NewStatus generates a status. +func NewStatus(prompt string, descriptor ocispec.Descriptor, offset uint64) *status { + return &status{ + prompt: prompt, + descriptor: descriptor, + offset: int64(offset), + } +} + +// StartTiming starts timing. +func StartTiming() *status { + now := time.Now() + return &status{ + offset: -1, + startTime: &now, + } +} + +// EndTiming ends timing and set status to done. +func EndTiming() *status { + now := time.Now() + return &status{ + offset: -1, + endTime: &now, + } +} + +// String returns human-readable TTY strings of the status. +func (s *status) String(width int) (string, string) { + if s == nil { + return "loading status...", "loading progress..." + } + // todo: doesn't support multiline prompt + total := uint64(s.descriptor.Size) + percent := float64(s.offset) / float64(total) + + name := s.descriptor.Annotations["org.opencontainers.image.title"] + if name == "" { + name = s.descriptor.MediaType + } + + // format: [left-------------------------------][margin][right-----------------------------] + // mark(1) bar(42) action(<10) name(126) size_per_size(19) percent(8) time(8) + // └─ digest(72) + var left string + var lenLeft int + if !s.done { + lenBar := int(percent * BarMaxLength) + bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", BarMaxLength-lenBar)) + left = fmt.Sprintf("%c %s %s %s", GetMark(s), bar, s.prompt, name) + // bar + wrapper(2) + space(1) + lenLeft = BarMaxLength + 2 + 1 + } else { + left = fmt.Sprintf("%c %s %s", GetMark(s), s.prompt, name) + } + // mark(1) + space(1) + prompt+ space(1) + name + lenLeft += 1 + 1 + utf8.RuneCountInString(s.prompt) + 1 + utf8.RuneCountInString(name) + + right := fmt.Sprintf(" %s/%s %6.2f%% %s", humanize.Bytes(uint64(s.offset)), humanize.Bytes(total), percent*100, s.DurationString()) + lenRight := utf8.RuneCountInString(right) + lenMargin := width - lenLeft - lenRight + if lenMargin < 0 { + // hide partial name with one space left + left = left[:len(left)+lenMargin-1] + "." + lenMargin = 0 + } + return fmt.Sprintf("%s%s%s", left, strings.Repeat(" ", lenMargin), right), fmt.Sprintf(" └─ %s", s.descriptor.Digest.String()) +} + +// DurationString returns a viewable TTY string of the status with duration. +func (s *status) DurationString() string { + if s.startTime == nil { + return "0ms" + } + + var d time.Duration + if s.endTime == nil { + d = time.Since(*s.startTime) + } else { + d = s.endTime.Sub(*s.startTime) + } + + switch { + case d > time.Minute: + d = d.Round(time.Second) + case d > time.Second: + d = d.Round(100 * time.Millisecond) + case d > time.Millisecond: + d = d.Round(time.Millisecond) + default: + d = d.Round(10 * time.Nanosecond) + } + return d.String() +} + +// Update updates a status. +func (s *status) Update(new *status) *status { + if s == nil { + s = &status{} + } + if new.offset > 0 { + s.descriptor = new.descriptor + s.offset = new.offset + } + if new.prompt != "" { + s.prompt = new.prompt + } + if new.startTime != nil { + s.startTime = new.startTime + } + if new.endTime != nil { + s.endTime = new.endTime + s.done = true + } + return s +} diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go new file mode 100644 index 000000000..959250ae6 --- /dev/null +++ b/cmd/oras/internal/display/track/reader.go @@ -0,0 +1,99 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package track + +import ( + "io" + "sync" + "sync/atomic" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/progress" +) + +type reader struct { + base io.Reader + offset atomic.Uint64 + actionPrompt string + donePrompt string + descriptor ocispec.Descriptor + mu sync.Mutex + m progress.Manager + ch progress.Status + once sync.Once +} + +// NewReader returns a new reader with tracked progress. +func NewReader(r io.Reader, descriptor ocispec.Descriptor, actionPrompt string, donePrompt string) (*reader, error) { + manager, err := progress.NewManager() + if err != nil { + return nil, err + } + return managedReader(r, descriptor, manager, actionPrompt, donePrompt) +} + +func managedReader(r io.Reader, descriptor ocispec.Descriptor, manager progress.Manager, actionPrompt string, donePrompt string) (*reader, error) { + return &reader{ + base: r, + descriptor: descriptor, + actionPrompt: actionPrompt, + donePrompt: donePrompt, + m: manager, + ch: manager.Add(), + }, nil +} + +// End closes the status channel. +func (r *reader) End() { + defer close(r.ch) + r.ch <- progress.NewStatus(r.donePrompt, r.descriptor, uint64(r.descriptor.Size)) + r.ch <- progress.EndTiming() +} + +// Stop stops the status channel and related manager. +func (r *reader) Stop() { + r.End() + r.m.StopAndWait() +} + +func (r *reader) Read(p []byte) (int, error) { + r.once.Do(func() { + r.ch <- progress.StartTiming() + }) + n, err := r.base.Read(p) + if err != nil && err != io.EOF { + return n, err + } + + offset := r.offset.Add(uint64(n)) + if err == io.EOF { + if offset != uint64(r.descriptor.Size) { + return n, io.ErrUnexpectedEOF + } + r.mu.Lock() + defer r.mu.Unlock() + r.ch <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) + } + + if r.mu.TryLock() { + defer r.mu.Unlock() + if len(r.ch) < progress.BUFFER_SIZE { + // intermediate progress might be ignored if buffer is full + r.ch <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) + } + } + return n, err +} diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 6c5817967..f47a46457 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -17,25 +17,40 @@ package option import ( "context" + "os" "github.com/sirupsen/logrus" "github.com/spf13/pflag" + "golang.org/x/term" "oras.land/oras/internal/trace" ) // Common option struct. type Common struct { - Debug bool - Verbose bool + Debug bool + Verbose bool + UseTTY bool + avoidTTY bool } // ApplyFlags applies flags to a command flag set. func (opts *Common) ApplyFlags(fs *pflag.FlagSet) { fs.BoolVarP(&opts.Debug, "debug", "d", false, "debug mode") fs.BoolVarP(&opts.Verbose, "verbose", "v", false, "verbose output") + fs.BoolVarP(&opts.avoidTTY, "noTTY", "", false, "[Preview] avoid using stdout as a terminal") } // WithContext returns a new FieldLogger and an associated Context derived from ctx. func (opts *Common) WithContext(ctx context.Context) (context.Context, logrus.FieldLogger) { return trace.NewLogger(ctx, opts.Debug, opts.Verbose) } + +// Parse gets target options from user input. +func (opts *Common) Parse() error { + if opts.avoidTTY { + opts.UseTTY = false + } else { + opts.UseTTY = term.IsTerminal(int(os.Stderr.Fd())) + } + return nil +} diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index d43ae382d..aa8ed8288 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -28,6 +28,7 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry/remote" + "oras.land/oras/cmd/oras/internal/display/track" "oras.land/oras/cmd/oras/internal/option" ) @@ -108,20 +109,39 @@ func fetchBlob(ctx context.Context, opts fetchBlobOptions) (fetchErr error) { if err != nil { return err } + desc, err := opts.doFetch(ctx, src) + if err != nil { + return err + } - var desc ocispec.Descriptor + // outputs blob's descriptor if `--descriptor` is used + if opts.OutputDescriptor { + descJSON, err := opts.Marshal(desc) + if err != nil { + return err + } + if err := opts.Output(os.Stdout, descJSON); err != nil { + return err + } + } + + return nil +} + +func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarget) (desc ocispec.Descriptor, fetchErr error) { + var err error if opts.outputPath == "" { // fetch blob descriptor only desc, err = oras.Resolve(ctx, src, opts.Reference, oras.DefaultResolveOptions) if err != nil { - return err + return ocispec.Descriptor{}, err } } else { // fetch blob content var rc io.ReadCloser desc, rc, err = oras.Fetch(ctx, src, opts.Reference, oras.DefaultFetchOptions) if err != nil { - return err + return ocispec.Descriptor{}, err } defer rc.Close() vr := content.NewVerifyReader(rc, desc) @@ -129,15 +149,27 @@ func fetchBlob(ctx context.Context, opts fetchBlobOptions) (fetchErr error) { // outputs blob content if "--output -" is used if opts.outputPath == "-" { if _, err := io.Copy(os.Stdout, vr); err != nil { - return err + return ocispec.Descriptor{}, err } - return vr.Verify() + if err := vr.Verify(); err != nil { + return ocispec.Descriptor{}, err + } + return desc, nil + } + var r io.Reader = vr + if opts.UseTTY { + trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ") + if err != nil { + return ocispec.Descriptor{}, err + } + defer trackedReader.Stop() + r = trackedReader } // save blob content into the local file if the output path is provided file, err := os.Create(opts.outputPath) if err != nil { - return err + return ocispec.Descriptor{}, err } defer func() { if err := file.Close(); fetchErr == nil { @@ -145,24 +177,12 @@ func fetchBlob(ctx context.Context, opts fetchBlobOptions) (fetchErr error) { } }() - if _, err := io.Copy(file, vr); err != nil { - return err + if _, err := io.Copy(file, r); err != nil { + return ocispec.Descriptor{}, err } if err := vr.Verify(); err != nil { - return err + return ocispec.Descriptor{}, err } } - - // outputs blob's descriptor if `--descriptor` is used - if opts.OutputDescriptor { - descJSON, err := opts.Marshal(desc) - if err != nil { - return err - } - if err := opts.Output(os.Stdout, descJSON); err != nil { - return err - } - } - - return nil + return desc, nil } diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index ea3bffaee..93b2ab2d0 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -19,11 +19,14 @@ import ( "context" "errors" "fmt" + "io" "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" + "oras.land/oras-go/v2" "oras.land/oras/cmd/oras/internal/display" + "oras.land/oras/cmd/oras/internal/display/track" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/file" ) @@ -98,7 +101,7 @@ Example - Push blob 'hi.txt' into an OCI image layout folder 'layout-dir': func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { ctx, logger := opts.WithContext(ctx) - repo, err := opts.NewTarget(opts.Common, logger) + target, err := opts.NewTarget(opts.Common, logger) if err != nil { return err } @@ -110,27 +113,19 @@ func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { } defer rc.Close() - exists, err := repo.Exists(ctx, desc) + exists, err := target.Exists(ctx, desc) if err != nil { return err } verbose := opts.Verbose && !opts.OutputDescriptor if exists { - if err := display.PrintStatus(desc, "Exists", verbose); err != nil { - return err - } + err = display.PrintStatus(desc, "Exists", verbose) } else { - if err := display.PrintStatus(desc, "Uploading", verbose); err != nil { - return err - } - if err = repo.Push(ctx, desc, rc); err != nil { - return err - } - if err := display.PrintStatus(desc, "Uploaded ", verbose); err != nil { - return err - } + err = opts.doPush(ctx, target, desc, rc) + } + if err != nil { + return err } - // outputs blob's descriptor if opts.OutputDescriptor { descJSON, err := opts.Marshal(desc) @@ -145,3 +140,29 @@ func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { return nil } + +// doPush pushes a blob to a registry or an OCI image layout +func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc ocispec.Descriptor, r io.Reader) error { + if !opts.UseTTY { + if err := display.PrintStatus(desc, "Uploading", opts.Verbose); err != nil { + return err + } + } + if opts.UseTTY { + trackedReader, err := track.NewReader(r, desc, "Uploading", "Uploaded ") + if err != nil { + return err + } + defer trackedReader.Stop() + r = trackedReader + } + if err := t.Push(ctx, desc, r); err != nil { + return err + } + if !opts.UseTTY { + if err := display.PrintStatus(desc, "Uploaded ", opts.Verbose); err != nil { + return err + } + } + return nil +} diff --git a/go.mod b/go.mod index b2f5c1eb3..f605ff7d7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module oras.land/oras go 1.21 require ( + github.com/containerd/console v1.0.3 + github.com/dustin/go-humanize v1.0.1 + github.com/morikuni/aec v1.0.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc4 github.com/oras-project/oras-credentials-go v0.2.0 diff --git a/go.sum b/go.sum index f85d8c6eb..a0bd9172f 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,15 @@ +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= @@ -24,6 +30,7 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From e7734719f2227f9cb35498396ac006e3ce37cb36 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 19 Sep 2023 08:20:02 +0000 Subject: [PATCH 02/98] add error when --debug is used with terminal output Signed-off-by: Billy Zha --- cmd/oras/internal/option/common.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index f47a46457..2354727a5 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -17,6 +17,7 @@ package option import ( "context" + "errors" "os" "github.com/sirupsen/logrus" @@ -50,6 +51,9 @@ func (opts *Common) Parse() error { if opts.avoidTTY { opts.UseTTY = false } else { + if opts.Debug { + return errors.New("cannot use --debug, add --noTTY to suppress terminal output") + } opts.UseTTY = term.IsTerminal(int(os.Stderr.Fd())) } return nil From 60445947cb2777a2564d4f0ea0d0f863df8247f1 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 19 Sep 2023 08:24:39 +0000 Subject: [PATCH 03/98] bug fix Signed-off-by: Billy Zha --- cmd/oras/internal/option/common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 2354727a5..ef0f9ddcb 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -51,10 +51,10 @@ func (opts *Common) Parse() error { if opts.avoidTTY { opts.UseTTY = false } else { - if opts.Debug { + opts.UseTTY = term.IsTerminal(int(os.Stderr.Fd())) + if opts.UseTTY && opts.Debug { return errors.New("cannot use --debug, add --noTTY to suppress terminal output") } - opts.UseTTY = term.IsTerminal(int(os.Stderr.Fd())) } return nil } From 6324b93cc9cb0a9da94d9b85ac50d1c331b56f04 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 20 Sep 2023 05:58:13 +0000 Subject: [PATCH 04/98] add test for console Signed-off-by: Billy Zha --- cmd/oras/internal/display/console/console.go | 12 +++- .../internal/display/console/console_test.go | 61 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 cmd/oras/internal/display/console/console_test.go diff --git a/cmd/oras/internal/display/console/console.go b/cmd/oras/internal/display/console/console.go index be1be4fe6..d24bc9b5d 100644 --- a/cmd/oras/internal/display/console/console.go +++ b/cmd/oras/internal/display/console/console.go @@ -23,6 +23,12 @@ import ( "github.com/morikuni/aec" ) +// MinialWidth is the minimal width of supported console. +const MinialWidth = 80 + +// MinialHeight is the minimal height of supported console. +const MinialHeight = 10 + // Console is a wrapper around containerd's console.Console and ANSI escape // codes. type Console struct { @@ -32,10 +38,10 @@ type Console struct { // Size returns the width and height of the console. // If the console size cannot be determined, returns a default value of 80x10. func (c *Console) Size() (width, height int) { - width = 80 - height = 10 + width = MinialWidth + height = MinialHeight size, err := c.Console.Size() - if err == nil && size.Height > 0 && size.Width > 0 { + if err == nil && size.Height > MinialHeight && size.Width > MinialWidth { width = int(size.Width) height = int(size.Height) } diff --git a/cmd/oras/internal/display/console/console_test.go b/cmd/oras/internal/display/console/console_test.go new file mode 100644 index 000000000..0f807afe1 --- /dev/null +++ b/cmd/oras/internal/display/console/console_test.go @@ -0,0 +1,61 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package console + +import ( + "testing" + + "github.com/containerd/console" +) + +func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) { + t.Helper() + if gotWidth != wantWidth { + t.Errorf("Console.Size() gotWidth = %v, want %v", gotWidth, wantWidth) + } + if gotHeight != wantHeight { + t.Errorf("Console.Size() gotHeight = %v, want %v", gotHeight, wantHeight) + } +} + +func TestConsole_Size(t *testing.T) { + pty, _, err := console.NewPty() + if err != nil { + t.Fatal(err) + } + c := &Console{ + Console: pty, + } + + // minimal width and height + gotWidth, gotHeight := c.Size() + validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) + + // zero width + pty.Resize(console.WinSize{Width: 0, Height: MinialHeight}) + gotWidth, gotHeight = c.Size() + validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) + + // zero height + pty.Resize(console.WinSize{Width: MinialWidth, Height: 0}) + gotWidth, gotHeight = c.Size() + validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) + + // valid zero and height + pty.Resize(console.WinSize{Width: 200, Height: 100}) + gotWidth, gotHeight = c.Size() + validateSize(t, gotWidth, gotHeight, 200, 100) +} From f1077bd62a163a22f07dc255d7b7320092f9e3da Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 20 Sep 2023 06:40:44 +0000 Subject: [PATCH 05/98] fix lint & pass tty to manager Signed-off-by: Billy Zha --- .../internal/display/console/console_test.go | 6 +++--- cmd/oras/internal/display/progress/manager.go | 9 ++++----- cmd/oras/internal/display/track/reader.go | 5 +++-- cmd/oras/internal/option/common.go | 16 ++++++++-------- cmd/oras/root/blob/fetch.go | 4 ++-- cmd/oras/root/blob/push.go | 9 ++++----- 6 files changed, 24 insertions(+), 25 deletions(-) diff --git a/cmd/oras/internal/display/console/console_test.go b/cmd/oras/internal/display/console/console_test.go index 0f807afe1..618705a95 100644 --- a/cmd/oras/internal/display/console/console_test.go +++ b/cmd/oras/internal/display/console/console_test.go @@ -45,17 +45,17 @@ func TestConsole_Size(t *testing.T) { validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) // zero width - pty.Resize(console.WinSize{Width: 0, Height: MinialHeight}) + _ = pty.Resize(console.WinSize{Width: 0, Height: MinialHeight}) gotWidth, gotHeight = c.Size() validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) // zero height - pty.Resize(console.WinSize{Width: MinialWidth, Height: 0}) + _ = pty.Resize(console.WinSize{Width: MinialWidth, Height: 0}) gotWidth, gotHeight = c.Size() validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) // valid zero and height - pty.Resize(console.WinSize{Width: 200, Height: 100}) + _ = pty.Resize(console.WinSize{Width: 200, Height: 100}) gotWidth, gotHeight = c.Size() validateSize(t, gotWidth, gotHeight, 200, 100) } diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index 64cca42a7..ee64fdcf2 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -45,17 +45,16 @@ type manager struct { renderTick *time.Ticker c *console.Console updating sync.WaitGroup - sync.WaitGroup - mu sync.Mutex - close sync.Once + mu sync.Mutex + close sync.Once } // NewManager initialized a new progress manager. -func NewManager() (Manager, error) { +func NewManager(f *os.File) (Manager, error) { var m manager var err error - m.c, err = console.GetConsole(os.Stderr) + m.c, err = console.GetConsole(f) if err != nil { return nil, err } diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 959250ae6..4b2e38e17 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -17,6 +17,7 @@ package track import ( "io" + "os" "sync" "sync/atomic" @@ -37,8 +38,8 @@ type reader struct { } // NewReader returns a new reader with tracked progress. -func NewReader(r io.Reader, descriptor ocispec.Descriptor, actionPrompt string, donePrompt string) (*reader, error) { - manager, err := progress.NewManager() +func NewReader(r io.Reader, descriptor ocispec.Descriptor, actionPrompt string, donePrompt string, tty *os.File) (*reader, error) { + manager, err := progress.NewManager(tty) if err != nil { return nil, err } diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index ef0f9ddcb..6fbd8878e 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -28,9 +28,10 @@ import ( // Common option struct. type Common struct { - Debug bool - Verbose bool - UseTTY bool + Debug bool + Verbose bool + TTY *os.File + avoidTTY bool } @@ -48,13 +49,12 @@ func (opts *Common) WithContext(ctx context.Context) (context.Context, logrus.Fi // Parse gets target options from user input. func (opts *Common) Parse() error { - if opts.avoidTTY { - opts.UseTTY = false - } else { - opts.UseTTY = term.IsTerminal(int(os.Stderr.Fd())) - if opts.UseTTY && opts.Debug { + f := os.Stderr + if !opts.avoidTTY && term.IsTerminal(int(f.Fd())) { + if opts.Debug { return errors.New("cannot use --debug, add --noTTY to suppress terminal output") } + opts.TTY = f } return nil } diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index aa8ed8288..fc4e18058 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -157,8 +157,8 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg return desc, nil } var r io.Reader = vr - if opts.UseTTY { - trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ") + if opts.TTY != nil { + trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ", opts.TTY) if err != nil { return ocispec.Descriptor{}, err } diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 93b2ab2d0..60d58c866 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -143,13 +143,12 @@ func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { // doPush pushes a blob to a registry or an OCI image layout func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc ocispec.Descriptor, r io.Reader) error { - if !opts.UseTTY { + if opts.TTY == nil { if err := display.PrintStatus(desc, "Uploading", opts.Verbose); err != nil { return err } - } - if opts.UseTTY { - trackedReader, err := track.NewReader(r, desc, "Uploading", "Uploaded ") + } else { + trackedReader, err := track.NewReader(r, desc, "Uploading", "Uploaded ", opts.TTY) if err != nil { return err } @@ -159,7 +158,7 @@ func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc oci if err := t.Push(ctx, desc, r); err != nil { return err } - if !opts.UseTTY { + if opts.TTY == nil { if err := display.PrintStatus(desc, "Uploaded ", opts.Verbose); err != nil { return err } From 941188f34e80033ddd0c29950ee71ce2eb8d45b0 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 20 Sep 2023 06:43:48 +0000 Subject: [PATCH 06/98] rename flag to disable tty Signed-off-by: Billy Zha --- cmd/oras/internal/option/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 6fbd8878e..69678546b 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -39,7 +39,7 @@ type Common struct { func (opts *Common) ApplyFlags(fs *pflag.FlagSet) { fs.BoolVarP(&opts.Debug, "debug", "d", false, "debug mode") fs.BoolVarP(&opts.Verbose, "verbose", "v", false, "verbose output") - fs.BoolVarP(&opts.avoidTTY, "noTTY", "", false, "[Preview] avoid using stdout as a terminal") + fs.BoolVarP(&opts.avoidTTY, "no-tty", "", false, "[Preview] avoid using stdout as a terminal") } // WithContext returns a new FieldLogger and an associated Context derived from ctx. From c72ebe5e75039b77f3f0898c1137c3d4b3570e98 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 21 Sep 2023 03:19:35 +0000 Subject: [PATCH 07/98] add test for blob push Signed-off-by: Billy Zha --- cmd/oras/root/blob/push_test.go | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 cmd/oras/root/blob/push_test.go diff --git a/cmd/oras/root/blob/push_test.go b/cmd/oras/root/blob/push_test.go new file mode 100644 index 000000000..ab8c7752d --- /dev/null +++ b/cmd/oras/root/blob/push_test.go @@ -0,0 +1,87 @@ +//go:build linux || zos || freebsd +// +build linux zos freebsd + +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package blob + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "strings" + "sync" + "testing" + + "github.com/containerd/console" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content/memory" +) + +func Test_pushBlobOptions_doPush(t *testing.T) { + // prepare + pty, slavePath, err := console.NewPty() + if err != nil { + t.Fatal(err) + } + slave, err := os.OpenFile(slavePath, os.O_RDWR, 0) + if err != nil { + t.Fatal(err) + } + defer slave.Close() + src := memory.New() + content := []byte("test") + r := bytes.NewReader(content) + desc := ocispec.Descriptor{ + MediaType: "application/octet-stream", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + var opts pushBlobOptions + opts.Common.TTY = slave + // test + err = opts.doPush(context.Background(), src, desc, r) + if err != nil { + t.Fatal(err) + } + // validate + var wg sync.WaitGroup + wg.Add(1) + var buffer bytes.Buffer + go func() { + defer wg.Done() + _, _ = io.Copy(&buffer, pty) + }() + slave.Close() + wg.Wait() + if err := orderedMatch(t, buffer.String(), "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + t.Fatal(err) + } +} + +func orderedMatch(t *testing.T, actual string, expected ...string) error { + for _, e := range expected { + i := strings.Index(actual, e) + if i < 0 { + return fmt.Errorf("expected to find %q in %q", e, actual) + } + actual = actual[i+len(e):] + } + return nil +} From 5fc75a83f6f215651e09bebbee2d9a3d09053f85 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 21 Sep 2023 05:47:38 +0000 Subject: [PATCH 08/98] fix render racing Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index ee64fdcf2..c0f500df7 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -39,14 +39,17 @@ const ( ) type manager struct { - statuses []*status - - done chan struct{} + statuses []*status + rwLock sync.RWMutex renderTick *time.Ticker c *console.Console updating sync.WaitGroup mu sync.Mutex close sync.Once + // done used to stop render routine + // doneDone used to mark render routine stopped + done chan struct{} + doneDone chan struct{} } // NewManager initialized a new progress manager. @@ -59,6 +62,7 @@ func NewManager(f *os.File) (Manager, error) { return nil, err } m.done = make(chan struct{}) + m.doneDone = make(chan struct{}) m.renderTick = time.NewTicker(bufFlushDuration) m.start() return &m, nil @@ -72,6 +76,7 @@ func (m *manager) start() { m.render() select { case <-m.done: + close(m.doneDone) return case <-m.renderTick.C: } @@ -92,7 +97,9 @@ func (m *manager) render() { } for ; offset < len; offset += 2 { + m.rwLock.RLock() status, progress := m.statuses[offset/2].String(width) + m.rwLock.RUnlock() m.c.OutputTo(uint(len-offset), status) m.c.OutputTo(uint(len-offset-1), progress) } @@ -119,7 +126,10 @@ func (m *manager) newStatus(id int) Status { func (m *manager) update(ch chan *status, id int) { defer m.updating.Done() for s := range ch { - m.statuses[id] = m.statuses[id].Update(s) + n := m.statuses[id].Update(s) + m.rwLock.Lock() + m.statuses[id] = n + m.rwLock.Unlock() } } @@ -135,5 +145,6 @@ func (m *manager) StopAndWait() { // 2. wait for all model update done m.updating.Wait() // 3. render last model + <-m.doneDone m.render() } From a2d3f8c87b69583dc2b859464039fb559b0c5cca Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 21 Sep 2023 06:21:13 +0000 Subject: [PATCH 09/98] fix race Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index c0f500df7..e0247e5dd 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -126,9 +126,8 @@ func (m *manager) newStatus(id int) Status { func (m *manager) update(ch chan *status, id int) { defer m.updating.Done() for s := range ch { - n := m.statuses[id].Update(s) m.rwLock.Lock() - m.statuses[id] = n + m.statuses[id] = m.statuses[id].Update(s) m.rwLock.Unlock() } } From 4a065fe1da972650c9adda39fa5fceae9bdcd34f Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 21 Sep 2023 08:26:37 +0000 Subject: [PATCH 10/98] add test for `blob fetch` Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch_test.go | 82 ++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 cmd/oras/root/blob/fetch_test.go diff --git a/cmd/oras/root/blob/fetch_test.go b/cmd/oras/root/blob/fetch_test.go new file mode 100644 index 000000000..a3247eb8a --- /dev/null +++ b/cmd/oras/root/blob/fetch_test.go @@ -0,0 +1,82 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package blob + +import ( + "bytes" + "context" + "io" + "os" + "sync" + "testing" + + "github.com/containerd/console" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content/memory" +) + +func Test_fetchBlobOptions_doFetch(t *testing.T) { + // prepare + pty, slavePath, err := console.NewPty() + if err != nil { + t.Fatal(err) + } + slave, err := os.OpenFile(slavePath, os.O_RDWR, 0) + if err != nil { + t.Fatal(err) + } + defer slave.Close() + src := memory.New() + content := []byte("test") + r := bytes.NewReader(content) + desc := ocispec.Descriptor{ + MediaType: "application/octet-stream", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + tag := "blob" + ctx := context.Background() + if err := src.Push(ctx, desc, r); err != nil { + t.Fatal(err) + } + if err := src.Tag(ctx, desc, tag); err != nil { + t.Fatal(err) + } + + var opts fetchBlobOptions + opts.Reference = tag + opts.Common.TTY = slave + opts.outputPath = t.TempDir() + "/test" + // test + _, err = opts.doFetch(ctx, src) + if err != nil { + t.Fatal(err) + } + // validate + var wg sync.WaitGroup + wg.Add(1) + var buffer bytes.Buffer + go func() { + defer wg.Done() + _, _ = io.Copy(&buffer, pty) + }() + slave.Close() + wg.Wait() + if err := orderedMatch(t, buffer.String(), "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + t.Fatal(err) + } +} From 00ca2fc54b0cbdcf10cb611e9ed7606555b4bbe7 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 22 Sep 2023 07:17:30 +0000 Subject: [PATCH 11/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/console/console.go | 26 +++++++++++-------- .../internal/display/console/console_test.go | 10 +++---- cmd/oras/internal/display/progress/manager.go | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/cmd/oras/internal/display/console/console.go b/cmd/oras/internal/display/console/console.go index d24bc9b5d..a22e0980c 100644 --- a/cmd/oras/internal/display/console/console.go +++ b/cmd/oras/internal/display/console/console.go @@ -23,11 +23,11 @@ import ( "github.com/morikuni/aec" ) -// MinialWidth is the minimal width of supported console. -const MinialWidth = 80 +// MinWidth is the minimal width of supported console. +const MinWidth = 80 -// MinialHeight is the minimal height of supported console. -const MinialHeight = 10 +// MinHeight is the minimal height of supported console. +const MinHeight = 10 // Console is a wrapper around containerd's console.Console and ANSI escape // codes. @@ -38,18 +38,22 @@ type Console struct { // Size returns the width and height of the console. // If the console size cannot be determined, returns a default value of 80x10. func (c *Console) Size() (width, height int) { - width = MinialWidth - height = MinialHeight + width = MinWidth + height = MinHeight size, err := c.Console.Size() - if err == nil && size.Height > MinialHeight && size.Width > MinialWidth { - width = int(size.Width) - height = int(size.Height) + if err == nil { + if size.Height > MinHeight { + height = int(size.Height) + } + if size.Width > MinWidth { + width = int(size.Width) + } } return } -// GetConsole returns a Console from a file. -func GetConsole(f *os.File) (*Console, error) { +// New generates a Console from a file. +func New(f *os.File) (*Console, error) { c, err := console.ConsoleFromFile(f) if err != nil { return nil, err diff --git a/cmd/oras/internal/display/console/console_test.go b/cmd/oras/internal/display/console/console_test.go index 618705a95..0338dbf1e 100644 --- a/cmd/oras/internal/display/console/console_test.go +++ b/cmd/oras/internal/display/console/console_test.go @@ -42,17 +42,17 @@ func TestConsole_Size(t *testing.T) { // minimal width and height gotWidth, gotHeight := c.Size() - validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) + validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero width - _ = pty.Resize(console.WinSize{Width: 0, Height: MinialHeight}) + _ = pty.Resize(console.WinSize{Width: 0, Height: MinHeight}) gotWidth, gotHeight = c.Size() - validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) + validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero height - _ = pty.Resize(console.WinSize{Width: MinialWidth, Height: 0}) + _ = pty.Resize(console.WinSize{Width: MinWidth, Height: 0}) gotWidth, gotHeight = c.Size() - validateSize(t, gotWidth, gotHeight, MinialWidth, MinialHeight) + validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // valid zero and height _ = pty.Resize(console.WinSize{Width: 200, Height: 100}) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index e0247e5dd..608612b14 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -57,7 +57,7 @@ func NewManager(f *os.File) (Manager, error) { var m manager var err error - m.c, err = console.GetConsole(f) + m.c, err = console.New(f) if err != nil { return nil, err } From c85e8097ba5b8f9177ebb9315a4824ded6d4d749 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 22 Sep 2023 07:41:40 +0000 Subject: [PATCH 12/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/console/console.go | 3 ++- cmd/oras/internal/display/progress/manager.go | 22 +++++++++---------- cmd/oras/internal/display/track/reader.go | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/cmd/oras/internal/display/console/console.go b/cmd/oras/internal/display/console/console.go index a22e0980c..a0aa3199b 100644 --- a/cmd/oras/internal/display/console/console.go +++ b/cmd/oras/internal/display/console/console.go @@ -80,7 +80,8 @@ func (c *Console) NewRow() { func (c *Console) OutputTo(upCnt uint, str string) { fmt.Fprint(c, "\0338") fmt.Fprint(c, aec.PreviousLine(upCnt)) - fmt.Fprint(c, str+" ") + fmt.Fprint(c, str) + fmt.Fprint(c, " ") fmt.Fprint(c, aec.EraseLine(aec.EraseModes.Tail)) } diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index 608612b14..7f753b4c1 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -23,7 +23,7 @@ import ( "oras.land/oras/cmd/oras/internal/display/console" ) -const BUFFER_SIZE = 20 +const BufferSize = 20 // Status is print message channel type Status chan<- *status @@ -42,7 +42,7 @@ type manager struct { statuses []*status rwLock sync.RWMutex renderTick *time.Ticker - c *console.Console + console *console.Console updating sync.WaitGroup mu sync.Mutex close sync.Once @@ -57,7 +57,7 @@ func NewManager(f *os.File) (Manager, error) { var m manager var err error - m.c, err = console.New(f) + m.console, err = console.New(f) if err != nil { return nil, err } @@ -70,7 +70,7 @@ func NewManager(f *os.File) (Manager, error) { func (m *manager) start() { m.renderTick.Reset(bufFlushDuration) - m.c.Save() + m.console.Save() go func() { for { m.render() @@ -88,7 +88,7 @@ func (m *manager) render() { m.mu.Lock() defer m.mu.Unlock() // todo: update size in another routine - width, height := m.c.Size() + width, height := m.console.Size() len := len(m.statuses) * 2 offset := 0 if len > height { @@ -100,8 +100,8 @@ func (m *manager) render() { m.rwLock.RLock() status, progress := m.statuses[offset/2].String(width) m.rwLock.RUnlock() - m.c.OutputTo(uint(len-offset), status) - m.c.OutputTo(uint(len-offset-1), progress) + m.console.OutputTo(uint(len-offset), status) + m.console.OutputTo(uint(len-offset-1), progress) } } @@ -111,13 +111,13 @@ func (m *manager) Add() Status { defer m.mu.Unlock() id := len(m.statuses) m.statuses = append(m.statuses, nil) - defer m.c.NewRow() - defer m.c.NewRow() + defer m.console.NewRow() + defer m.console.NewRow() return m.newStatus(id) } func (m *manager) newStatus(id int) Status { - ch := make(chan *status, BUFFER_SIZE) + ch := make(chan *status, BufferSize) m.updating.Add(1) go m.update(ch, id) return ch @@ -139,7 +139,7 @@ func (m *manager) StopAndWait() { close(m.done) defer m.close.Do(func() { // 4. restore cursor, mark done - m.c.Restore() + m.console.Restore() }) // 2. wait for all model update done m.updating.Wait() diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 4b2e38e17..02d2c531c 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -91,7 +91,7 @@ func (r *reader) Read(p []byte) (int, error) { if r.mu.TryLock() { defer r.mu.Unlock() - if len(r.ch) < progress.BUFFER_SIZE { + if len(r.ch) < progress.BufferSize { // intermediate progress might be ignored if buffer is full r.ch <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) } From 997a63ee7258cc6cb2343de273f4793d2038da0d Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 22 Sep 2023 08:22:42 +0000 Subject: [PATCH 13/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/console/console.go | 44 ++++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/cmd/oras/internal/display/console/console.go b/cmd/oras/internal/display/console/console.go index a0aa3199b..e9024b704 100644 --- a/cmd/oras/internal/display/console/console.go +++ b/cmd/oras/internal/display/console/console.go @@ -16,18 +16,22 @@ limitations under the License. package console import ( - "fmt" "os" "github.com/containerd/console" "github.com/morikuni/aec" ) -// MinWidth is the minimal width of supported console. -const MinWidth = 80 - -// MinHeight is the minimal height of supported console. -const MinHeight = 10 +const ( + // MinWidth is the minimal width of supported console. + MinWidth = 80 + // MinHeight is the minimal height of supported console. + MinHeight = 10 + // cannot use aec.Save since DEC has better compatilibity than SCO + Save = "\0337" + // cannot use aec.Restore since DEC has better compatilibity than SCO + Restore = "\0338" +) // Console is a wrapper around containerd's console.Console and ANSI escape // codes. @@ -63,33 +67,29 @@ func New(f *os.File) (*Console, error) { // Save saves the current cursor position. func (c *Console) Save() { - fmt.Fprint(c, aec.Hide) - // cannot use aec.Save since DEC has better compatilibity than SCO - fmt.Fprint(c, "\0337") + c.Write([]byte(aec.Hide.Apply(Save))) } // NewRow allocates a horizontal space to the output area with scroll if needed. func (c *Console) NewRow() { - // cannot use aec.Restore since DEC has better compatilibity than SCO - fmt.Fprint(c, "\0338") - fmt.Fprint(c, "\n") - fmt.Fprint(c, "\0337") + c.Write([]byte(Restore)) + c.Write([]byte("\n")) + c.Write([]byte(Save)) } // OutputTo outputs a string to a specific line. func (c *Console) OutputTo(upCnt uint, str string) { - fmt.Fprint(c, "\0338") - fmt.Fprint(c, aec.PreviousLine(upCnt)) - fmt.Fprint(c, str) - fmt.Fprint(c, " ") - fmt.Fprint(c, aec.EraseLine(aec.EraseModes.Tail)) + c.Write([]byte(Restore)) + c.Write([]byte(aec.PreviousLine(upCnt).Apply(str))) + c.Write([]byte(" ")) + c.Write([]byte(aec.EraseLine(aec.EraseModes.Tail).String())) } // Restore restores the saved cursor position. func (c *Console) Restore() { // cannot use aec.Restore since DEC has better compatilibity than SCO - fmt.Fprint(c, "\0338") - fmt.Fprint(c, aec.Column(0)) - fmt.Fprint(c, aec.EraseLine(aec.EraseModes.All)) - fmt.Fprint(c, aec.Show) + c.Write([]byte(Restore)) + c.Write([]byte(aec.Column(0). + With(aec.EraseLine(aec.EraseModes.All)). + With(aec.Show).String())) } From 144e907364d618e401d79c2feeccbff39667ad67 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 22 Sep 2023 08:47:11 +0000 Subject: [PATCH 14/98] fix lint Signed-off-by: Billy Zha --- cmd/oras/internal/display/console/console.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/oras/internal/display/console/console.go b/cmd/oras/internal/display/console/console.go index e9024b704..6a932d68a 100644 --- a/cmd/oras/internal/display/console/console.go +++ b/cmd/oras/internal/display/console/console.go @@ -67,29 +67,29 @@ func New(f *os.File) (*Console, error) { // Save saves the current cursor position. func (c *Console) Save() { - c.Write([]byte(aec.Hide.Apply(Save))) + _, _ = c.Write([]byte(aec.Hide.Apply(Save))) } // NewRow allocates a horizontal space to the output area with scroll if needed. func (c *Console) NewRow() { - c.Write([]byte(Restore)) - c.Write([]byte("\n")) - c.Write([]byte(Save)) + _, _ = c.Write([]byte(Restore)) + _, _ = c.Write([]byte("\n")) + _, _ = c.Write([]byte(Save)) } // OutputTo outputs a string to a specific line. func (c *Console) OutputTo(upCnt uint, str string) { - c.Write([]byte(Restore)) - c.Write([]byte(aec.PreviousLine(upCnt).Apply(str))) - c.Write([]byte(" ")) - c.Write([]byte(aec.EraseLine(aec.EraseModes.Tail).String())) + _, _ = c.Write([]byte(Restore)) + _, _ = c.Write([]byte(aec.PreviousLine(upCnt).Apply(str))) + _, _ = c.Write([]byte(" ")) + _, _ = c.Write([]byte(aec.EraseLine(aec.EraseModes.Tail).String())) } // Restore restores the saved cursor position. func (c *Console) Restore() { // cannot use aec.Restore since DEC has better compatilibity than SCO - c.Write([]byte(Restore)) - c.Write([]byte(aec.Column(0). + _, _ = c.Write([]byte(Restore)) + _, _ = c.Write([]byte(aec.Column(0). With(aec.EraseLine(aec.EraseModes.All)). With(aec.Show).String())) } From 1b1f23c888acc06e7267fdb58a465e5ff3c8ad4a Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 22 Sep 2023 08:59:15 +0000 Subject: [PATCH 15/98] refactor spinner mark Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/mark.go | 13 ++++++------- cmd/oras/internal/display/progress/status.go | 6 ++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cmd/oras/internal/display/progress/mark.go b/cmd/oras/internal/display/progress/mark.go index ce6988a96..0781e19a7 100644 --- a/cmd/oras/internal/display/progress/mark.go +++ b/cmd/oras/internal/display/progress/mark.go @@ -18,14 +18,13 @@ package progress var ( spinner = []rune("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") spinnerLen = len(spinner) - spinnerPos = 0 ) +type mark int + // GetMark returns the rune of status mark. -func GetMark(s *status) rune { - if s.done { - return '√' - } - spinnerPos = (spinnerPos + 1) % spinnerLen - return spinner[spinnerPos/2] +func (m *mark) GetMark() rune { + last := int(*m) + *m = mark((last + 1) % spinnerLen) + return spinner[last] } diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 5d6d89b06..7fb89c907 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -36,6 +36,7 @@ type status struct { offset int64 startTime *time.Time endTime *time.Time + mark mark } // NewStatus generates a status. @@ -87,11 +88,12 @@ func (s *status) String(width int) (string, string) { if !s.done { lenBar := int(percent * BarMaxLength) bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", BarMaxLength-lenBar)) - left = fmt.Sprintf("%c %s %s %s", GetMark(s), bar, s.prompt, name) + mark := s.mark.GetMark() + left = fmt.Sprintf("%c %s %s %s", mark, bar, s.prompt, name) // bar + wrapper(2) + space(1) lenLeft = BarMaxLength + 2 + 1 } else { - left = fmt.Sprintf("%c %s %s", GetMark(s), s.prompt, name) + left = fmt.Sprintf("√ %s %s", s.prompt, name) } // mark(1) + space(1) + prompt+ space(1) + name lenLeft += 1 + 1 + utf8.RuneCountInString(s.prompt) + 1 + utf8.RuneCountInString(name) From 008a61d60cb0bb914ca4ee3e5927929b017b8884 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 25 Sep 2023 03:44:02 +0000 Subject: [PATCH 16/98] refactor: improve manager and status Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 121 +++++++++--------- cmd/oras/internal/display/progress/mark.go | 17 +-- cmd/oras/internal/display/progress/status.go | 81 +++++++----- cmd/oras/internal/display/track/reader.go | 2 +- 4 files changed, 116 insertions(+), 105 deletions(-) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index 7f753b4c1..8f2ac7ea9 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -31,65 +31,60 @@ type Status chan<- *status // Manager is progress view master type Manager interface { Add() Status - StopAndWait() + Close() } -const ( - bufFlushDuration = 100 * time.Millisecond -) +const bufFlushDuration = 100 * time.Millisecond type manager struct { - statuses []*status - rwLock sync.RWMutex - renderTick *time.Ticker - console *console.Console - updating sync.WaitGroup - mu sync.Mutex - close sync.Once - // done used to stop render routine - // doneDone used to mark render routine stopped - done chan struct{} - doneDone chan struct{} + status []*status + statusLock sync.RWMutex + console *console.Console + updating sync.WaitGroup + renderDone chan struct{} + renderClosed chan struct{} } // NewManager initialized a new progress manager. func NewManager(f *os.File) (Manager, error) { - var m manager - var err error - - m.console, err = console.New(f) + c, err := console.New(f) if err != nil { return nil, err } - m.done = make(chan struct{}) - m.doneDone = make(chan struct{}) - m.renderTick = time.NewTicker(bufFlushDuration) + m := &manager{ + console: c, + renderDone: make(chan struct{}), + renderClosed: make(chan struct{}), + } m.start() - return &m, nil + return m, nil } func (m *manager) start() { - m.renderTick.Reset(bufFlushDuration) m.console.Save() + renderTicker := time.NewTicker(bufFlushDuration) go func() { + defer m.console.Restore() + defer renderTicker.Stop() for { m.render() select { - case <-m.done: - close(m.doneDone) + case <-m.renderDone: + m.render() + close(m.renderClosed) return - case <-m.renderTick.C: + case <-renderTicker.C: } } }() } func (m *manager) render() { - m.mu.Lock() - defer m.mu.Unlock() + m.statusLock.RLock() + defer m.statusLock.RUnlock() // todo: update size in another routine width, height := m.console.Size() - len := len(m.statuses) * 2 + len := len(m.status) * 2 offset := 0 if len > height { // skip statuses that cannot be rendered @@ -97,9 +92,7 @@ func (m *manager) render() { } for ; offset < len; offset += 2 { - m.rwLock.RLock() - status, progress := m.statuses[offset/2].String(width) - m.rwLock.RUnlock() + status, progress := m.status[offset/2].String(width) m.console.OutputTo(uint(len-offset), status) m.console.OutputTo(uint(len-offset-1), progress) } @@ -107,43 +100,51 @@ func (m *manager) render() { // Add appends a new status with 2-line space for rendering. func (m *manager) Add() Status { - m.mu.Lock() - defer m.mu.Unlock() - id := len(m.statuses) - m.statuses = append(m.statuses, nil) + if m.closed() { + return nil + } + + s := newStatus() + m.statusLock.Lock() + m.status = append(m.status, s) + m.statusLock.Unlock() + defer m.console.NewRow() defer m.console.NewRow() - return m.newStatus(id) + return m.statusChan(s) } -func (m *manager) newStatus(id int) Status { +func (m *manager) statusChan(s *status) Status { ch := make(chan *status, BufferSize) m.updating.Add(1) - go m.update(ch, id) + go func() { + defer m.updating.Done() + for newStatus := range ch { + s.Update(newStatus) + } + }() return ch } -func (m *manager) update(ch chan *status, id int) { - defer m.updating.Done() - for s := range ch { - m.rwLock.Lock() - m.statuses[id] = m.statuses[id].Update(s) - m.rwLock.Unlock() +// Close stops all status and waits for updating and rendering. +func (m *manager) Close() { + if m.closed() { + return } -} -// StopAndWait stops all status and waits for updating and rendering. -func (m *manager) StopAndWait() { - // 1. stop periodic render - m.renderTick.Stop() - close(m.done) - defer m.close.Do(func() { - // 4. restore cursor, mark done - m.console.Restore() - }) - // 2. wait for all model update done + // 1 wait for all model update done m.updating.Wait() - // 3. render last model - <-m.doneDone - m.render() + // 2. stop periodic render + close(m.renderDone) + // 3. wait for the render stop + <-m.renderClosed +} + +func (m *manager) closed() bool { + select { + case <-m.renderClosed: + return true + default: + return false + } } diff --git a/cmd/oras/internal/display/progress/mark.go b/cmd/oras/internal/display/progress/mark.go index 0781e19a7..8ba919a82 100644 --- a/cmd/oras/internal/display/progress/mark.go +++ b/cmd/oras/internal/display/progress/mark.go @@ -15,16 +15,13 @@ limitations under the License. package progress -var ( - spinner = []rune("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") - spinnerLen = len(spinner) -) +var spinnerSymbol = []rune("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") -type mark int +type spinner int -// GetMark returns the rune of status mark. -func (m *mark) GetMark() rune { - last := int(*m) - *m = mark((last + 1) % spinnerLen) - return spinner[last] +// symbol returns the rune of status mark and shift to the next. +func (s *spinner) symbol() rune { + last := int(*s) + *s = spinner((last + 1) % len(spinnerSymbol)) + return spinnerSymbol[last] } diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 7fb89c907..6611906d6 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -18,6 +18,7 @@ package progress import ( "fmt" "strings" + "sync" "time" "unicode/utf8" @@ -34,9 +35,17 @@ type status struct { prompt string descriptor ocispec.Descriptor offset int64 - startTime *time.Time - endTime *time.Time - mark mark + startTime time.Time + endTime time.Time + mark spinner + lock sync.RWMutex +} + +// newStatus generates a base empty status +func newStatus() *status { + return &status{ + offset: -1, + } } // NewStatus generates a status. @@ -50,25 +59,30 @@ func NewStatus(prompt string, descriptor ocispec.Descriptor, offset uint64) *sta // StartTiming starts timing. func StartTiming() *status { - now := time.Now() return &status{ offset: -1, - startTime: &now, + startTime: time.Now(), } } // EndTiming ends timing and set status to done. func EndTiming() *status { - now := time.Now() return &status{ offset: -1, - endTime: &now, + endTime: time.Now(), } } +func (s *status) isZero() bool { + return s.offset < 0 && s.startTime.IsZero() && s.endTime.IsZero() +} + // String returns human-readable TTY strings of the status. func (s *status) String(width int) (string, string) { - if s == nil { + s.lock.RLock() + defer s.lock.RUnlock() + + if s.isZero() { return "loading status...", "loading progress..." } // todo: doesn't support multiline prompt @@ -88,17 +102,17 @@ func (s *status) String(width int) (string, string) { if !s.done { lenBar := int(percent * BarMaxLength) bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", BarMaxLength-lenBar)) - mark := s.mark.GetMark() + mark := s.mark.symbol() left = fmt.Sprintf("%c %s %s %s", mark, bar, s.prompt, name) - // bar + wrapper(2) + space(1) - lenLeft = BarMaxLength + 2 + 1 + // bar + wrapper(2) + space(1) = len(bar) + 3 + lenLeft = BarMaxLength + 3 } else { left = fmt.Sprintf("√ %s %s", s.prompt, name) } - // mark(1) + space(1) + prompt+ space(1) + name - lenLeft += 1 + 1 + utf8.RuneCountInString(s.prompt) + 1 + utf8.RuneCountInString(name) + // mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3 + lenLeft += 3 + utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) - right := fmt.Sprintf(" %s/%s %6.2f%% %s", humanize.Bytes(uint64(s.offset)), humanize.Bytes(total), percent*100, s.DurationString()) + right := fmt.Sprintf(" %s/%s %6.2f%% %s", humanize.Bytes(uint64(s.offset)), humanize.Bytes(total), percent*100, s.durationString()) lenRight := utf8.RuneCountInString(right) lenMargin := width - lenLeft - lenRight if lenMargin < 0 { @@ -109,17 +123,17 @@ func (s *status) String(width int) (string, string) { return fmt.Sprintf("%s%s%s", left, strings.Repeat(" ", lenMargin), right), fmt.Sprintf(" └─ %s", s.descriptor.Digest.String()) } -// DurationString returns a viewable TTY string of the status with duration. -func (s *status) DurationString() string { - if s.startTime == nil { +// durationString returns a viewable TTY string of the status with duration. +func (s *status) durationString() string { + if s.startTime.IsZero() { return "0ms" } var d time.Duration - if s.endTime == nil { - d = time.Since(*s.startTime) + if s.endTime.IsZero() { + d = time.Since(s.startTime) } else { - d = s.endTime.Sub(*s.startTime) + d = s.endTime.Sub(s.startTime) } switch { @@ -136,23 +150,22 @@ func (s *status) DurationString() string { } // Update updates a status. -func (s *status) Update(new *status) *status { - if s == nil { - s = &status{} - } - if new.offset > 0 { - s.descriptor = new.descriptor - s.offset = new.offset +func (s *status) Update(n *status) { + s.lock.Lock() + defer s.lock.Unlock() + + if n.offset >= 0 { + s.offset = n.offset + s.descriptor = n.descriptor } - if new.prompt != "" { - s.prompt = new.prompt + if n.prompt != "" { + s.prompt = n.prompt } - if new.startTime != nil { - s.startTime = new.startTime + if !n.startTime.IsZero() { + s.startTime = n.startTime } - if new.endTime != nil { - s.endTime = new.endTime + if !n.endTime.IsZero() { + s.endTime = n.endTime s.done = true } - return s } diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 02d2c531c..110c57e41 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -67,7 +67,7 @@ func (r *reader) End() { // Stop stops the status channel and related manager. func (r *reader) Stop() { r.End() - r.m.StopAndWait() + r.m.Close() } func (r *reader) Read(p []byte) (int, error) { From 95ad473cbe644d19005be5fe777d48504384ac9f Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 25 Sep 2023 15:48:05 +0800 Subject: [PATCH 17/98] add error for manager functions Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 18 +++++++++++------- cmd/oras/internal/display/progress/status.go | 2 +- cmd/oras/internal/display/track/reader.go | 11 ++++++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index 8f2ac7ea9..0ec21b7ee 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -16,6 +16,7 @@ limitations under the License. package progress import ( + "errors" "os" "sync" "time" @@ -25,13 +26,15 @@ import ( const BufferSize = 20 +var errManagerStopped = errors.New("progress output manage has already been stopped") + // Status is print message channel type Status chan<- *status // Manager is progress view master type Manager interface { - Add() Status - Close() + Add() (Status, error) + Close() error } const bufFlushDuration = 100 * time.Millisecond @@ -99,9 +102,9 @@ func (m *manager) render() { } // Add appends a new status with 2-line space for rendering. -func (m *manager) Add() Status { +func (m *manager) Add() (Status, error) { if m.closed() { - return nil + return nil, errManagerStopped } s := newStatus() @@ -111,7 +114,7 @@ func (m *manager) Add() Status { defer m.console.NewRow() defer m.console.NewRow() - return m.statusChan(s) + return m.statusChan(s), nil } func (m *manager) statusChan(s *status) Status { @@ -127,9 +130,9 @@ func (m *manager) statusChan(s *status) Status { } // Close stops all status and waits for updating and rendering. -func (m *manager) Close() { +func (m *manager) Close() error { if m.closed() { - return + return errManagerStopped } // 1 wait for all model update done @@ -138,6 +141,7 @@ func (m *manager) Close() { close(m.renderDone) // 3. wait for the render stop <-m.renderClosed + return nil } func (m *manager) closed() bool { diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 6611906d6..4efe647ee 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -31,7 +31,7 @@ const BarMaxLength = 40 // status is used as message to update progress view. type status struct { - done bool + done bool // done is true when the end time is set prompt string descriptor ocispec.Descriptor offset int64 diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 110c57e41..6afa4d03a 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -47,13 +47,18 @@ func NewReader(r io.Reader, descriptor ocispec.Descriptor, actionPrompt string, } func managedReader(r io.Reader, descriptor ocispec.Descriptor, manager progress.Manager, actionPrompt string, donePrompt string) (*reader, error) { + ch, err := manager.Add() + if err != nil { + return nil, err + } + return &reader{ base: r, descriptor: descriptor, actionPrompt: actionPrompt, donePrompt: donePrompt, m: manager, - ch: manager.Add(), + ch: ch, }, nil } @@ -65,9 +70,9 @@ func (r *reader) End() { } // Stop stops the status channel and related manager. -func (r *reader) Stop() { +func (r *reader) Stop() error { r.End() - r.m.Close() + return r.m.Close() } func (r *reader) Read(p []byte) (int, error) { From ed84a697d711fc9b13ac2dadc66697e45e7de782 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 25 Sep 2023 07:56:34 +0000 Subject: [PATCH 18/98] remove unnecessary lock Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 6afa4d03a..f3ff03d51 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -89,17 +89,12 @@ func (r *reader) Read(p []byte) (int, error) { if offset != uint64(r.descriptor.Size) { return n, io.ErrUnexpectedEOF } - r.mu.Lock() - defer r.mu.Unlock() r.ch <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) } - if r.mu.TryLock() { - defer r.mu.Unlock() - if len(r.ch) < progress.BufferSize { - // intermediate progress might be ignored if buffer is full - r.ch <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) - } + if len(r.ch) < progress.BufferSize { + // intermediate progress might be ignored if buffer is full + r.ch <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) } return n, err } From 47ef494d4137202416c222d9729e4ab628904b20 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 25 Sep 2023 07:58:40 +0000 Subject: [PATCH 19/98] update reader naming Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index f3ff03d51..1fce15d3e 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -33,7 +33,7 @@ type reader struct { descriptor ocispec.Descriptor mu sync.Mutex m progress.Manager - ch progress.Status + status progress.Status once sync.Once } @@ -58,15 +58,15 @@ func managedReader(r io.Reader, descriptor ocispec.Descriptor, manager progress. actionPrompt: actionPrompt, donePrompt: donePrompt, m: manager, - ch: ch, + status: ch, }, nil } // End closes the status channel. func (r *reader) End() { - defer close(r.ch) - r.ch <- progress.NewStatus(r.donePrompt, r.descriptor, uint64(r.descriptor.Size)) - r.ch <- progress.EndTiming() + defer close(r.status) + r.status <- progress.NewStatus(r.donePrompt, r.descriptor, uint64(r.descriptor.Size)) + r.status <- progress.EndTiming() } // Stop stops the status channel and related manager. @@ -77,7 +77,7 @@ func (r *reader) Stop() error { func (r *reader) Read(p []byte) (int, error) { r.once.Do(func() { - r.ch <- progress.StartTiming() + r.status <- progress.StartTiming() }) n, err := r.base.Read(p) if err != nil && err != io.EOF { @@ -89,12 +89,12 @@ func (r *reader) Read(p []byte) (int, error) { if offset != uint64(r.descriptor.Size) { return n, io.ErrUnexpectedEOF } - r.ch <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) + r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) } - if len(r.ch) < progress.BufferSize { + if len(r.status) < progress.BufferSize { // intermediate progress might be ignored if buffer is full - r.ch <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) + r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) } return n, err } From 7e249ed0ab5b4f35f23ac1351f43a3e08d192af4 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 25 Sep 2023 09:09:56 +0000 Subject: [PATCH 20/98] remove reader.once Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 10 ++++------ cmd/oras/root/blob/fetch.go | 20 +++++++++++--------- cmd/oras/root/blob/push.go | 1 + 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 1fce15d3e..e5a186c02 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -18,7 +18,6 @@ package track import ( "io" "os" - "sync" "sync/atomic" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -31,10 +30,8 @@ type reader struct { actionPrompt string donePrompt string descriptor ocispec.Descriptor - mu sync.Mutex m progress.Manager status progress.Status - once sync.Once } // NewReader returns a new reader with tracked progress. @@ -75,10 +72,11 @@ func (r *reader) Stop() error { return r.m.Close() } +func (r *reader) Start() { + r.status <- progress.StartTiming() +} + func (r *reader) Read(p []byte) (int, error) { - r.once.Do(func() { - r.status <- progress.StartTiming() - }) n, err := r.base.Read(p) if err != nil && err != io.EOF { return n, err diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index fc4e18058..9121fcca4 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -156,15 +156,6 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg } return desc, nil } - var r io.Reader = vr - if opts.TTY != nil { - trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ", opts.TTY) - if err != nil { - return ocispec.Descriptor{}, err - } - defer trackedReader.Stop() - r = trackedReader - } // save blob content into the local file if the output path is provided file, err := os.Create(opts.outputPath) @@ -177,6 +168,17 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg } }() + var r io.Reader = vr + if opts.TTY != nil { + trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ", opts.TTY) + if err != nil { + return ocispec.Descriptor{}, err + } + defer trackedReader.Stop() + trackedReader.Start() + r = trackedReader + } + if _, err := io.Copy(file, r); err != nil { return ocispec.Descriptor{}, err } diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 60d58c866..d2a6d5491 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -153,6 +153,7 @@ func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc oci return err } defer trackedReader.Stop() + trackedReader.Start() r = trackedReader } if err := t.Push(ctx, desc, r); err != nil { From a6476d4be95ae3a1356d150d927bfc947925d695 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 25 Sep 2023 09:20:31 +0000 Subject: [PATCH 21/98] ignore returned error of manager Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 4 +++- cmd/oras/root/blob/push.go | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 9121fcca4..8d1ac816f 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -174,7 +174,9 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg if err != nil { return ocispec.Descriptor{}, err } - defer trackedReader.Stop() + defer func() { + _ = trackedReader.Stop() + }() trackedReader.Start() r = trackedReader } diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index d2a6d5491..83bb6df8c 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -152,7 +152,9 @@ func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc oci if err != nil { return err } - defer trackedReader.Stop() + defer func() { + _ = trackedReader.Stop() + }() trackedReader.Start() r = trackedReader } From 8c7458bd70716cef57c27b7f2171a7316302578c Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 25 Sep 2023 09:33:17 +0000 Subject: [PATCH 22/98] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index e5a186c02..d63ad4224 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -59,23 +59,20 @@ func managedReader(r io.Reader, descriptor ocispec.Descriptor, manager progress. }, nil } -// End closes the status channel. -func (r *reader) End() { - defer close(r.status) - r.status <- progress.NewStatus(r.donePrompt, r.descriptor, uint64(r.descriptor.Size)) - r.status <- progress.EndTiming() -} - // Stop stops the status channel and related manager. func (r *reader) Stop() error { - r.End() + r.status <- progress.NewStatus(r.donePrompt, r.descriptor, uint64(r.descriptor.Size)) + r.status <- progress.EndTiming() + close(r.status) return r.m.Close() } +// Start sends the start timing to the status channel. func (r *reader) Start() { r.status <- progress.StartTiming() } +// Read reads from the underlying reader and updates the progress. func (r *reader) Read(p []byte) (int, error) { n, err := r.base.Read(p) if err != nil && err != io.EOF { From 1584fd21f71e9e24418585aa0d60b091c6f3e149 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 28 Sep 2023 02:53:58 +0000 Subject: [PATCH 23/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 11 +++++------ .../internal/display/progress/{mark.go => spinner.go} | 6 +++--- cmd/oras/internal/option/common.go | 8 ++++---- 3 files changed, 12 insertions(+), 13 deletions(-) rename cmd/oras/internal/display/progress/{mark.go => spinner.go} (83%) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index 0ec21b7ee..7ecca7bcc 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -24,6 +24,7 @@ import ( "oras.land/oras/cmd/oras/internal/display/console" ) +// BufferSize is the size of the status channel buffer. const BufferSize = 20 var errManagerStopped = errors.New("progress output manage has already been stopped") @@ -70,13 +71,14 @@ func (m *manager) start() { defer m.console.Restore() defer renderTicker.Stop() for { - m.render() select { case <-m.renderDone: + m.updating.Wait() m.render() close(m.renderClosed) return case <-renderTicker.C: + m.render() } } }() @@ -134,12 +136,9 @@ func (m *manager) Close() error { if m.closed() { return errManagerStopped } - - // 1 wait for all model update done - m.updating.Wait() - // 2. stop periodic render + // 1. stop periodic rendering close(m.renderDone) - // 3. wait for the render stop + // 2. wait for the render stop <-m.renderClosed return nil } diff --git a/cmd/oras/internal/display/progress/mark.go b/cmd/oras/internal/display/progress/spinner.go similarity index 83% rename from cmd/oras/internal/display/progress/mark.go rename to cmd/oras/internal/display/progress/spinner.go index 8ba919a82..b874412f1 100644 --- a/cmd/oras/internal/display/progress/mark.go +++ b/cmd/oras/internal/display/progress/spinner.go @@ -15,13 +15,13 @@ limitations under the License. package progress -var spinnerSymbol = []rune("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") +var spinnerSymbols = []rune("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") type spinner int // symbol returns the rune of status mark and shift to the next. func (s *spinner) symbol() rune { last := int(*s) - *s = spinner((last + 1) % len(spinnerSymbol)) - return spinnerSymbol[last] + *s = spinner((last + 1) % len(spinnerSymbols)) + return spinnerSymbols[last] } diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 69678546b..6833751f8 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -32,14 +32,14 @@ type Common struct { Verbose bool TTY *os.File - avoidTTY bool + noTTY bool } // ApplyFlags applies flags to a command flag set. func (opts *Common) ApplyFlags(fs *pflag.FlagSet) { fs.BoolVarP(&opts.Debug, "debug", "d", false, "debug mode") fs.BoolVarP(&opts.Verbose, "verbose", "v", false, "verbose output") - fs.BoolVarP(&opts.avoidTTY, "no-tty", "", false, "[Preview] avoid using stdout as a terminal") + fs.BoolVarP(&opts.noTTY, "no-tty", "", false, "[Preview] avoid using stdout as a terminal") } // WithContext returns a new FieldLogger and an associated Context derived from ctx. @@ -50,9 +50,9 @@ func (opts *Common) WithContext(ctx context.Context) (context.Context, logrus.Fi // Parse gets target options from user input. func (opts *Common) Parse() error { f := os.Stderr - if !opts.avoidTTY && term.IsTerminal(int(f.Fd())) { + if !opts.noTTY && term.IsTerminal(int(f.Fd())) { if opts.Debug { - return errors.New("cannot use --debug, add --noTTY to suppress terminal output") + return errors.New("cannot use --debug, add --no-tty to suppress terminal output") } opts.TTY = f } From b44e2e8d163ffa571fb4758d4bc12b5c5b30bc40 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 28 Sep 2023 08:06:44 +0000 Subject: [PATCH 24/98] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status.go | 5 ++++- cmd/oras/internal/display/track/reader.go | 14 +++++++++----- cmd/oras/root/blob/push.go | 3 ++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 4efe647ee..ffedeecf2 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -87,7 +87,10 @@ func (s *status) String(width int) (string, string) { } // todo: doesn't support multiline prompt total := uint64(s.descriptor.Size) - percent := float64(s.offset) / float64(total) + var percent float64 + if s.offset >= 0 { + percent = float64(s.offset) / float64(total) + } name := s.descriptor.Annotations["org.opencontainers.image.title"] if name == "" { diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index d63ad4224..cf995d209 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -30,7 +30,7 @@ type reader struct { actionPrompt string donePrompt string descriptor ocispec.Descriptor - m progress.Manager + manager progress.Manager status progress.Status } @@ -54,17 +54,21 @@ func managedReader(r io.Reader, descriptor ocispec.Descriptor, manager progress. descriptor: descriptor, actionPrompt: actionPrompt, donePrompt: donePrompt, - m: manager, + manager: manager, status: ch, }, nil } -// Stop stops the status channel and related manager. -func (r *reader) Stop() error { +// StopManager stops the status channel and related manager. +func (r *reader) StopManager() error { + return r.manager.Close() +} + +// Stop stops the status channel without closing the manager. +func (r *reader) Stop() { r.status <- progress.NewStatus(r.donePrompt, r.descriptor, uint64(r.descriptor.Size)) r.status <- progress.EndTiming() close(r.status) - return r.m.Close() } // Start sends the start timing to the status channel. diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 83bb6df8c..207e4fc7d 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -153,7 +153,8 @@ func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc oci return err } defer func() { - _ = trackedReader.Stop() + trackedReader.Stop() + _ = trackedReader.StopManager() }() trackedReader.Start() r = trackedReader From ea6476fda5c86e9010fdd4123b4a4ea004618e41 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 28 Sep 2023 08:07:13 +0000 Subject: [PATCH 25/98] code clean Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 8d1ac816f..18e6c4282 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -175,7 +175,8 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg return ocispec.Descriptor{}, err } defer func() { - _ = trackedReader.Stop() + trackedReader.Stop() + _ = trackedReader.StopManager() }() trackedReader.Start() r = trackedReader From b06383cbc1bc4c5019bb6265055b8358d213aeae Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 28 Sep 2023 08:11:39 +0000 Subject: [PATCH 26/98] feat: support progress bar for `oras cp` Signed-off-by: Billy Zha --- cmd/oras/internal/display/print.go | 17 +++ cmd/oras/internal/display/prompt/prompt.go | 60 ++++++++++ cmd/oras/internal/display/track/target.go | 129 +++++++++++++++++++++ cmd/oras/root/cp.go | 75 +++++++----- 4 files changed, 254 insertions(+), 27 deletions(-) create mode 100644 cmd/oras/internal/display/prompt/prompt.go create mode 100644 cmd/oras/internal/display/track/target.go diff --git a/cmd/oras/internal/display/print.go b/cmd/oras/internal/display/print.go index 48374a327..6bbbb757d 100644 --- a/cmd/oras/internal/display/print.go +++ b/cmd/oras/internal/display/print.go @@ -25,6 +25,7 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry" + "oras.land/oras/cmd/oras/internal/display/track" ) var printLock sync.Mutex @@ -57,6 +58,22 @@ func PrintStatus(desc ocispec.Descriptor, status string, verbose bool) error { return Print(status, ShortDigest(desc), name) } +// PrintStatusWithoutTrackable prints transfer status if trackable is no set. +func PrintStatusWithoutTrackable(desc ocispec.Descriptor, status string, verbose bool, trackable track.Trackable) error { + if trackable != nil { + return nil + } + return PrintStatus(desc, status, verbose) +} + +// PrintStatusWithTrackable prints transfer status if trackable is set. +func PrintStatusWithTrackable(desc ocispec.Descriptor, status string, verbose bool, trackable track.Trackable) error { + if trackable == nil { + return PrintStatus(desc, status, verbose) + } + return trackable.Prompt(desc, status) +} + // PrintSuccessorStatus prints transfer status of successors. func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, status string, fetcher content.Fetcher, committed *sync.Map, verbose bool) error { successors, err := content.Successors(ctx, fetcher, desc) diff --git a/cmd/oras/internal/display/prompt/prompt.go b/cmd/oras/internal/display/prompt/prompt.go new file mode 100644 index 000000000..250b81638 --- /dev/null +++ b/cmd/oras/internal/display/prompt/prompt.go @@ -0,0 +1,60 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package prompt + +import ( + "fmt" + "reflect" +) + +var () + +type prompt struct { + Copying, Skipped, Exists, Copied, Downloading, Downloaded string +} + +func New() *prompt { + var p prompt + p.pad() + return &p +} + +// pad appends the prompt strings with spaces to make them the same length. +func (p *prompt) pad() { + v := reflect.ValueOf(p).Elem() + t := v.Type() + maxLen := 0 + + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + if f.IsValid() && f.CanSet() && f.Kind() == reflect.String { + sf := t.Field(i) + len := len(sf.Name) + if len > maxLen { + maxLen = len + } + } + + } + + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + if f.IsValid() && f.CanSet() && f.Kind() == reflect.String { + sf := t.Field(i) + f.SetString(fmt.Sprintf("%-*s", maxLen, sf.Name)) + } + } +} diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go new file mode 100644 index 000000000..c2e8bbd60 --- /dev/null +++ b/cmd/oras/internal/display/track/target.go @@ -0,0 +1,129 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package track + +import ( + "context" + "fmt" + "io" + "os" + "reflect" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/registry" + "oras.land/oras/cmd/oras/internal/display/progress" +) + +// Trackable can be tracked and supprots explicit prompting and stoping. +type Trackable interface { + Prompt(desc ocispec.Descriptor, prompt string) error + Close() error +} + +// Target is a wrapper for oras.Target with tracked pushing. +type Target interface { + oras.GraphTarget + Trackable +} + +type target struct { + oras.Target + manager progress.Manager + actionPrompt string + donePrompt string +} + +func NewTarget(t oras.Target, actionPrompt, donePrompt string, tty *os.File) (Target, error) { + manager, err := progress.NewManager(tty) + if err != nil { + return nil, err + } + + return &target{ + Target: t, + manager: manager, + actionPrompt: actionPrompt, + donePrompt: donePrompt, + }, nil +} + +func (t *target) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { + r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) + if err != nil { + return err + } + defer r.Stop() + r.Start() + if err := t.Target.Push(ctx, expected, r); err != nil { + return err + } + + r.status <- progress.EndTiming() + r.status <- progress.NewStatus(t.donePrompt, expected, uint64(expected.Size)) + return nil +} + +func (t *target) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { + r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) + if err != nil { + return err + } + defer r.Stop() + r.Start() + if rp, ok := t.Target.(registry.ReferencePusher); ok { + err = rp.PushReference(ctx, expected, r, reference) + } else { + if err := t.Target.Push(ctx, expected, r); err != nil { + return err + } + err = t.Target.Tag(ctx, expected, reference) + } + + if err != nil { + return err + } + + r.status <- progress.EndTiming() + r.status <- progress.NewStatus(t.donePrompt, expected, uint64(expected.Size)) + return nil +} + +func (t *target) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + if p, ok := t.Target.(content.PredecessorFinder); ok { + return p.Predecessors(ctx, node) + } + return nil, fmt.Errorf("target %v does not support Predecessors", reflect.TypeOf(t.Target)) +} + +func (t *target) Close() error { + if err := t.manager.Close(); err != nil { + return err + } + return nil +} + +func (t *target) Prompt(desc ocispec.Descriptor, prompt string) error { + status, err := t.manager.Add() + if err != nil { + return err + } + defer close(status) + status <- progress.NewStatus(prompt, desc, uint64(desc.Size)) + status <- progress.EndTiming() + return nil +} diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 107c1bb9f..19bbd589f 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -29,6 +29,7 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras/cmd/oras/internal/display" + "oras.land/oras/cmd/oras/internal/display/track" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/docker" "oras.land/oras/internal/graph" @@ -117,6 +118,42 @@ func runCopy(ctx context.Context, opts copyOptions) error { return err } + desc, err := doCopy(ctx, src, dst, opts) + if err != nil { + return err + } + + if from, err := digest.Parse(opts.From.Reference); err == nil && from != desc.Digest { + // correct source digest + opts.From.RawReference = fmt.Sprintf("%s@%s", opts.From.Path, desc.Digest.String()) + } + fmt.Println("Copied", opts.From.AnnotatedReference(), "=>", opts.To.AnnotatedReference()) + + if len(opts.extraRefs) != 0 { + tagNOpts := oras.DefaultTagNOptions + tagNOpts.Concurrency = opts.concurrency + if _, err = oras.TagN(ctx, display.NewTagStatusPrinter(dst), opts.To.Reference, opts.extraRefs, tagNOpts); err != nil { + return err + } + } + + fmt.Println("Digest:", desc.Digest) + + return nil +} + +func doCopy(ctx context.Context, src option.ReadOnlyGraphTagFinderTarget, dst oras.GraphTarget, opts copyOptions) (ocispec.Descriptor, error) { + var trackable track.Trackable + if opts.TTY != nil { + target, err := track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) + if err != nil { + return ocispec.Descriptor{}, err + } + defer target.Close() + trackable = target + dst = target + } + // Prepare copy options committed := &sync.Map{} extendedCopyOptions := oras.DefaultExtendedCopyOptions @@ -124,33 +161,37 @@ func runCopy(ctx context.Context, opts copyOptions) error { extendedCopyOptions.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { return graph.Referrers(ctx, src, desc, "") } - extendedCopyOptions.PreCopy = display.StatusPrinter("Copying", opts.Verbose) + extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + return display.PrintStatusWithoutTrackable(desc, "Copying", opts.Verbose, trackable) + } extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - if err := display.PrintSuccessorStatus(ctx, desc, "Skipped", dst, committed, opts.Verbose); err != nil { + var err error + if err = display.PrintSuccessorStatus(ctx, desc, "Skipped", dst, committed, opts.Verbose); err != nil { return err } - return display.PrintStatus(desc, "Copied ", opts.Verbose) + return display.PrintStatusWithoutTrackable(desc, "Copied ", opts.Verbose, trackable) } extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintStatus(desc, "Exists ", opts.Verbose) + return display.PrintStatusWithTrackable(desc, "Exists ", opts.Verbose, trackable) } var desc ocispec.Descriptor + var err error rOpts := oras.DefaultResolveOptions rOpts.TargetPlatform = opts.Platform.Platform if opts.recursive { desc, err = oras.Resolve(ctx, src, opts.From.Reference, rOpts) if err != nil { - return fmt.Errorf("failed to resolve %s: %w", opts.From.Reference, err) + return ocispec.Descriptor{}, fmt.Errorf("failed to resolve %s: %w", opts.From.Reference, err) } err = recursiveCopy(ctx, src, dst, opts.To.Reference, desc, extendedCopyOptions) } else { if opts.To.Reference == "" { desc, err = oras.Resolve(ctx, src, opts.From.Reference, rOpts) if err != nil { - return fmt.Errorf("failed to resolve %s: %w", opts.From.Reference, err) + return ocispec.Descriptor{}, fmt.Errorf("failed to resolve %s: %w", opts.From.Reference, err) } err = oras.CopyGraph(ctx, src, dst, desc, extendedCopyOptions.CopyGraphOptions) } else { @@ -163,27 +204,7 @@ func runCopy(ctx context.Context, opts copyOptions) error { desc, err = oras.Copy(ctx, src, opts.From.Reference, dst, opts.To.Reference, copyOptions) } } - if err != nil { - return err - } - - if from, err := digest.Parse(opts.From.Reference); err == nil && from != desc.Digest { - // correct source digest - opts.From.RawReference = fmt.Sprintf("%s@%s", opts.From.Path, desc.Digest.String()) - } - fmt.Println("Copied", opts.From.AnnotatedReference(), "=>", opts.To.AnnotatedReference()) - - if len(opts.extraRefs) != 0 { - tagNOpts := oras.DefaultTagNOptions - tagNOpts.Concurrency = opts.concurrency - if _, err = oras.TagN(ctx, display.NewTagStatusPrinter(dst), opts.To.Reference, opts.extraRefs, tagNOpts); err != nil { - return err - } - } - - fmt.Println("Digest:", desc.Digest) - - return nil + return desc, err } // recursiveCopy copies an artifact and its referrers from one target to another. From ad0608da27c11dd964418cce982dbcda0d0db8d3 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 03:29:34 +0000 Subject: [PATCH 27/98] add test for spinner Signed-off-by: Billy Zha --- .../internal/display/progress/spinner_test.go | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 cmd/oras/internal/display/progress/spinner_test.go diff --git a/cmd/oras/internal/display/progress/spinner_test.go b/cmd/oras/internal/display/progress/spinner_test.go new file mode 100644 index 000000000..1a799b76d --- /dev/null +++ b/cmd/oras/internal/display/progress/spinner_test.go @@ -0,0 +1,30 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package progress + +import "testing" + +func Test_spinner_symbol(t *testing.T) { + var s spinner + for i := 0; i < len(spinnerSymbols); i++ { + if s.symbol() != spinnerSymbols[i] { + t.Errorf("symbol() = %v, want %v", s.symbol(), spinnerSymbols[i]) + } + } + if s.symbol() != spinnerSymbols[0] { + t.Errorf("symbol() = %v, want %v", s.symbol(), spinnerSymbols[0]) + } +} From 1aab8bd8371d30c292da26d03e9538a3dcce3dbb Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 04:58:15 +0000 Subject: [PATCH 28/98] add cross-package coverage generation Signed-off-by: Billy Zha --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 057ef9aaf..2261cebb0 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ LDFLAGS += -X $(PROJECT_PKG)/internal/version.GitTreeState=${GIT_DIRTY} .PHONY: test test: tidy vendor check-encoding ## tidy and run tests - $(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic ./... + $(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./... .PHONY: teste2e teste2e: ## run end to end tests From 78b0d42e2f452cfc4caca4c31a40abf238d80ab3 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 06:50:36 +0000 Subject: [PATCH 29/98] Refactor tests Signed-off-by: Billy Zha --- .../internal/display/console/console_test.go | 2 + .../display/console/testutils/testutils.go | 66 +++++++++++++++++++ cmd/oras/root/blob/fetch_test.go | 25 ++----- cmd/oras/root/blob/push_test.go | 38 ++--------- 4 files changed, 77 insertions(+), 54 deletions(-) create mode 100644 cmd/oras/internal/display/console/testutils/testutils.go diff --git a/cmd/oras/internal/display/console/console_test.go b/cmd/oras/internal/display/console/console_test.go index 0338dbf1e..cf00a88ed 100644 --- a/cmd/oras/internal/display/console/console_test.go +++ b/cmd/oras/internal/display/console/console_test.go @@ -1,3 +1,5 @@ +//go:build darwin || freebsd || linux || netbsd || openbsd || solaris + /* Copyright The ORAS Authors. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/cmd/oras/internal/display/console/testutils/testutils.go b/cmd/oras/internal/display/console/testutils/testutils.go new file mode 100644 index 000000000..894b9aed4 --- /dev/null +++ b/cmd/oras/internal/display/console/testutils/testutils.go @@ -0,0 +1,66 @@ +//go:build darwin || freebsd || linux || netbsd || openbsd || solaris + +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testutils + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "sync" + + "github.com/containerd/console" +) + +// NewPty creates a new pty pair for testing, caller is responsible for closing +// the returned slave if err is not nil. +func NewPty() (console.Console, *os.File, error) { + pty, slavePath, err := console.NewPty() + if err != nil { + return nil, nil, err + } + slave, err := os.OpenFile(slavePath, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return pty, slave, nil +} + +// OrderedMatch checks that the output from the pty matches the expected strings +func OrderedMatch(pty console.Console, slave *os.File, expected ...string) error { + var wg sync.WaitGroup + wg.Add(1) + var buffer bytes.Buffer + go func() { + defer wg.Done() + _, _ = io.Copy(&buffer, pty) + }() + slave.Close() + wg.Wait() + + got := buffer.String() + for _, e := range expected { + i := strings.Index(got, e) + if i < 0 { + return fmt.Errorf("failed to find %q in %q", e, got) + } + got = got[i+len(e):] + } + return nil +} diff --git a/cmd/oras/root/blob/fetch_test.go b/cmd/oras/root/blob/fetch_test.go index a3247eb8a..0dcf17407 100644 --- a/cmd/oras/root/blob/fetch_test.go +++ b/cmd/oras/root/blob/fetch_test.go @@ -1,3 +1,5 @@ +//go:build darwin || freebsd || linux || netbsd || openbsd || solaris + /* Copyright The ORAS Authors. Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,24 +20,17 @@ package blob import ( "bytes" "context" - "io" - "os" - "sync" "testing" - "github.com/containerd/console" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content/memory" + "oras.land/oras/cmd/oras/internal/display/console/testutils" ) func Test_fetchBlobOptions_doFetch(t *testing.T) { // prepare - pty, slavePath, err := console.NewPty() - if err != nil { - t.Fatal(err) - } - slave, err := os.OpenFile(slavePath, os.O_RDWR, 0) + pty, slave, err := testutils.NewPty() if err != nil { t.Fatal(err) } @@ -56,7 +51,6 @@ func Test_fetchBlobOptions_doFetch(t *testing.T) { if err := src.Tag(ctx, desc, tag); err != nil { t.Fatal(err) } - var opts fetchBlobOptions opts.Reference = tag opts.Common.TTY = slave @@ -67,16 +61,7 @@ func Test_fetchBlobOptions_doFetch(t *testing.T) { t.Fatal(err) } // validate - var wg sync.WaitGroup - wg.Add(1) - var buffer bytes.Buffer - go func() { - defer wg.Done() - _, _ = io.Copy(&buffer, pty) - }() - slave.Close() - wg.Wait() - if err := orderedMatch(t, buffer.String(), "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + if err = testutils.OrderedMatch(pty, slave, "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } diff --git a/cmd/oras/root/blob/push_test.go b/cmd/oras/root/blob/push_test.go index ab8c7752d..1bc83745c 100644 --- a/cmd/oras/root/blob/push_test.go +++ b/cmd/oras/root/blob/push_test.go @@ -1,5 +1,4 @@ -//go:build linux || zos || freebsd -// +build linux zos freebsd +//go:build darwin || freebsd || linux || netbsd || openbsd || solaris /* Copyright The ORAS Authors. @@ -21,26 +20,17 @@ package blob import ( "bytes" "context" - "fmt" - "io" - "os" - "strings" - "sync" "testing" - "github.com/containerd/console" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content/memory" + "oras.land/oras/cmd/oras/internal/display/console/testutils" ) func Test_pushBlobOptions_doPush(t *testing.T) { // prepare - pty, slavePath, err := console.NewPty() - if err != nil { - t.Fatal(err) - } - slave, err := os.OpenFile(slavePath, os.O_RDWR, 0) + pty, slave, err := testutils.NewPty() if err != nil { t.Fatal(err) } @@ -61,27 +51,7 @@ func Test_pushBlobOptions_doPush(t *testing.T) { t.Fatal(err) } // validate - var wg sync.WaitGroup - wg.Add(1) - var buffer bytes.Buffer - go func() { - defer wg.Done() - _, _ = io.Copy(&buffer, pty) - }() - slave.Close() - wg.Wait() - if err := orderedMatch(t, buffer.String(), "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + if err = testutils.OrderedMatch(pty, slave, "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } - -func orderedMatch(t *testing.T, actual string, expected ...string) error { - for _, e := range expected { - i := strings.Index(actual, e) - if i < 0 { - return fmt.Errorf("expected to find %q in %q", e, actual) - } - actual = actual[i+len(e):] - } - return nil -} From 2b13d6aa84d135ee6b9e1fcc3cf9dbb6bac7411f Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 07:23:50 +0000 Subject: [PATCH 30/98] cover common tests Signed-off-by: Billy Zha --- cmd/oras/internal/option/common.go | 6 +++- cmd/oras/internal/option/common_unix_test.go | 38 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 cmd/oras/internal/option/common_unix_test.go diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 6833751f8..72b524c1a 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -49,7 +49,11 @@ func (opts *Common) WithContext(ctx context.Context) (context.Context, logrus.Fi // Parse gets target options from user input. func (opts *Common) Parse() error { - f := os.Stderr + return opts.parseTTY(os.Stderr) +} + +// parseTTY gets target options from user input. +func (opts *Common) parseTTY(f *os.File) error { if !opts.noTTY && term.IsTerminal(int(f.Fd())) { if opts.Debug { return errors.New("cannot use --debug, add --no-tty to suppress terminal output") diff --git a/cmd/oras/internal/option/common_unix_test.go b/cmd/oras/internal/option/common_unix_test.go new file mode 100644 index 000000000..9851d7629 --- /dev/null +++ b/cmd/oras/internal/option/common_unix_test.go @@ -0,0 +1,38 @@ +//go:build darwin || freebsd || linux || netbsd || openbsd || solaris + +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package option + +import ( + "testing" + + "oras.land/oras/cmd/oras/internal/display/console/testutils" +) + +func TestCommon_parseTTY(t *testing.T) { + _, slave, err := testutils.NewPty() + if err != nil { + t.Fatal(err) + } + defer slave.Close() + var opts Common + // test + opts.Debug = true + if err := opts.parseTTY(slave); err == nil { + t.Fatal("expected error when debug is set to TTY output") + } +} From 6bf4f3d8d23ff4954fb0816a58ca8a7a8a1a5438 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 08:03:00 +0000 Subject: [PATCH 31/98] add status coverage Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status.go | 23 ++++--- .../internal/display/progress/status_test.go | 66 +++++++++++++++++++ 2 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 cmd/oras/internal/display/progress/status_test.go diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index ffedeecf2..9a3466d91 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -27,7 +27,12 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -const BarMaxLength = 40 +const ( + barMaxLength = 40 + zeroDuration = "0s" // default zero value of time.Duration.String() + zeroStatus = "loading status..." + zeroProgress = "loading progress..." +) // status is used as message to update progress view. type status struct { @@ -83,7 +88,7 @@ func (s *status) String(width int) (string, string) { defer s.lock.RUnlock() if s.isZero() { - return "loading status...", "loading progress..." + return zeroStatus, zeroProgress } // todo: doesn't support multiline prompt total := uint64(s.descriptor.Size) @@ -103,12 +108,12 @@ func (s *status) String(width int) (string, string) { var left string var lenLeft int if !s.done { - lenBar := int(percent * BarMaxLength) - bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", BarMaxLength-lenBar)) + lenBar := int(percent * barMaxLength) + bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barMaxLength-lenBar)) mark := s.mark.symbol() left = fmt.Sprintf("%c %s %s %s", mark, bar, s.prompt, name) // bar + wrapper(2) + space(1) = len(bar) + 3 - lenLeft = BarMaxLength + 3 + lenLeft = barMaxLength + 3 } else { left = fmt.Sprintf("√ %s %s", s.prompt, name) } @@ -129,7 +134,7 @@ func (s *status) String(width int) (string, string) { // durationString returns a viewable TTY string of the status with duration. func (s *status) durationString() string { if s.startTime.IsZero() { - return "0ms" + return zeroDuration } var d time.Duration @@ -140,14 +145,12 @@ func (s *status) durationString() string { } switch { - case d > time.Minute: - d = d.Round(time.Second) case d > time.Second: - d = d.Round(100 * time.Millisecond) + d = d.Round(time.Second) case d > time.Millisecond: d = d.Round(time.Millisecond) default: - d = d.Round(10 * time.Nanosecond) + d = d.Round(time.Microsecond) } return d.String() } diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go new file mode 100644 index 000000000..603fd9a81 --- /dev/null +++ b/cmd/oras/internal/display/progress/status_test.go @@ -0,0 +1,66 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package progress + +import ( + "testing" + "time" + + "oras.land/oras/cmd/oras/internal/display/console" +) + +func Test_status_String(t *testing.T) { + // zero status and progress + s := newStatus() + if status, progress := s.String(console.MinWidth); status != zeroStatus || progress != zeroProgress { + t.Errorf("status.String() = %v, %v, want %v, %v", status, progress, zeroStatus, zeroProgress) + } +} + +func Test_status_durationString(t *testing.T) { + // zero duration + s := newStatus() + if d := s.durationString(); d != zeroDuration { + t.Errorf("status.durationString() = %v, want %v", d, zeroDuration) + } + + // not ended + s.startTime = time.Now().Add(-time.Second) + if d := s.durationString(); d == zeroDuration { + t.Errorf("status.durationString() = %v, want not %v", d, zeroDuration) + } + + // ended: 61 seconds + s.startTime = time.Now() + s.endTime = s.startTime.Add(61 * time.Second) + if d := s.durationString(); d != "1m1s" { + t.Errorf("status.durationString() = %v, want %v", d, "1m1s") + } + + // ended: 1001 Microsecond + s.startTime = time.Now() + s.endTime = s.startTime.Add(1001 * time.Microsecond) + if d := s.durationString(); d != "1ms" { + t.Errorf("status.durationString() = %v, want %v", d, "1ms") + } + + // ended: 1001 Nanosecond + s.startTime = time.Now() + s.endTime = s.startTime.Add(1001 * time.Nanosecond) + if d := s.durationString(); d != "1µs" { + t.Errorf("status.durationString() = %v, want %v", d, "1µs") + } +} From a7f3f967e7ff92d785e7a3006da9b9a19a1ace1a Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 08:04:56 +0000 Subject: [PATCH 32/98] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status_test.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 603fd9a81..79cfd305f 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -46,21 +46,24 @@ func Test_status_durationString(t *testing.T) { // ended: 61 seconds s.startTime = time.Now() s.endTime = s.startTime.Add(61 * time.Second) - if d := s.durationString(); d != "1m1s" { - t.Errorf("status.durationString() = %v, want %v", d, "1m1s") + want := "1m1s" + if d := s.durationString(); d != want { + t.Errorf("status.durationString() = %v, want %v", d, want) } // ended: 1001 Microsecond s.startTime = time.Now() s.endTime = s.startTime.Add(1001 * time.Microsecond) - if d := s.durationString(); d != "1ms" { - t.Errorf("status.durationString() = %v, want %v", d, "1ms") + want = "1ms" + if d := s.durationString(); d != want { + t.Errorf("status.durationString() = %v, want %v", d, want) } // ended: 1001 Nanosecond s.startTime = time.Now() s.endTime = s.startTime.Add(1001 * time.Nanosecond) - if d := s.durationString(); d != "1µs" { - t.Errorf("status.durationString() = %v, want %v", d, "1µs") + want = "1µs" + if d := s.durationString(); d != want { + t.Errorf("status.durationString() = %v, want %v", d, want) } } From 5183dc9069c933e37f0e863ddbf4620f771cc59e Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 08:43:27 +0000 Subject: [PATCH 33/98] add doc for stdout Signed-off-by: Billy Zha --- cmd/oras/internal/option/common.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 72b524c1a..4b0c4b154 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -49,6 +49,7 @@ func (opts *Common) WithContext(ctx context.Context) (context.Context, logrus.Fi // Parse gets target options from user input. func (opts *Common) Parse() error { + // use STDERR as TTY output since STDOUT is reserved for pipeable output return opts.parseTTY(os.Stderr) } From 1f45188471a77aaa70c3bb5762a068132a96e85c Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 08:57:58 +0000 Subject: [PATCH 34/98] add status tests Signed-off-by: Billy Zha --- .../display/console/testutils/testutils.go | 13 ++++--- cmd/oras/internal/display/progress/status.go | 4 +-- .../internal/display/progress/status_test.go | 35 +++++++++++++++++-- cmd/oras/root/blob/fetch_test.go | 2 +- cmd/oras/root/blob/push_test.go | 2 +- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/cmd/oras/internal/display/console/testutils/testutils.go b/cmd/oras/internal/display/console/testutils/testutils.go index 894b9aed4..9a0eb556a 100644 --- a/cmd/oras/internal/display/console/testutils/testutils.go +++ b/cmd/oras/internal/display/console/testutils/testutils.go @@ -42,8 +42,9 @@ func NewPty() (console.Console, *os.File, error) { return pty, slave, nil } -// OrderedMatch checks that the output from the pty matches the expected strings -func OrderedMatch(pty console.Console, slave *os.File, expected ...string) error { +// MatchPty checks that the output matches the expected strings in specified +// order. +func MatchPty(pty console.Console, slave *os.File, expected ...string) error { var wg sync.WaitGroup wg.Add(1) var buffer bytes.Buffer @@ -54,8 +55,12 @@ func OrderedMatch(pty console.Console, slave *os.File, expected ...string) error slave.Close() wg.Wait() - got := buffer.String() - for _, e := range expected { + return OrderedMatch(buffer.String()) +} + +// OrderedMatch matches the got with the expected strings in order. +func OrderedMatch(got string, want ...string) error { + for _, e := range want { i := strings.Index(got, e) if i < 0 { return fmt.Errorf("failed to find %q in %q", e, got) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 9a3466d91..3724a5df5 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -31,7 +31,7 @@ const ( barMaxLength = 40 zeroDuration = "0s" // default zero value of time.Duration.String() zeroStatus = "loading status..." - zeroProgress = "loading progress..." + zeroDigest = " └─ loading digest..." ) // status is used as message to update progress view. @@ -88,7 +88,7 @@ func (s *status) String(width int) (string, string) { defer s.lock.RUnlock() if s.isZero() { - return zeroStatus, zeroProgress + return zeroStatus, zeroDigest } // todo: doesn't support multiline prompt total := uint64(s.descriptor.Size) diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 79cfd305f..3798a9e88 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -19,14 +19,45 @@ import ( "testing" "time" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/console" + "oras.land/oras/cmd/oras/internal/display/console/testutils" ) func Test_status_String(t *testing.T) { // zero status and progress s := newStatus() - if status, progress := s.String(console.MinWidth); status != zeroStatus || progress != zeroProgress { - t.Errorf("status.String() = %v, %v, want %v, %v", status, progress, zeroStatus, zeroProgress) + if status, digest := s.String(console.MinWidth); status != zeroStatus || digest != zeroDigest { + t.Errorf("status.String() = %v, %v, want %v, %v", status, digest, zeroStatus, zeroDigest) + } + + // not done + s.startTime = time.Now().Add(-time.Minute) + s.prompt = "test" + s.descriptor = ocispec.Descriptor{ + MediaType: "application/vnd.oci.empty.oras.test.v1+json", + Size: 2, + Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + } + s.offset = 0 + // full name + status, digest := s.String(120) + if err := testutils.OrderedMatch(status+digest, " [\x1b[7m\x1b[0m........................................]", s.prompt, s.descriptor.MediaType, "0 B/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + t.Error(err) + } + // partial name + status, digest = s.String(console.MinWidth) + if err := testutils.OrderedMatch(status+digest, " [\x1b[7m\x1b[0m........................................]", s.prompt, "applicat.", "0 B/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + t.Error(err) + } + + // done + s.done = true + s.endTime = s.startTime.Add(time.Minute) + s.offset = s.descriptor.Size + status, digest = s.String(120) + if err := testutils.OrderedMatch(status+digest, "√", s.prompt, s.descriptor.MediaType, "2 B/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { + t.Error(err) } } diff --git a/cmd/oras/root/blob/fetch_test.go b/cmd/oras/root/blob/fetch_test.go index 0dcf17407..d0b752a13 100644 --- a/cmd/oras/root/blob/fetch_test.go +++ b/cmd/oras/root/blob/fetch_test.go @@ -61,7 +61,7 @@ func Test_fetchBlobOptions_doFetch(t *testing.T) { t.Fatal(err) } // validate - if err = testutils.OrderedMatch(pty, slave, "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + if err = testutils.MatchPty(pty, slave, "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } diff --git a/cmd/oras/root/blob/push_test.go b/cmd/oras/root/blob/push_test.go index 1bc83745c..12f462f9f 100644 --- a/cmd/oras/root/blob/push_test.go +++ b/cmd/oras/root/blob/push_test.go @@ -51,7 +51,7 @@ func Test_pushBlobOptions_doPush(t *testing.T) { t.Fatal(err) } // validate - if err = testutils.OrderedMatch(pty, slave, "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + if err = testutils.MatchPty(pty, slave, "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } From 7133edd5b99230224778b17a819279fb41d9d8cd Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 09:05:11 +0000 Subject: [PATCH 35/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status.go | 2 +- cmd/oras/internal/display/track/reader.go | 13 ++++++------- cmd/oras/internal/display/track/target.go | 6 +++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 3724a5df5..9e0d5c19a 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -54,7 +54,7 @@ func newStatus() *status { } // NewStatus generates a status. -func NewStatus(prompt string, descriptor ocispec.Descriptor, offset uint64) *status { +func NewStatus(prompt string, descriptor ocispec.Descriptor, offset int64) *status { return &status{ prompt: prompt, descriptor: descriptor, diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index cf995d209..b19784f60 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -18,7 +18,6 @@ package track import ( "io" "os" - "sync/atomic" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/progress" @@ -26,7 +25,7 @@ import ( type reader struct { base io.Reader - offset atomic.Uint64 + offset int64 actionPrompt string donePrompt string descriptor ocispec.Descriptor @@ -66,7 +65,7 @@ func (r *reader) StopManager() error { // Stop stops the status channel without closing the manager. func (r *reader) Stop() { - r.status <- progress.NewStatus(r.donePrompt, r.descriptor, uint64(r.descriptor.Size)) + r.status <- progress.NewStatus(r.donePrompt, r.descriptor, r.descriptor.Size) r.status <- progress.EndTiming() close(r.status) } @@ -83,17 +82,17 @@ func (r *reader) Read(p []byte) (int, error) { return n, err } - offset := r.offset.Add(uint64(n)) + r.offset = r.offset + int64(n) if err == io.EOF { - if offset != uint64(r.descriptor.Size) { + if r.offset != r.descriptor.Size { return n, io.ErrUnexpectedEOF } - r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) + r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, r.offset) } if len(r.status) < progress.BufferSize { // intermediate progress might be ignored if buffer is full - r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) + r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, r.offset) } return n, err } diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index c2e8bbd60..2d0b93dcc 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -74,7 +74,7 @@ func (t *target) Push(ctx context.Context, expected ocispec.Descriptor, content } r.status <- progress.EndTiming() - r.status <- progress.NewStatus(t.donePrompt, expected, uint64(expected.Size)) + r.status <- progress.NewStatus(t.donePrompt, expected, expected.Size) return nil } @@ -99,7 +99,7 @@ func (t *target) PushReference(ctx context.Context, expected ocispec.Descriptor, } r.status <- progress.EndTiming() - r.status <- progress.NewStatus(t.donePrompt, expected, uint64(expected.Size)) + r.status <- progress.NewStatus(t.donePrompt, expected, expected.Size) return nil } @@ -123,7 +123,7 @@ func (t *target) Prompt(desc ocispec.Descriptor, prompt string) error { return err } defer close(status) - status <- progress.NewStatus(prompt, desc, uint64(desc.Size)) + status <- progress.NewStatus(prompt, desc, desc.Size) status <- progress.EndTiming() return nil } From 3ad8eaa58703446c002a189dfc0efbccfdb48594 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 08:43:27 +0000 Subject: [PATCH 36/98] add doc for stdout Signed-off-by: Billy Zha --- cmd/oras/internal/option/common.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 72b524c1a..4b0c4b154 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -49,6 +49,7 @@ func (opts *Common) WithContext(ctx context.Context) (context.Context, logrus.Fi // Parse gets target options from user input. func (opts *Common) Parse() error { + // use STDERR as TTY output since STDOUT is reserved for pipeable output return opts.parseTTY(os.Stderr) } From 243dc3784011bea081be1c355e03a69a8facbb6d Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 08:57:58 +0000 Subject: [PATCH 37/98] add status tests Signed-off-by: Billy Zha --- .../display/console/testutils/testutils.go | 13 ++++--- cmd/oras/internal/display/progress/status.go | 4 +-- .../internal/display/progress/status_test.go | 35 +++++++++++++++++-- cmd/oras/root/blob/fetch_test.go | 2 +- cmd/oras/root/blob/push_test.go | 2 +- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/cmd/oras/internal/display/console/testutils/testutils.go b/cmd/oras/internal/display/console/testutils/testutils.go index 894b9aed4..9a0eb556a 100644 --- a/cmd/oras/internal/display/console/testutils/testutils.go +++ b/cmd/oras/internal/display/console/testutils/testutils.go @@ -42,8 +42,9 @@ func NewPty() (console.Console, *os.File, error) { return pty, slave, nil } -// OrderedMatch checks that the output from the pty matches the expected strings -func OrderedMatch(pty console.Console, slave *os.File, expected ...string) error { +// MatchPty checks that the output matches the expected strings in specified +// order. +func MatchPty(pty console.Console, slave *os.File, expected ...string) error { var wg sync.WaitGroup wg.Add(1) var buffer bytes.Buffer @@ -54,8 +55,12 @@ func OrderedMatch(pty console.Console, slave *os.File, expected ...string) error slave.Close() wg.Wait() - got := buffer.String() - for _, e := range expected { + return OrderedMatch(buffer.String()) +} + +// OrderedMatch matches the got with the expected strings in order. +func OrderedMatch(got string, want ...string) error { + for _, e := range want { i := strings.Index(got, e) if i < 0 { return fmt.Errorf("failed to find %q in %q", e, got) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 9a3466d91..3724a5df5 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -31,7 +31,7 @@ const ( barMaxLength = 40 zeroDuration = "0s" // default zero value of time.Duration.String() zeroStatus = "loading status..." - zeroProgress = "loading progress..." + zeroDigest = " └─ loading digest..." ) // status is used as message to update progress view. @@ -88,7 +88,7 @@ func (s *status) String(width int) (string, string) { defer s.lock.RUnlock() if s.isZero() { - return zeroStatus, zeroProgress + return zeroStatus, zeroDigest } // todo: doesn't support multiline prompt total := uint64(s.descriptor.Size) diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 79cfd305f..3798a9e88 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -19,14 +19,45 @@ import ( "testing" "time" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/console" + "oras.land/oras/cmd/oras/internal/display/console/testutils" ) func Test_status_String(t *testing.T) { // zero status and progress s := newStatus() - if status, progress := s.String(console.MinWidth); status != zeroStatus || progress != zeroProgress { - t.Errorf("status.String() = %v, %v, want %v, %v", status, progress, zeroStatus, zeroProgress) + if status, digest := s.String(console.MinWidth); status != zeroStatus || digest != zeroDigest { + t.Errorf("status.String() = %v, %v, want %v, %v", status, digest, zeroStatus, zeroDigest) + } + + // not done + s.startTime = time.Now().Add(-time.Minute) + s.prompt = "test" + s.descriptor = ocispec.Descriptor{ + MediaType: "application/vnd.oci.empty.oras.test.v1+json", + Size: 2, + Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + } + s.offset = 0 + // full name + status, digest := s.String(120) + if err := testutils.OrderedMatch(status+digest, " [\x1b[7m\x1b[0m........................................]", s.prompt, s.descriptor.MediaType, "0 B/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + t.Error(err) + } + // partial name + status, digest = s.String(console.MinWidth) + if err := testutils.OrderedMatch(status+digest, " [\x1b[7m\x1b[0m........................................]", s.prompt, "applicat.", "0 B/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + t.Error(err) + } + + // done + s.done = true + s.endTime = s.startTime.Add(time.Minute) + s.offset = s.descriptor.Size + status, digest = s.String(120) + if err := testutils.OrderedMatch(status+digest, "√", s.prompt, s.descriptor.MediaType, "2 B/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { + t.Error(err) } } diff --git a/cmd/oras/root/blob/fetch_test.go b/cmd/oras/root/blob/fetch_test.go index 0dcf17407..d0b752a13 100644 --- a/cmd/oras/root/blob/fetch_test.go +++ b/cmd/oras/root/blob/fetch_test.go @@ -61,7 +61,7 @@ func Test_fetchBlobOptions_doFetch(t *testing.T) { t.Fatal(err) } // validate - if err = testutils.OrderedMatch(pty, slave, "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + if err = testutils.MatchPty(pty, slave, "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } diff --git a/cmd/oras/root/blob/push_test.go b/cmd/oras/root/blob/push_test.go index 1bc83745c..12f462f9f 100644 --- a/cmd/oras/root/blob/push_test.go +++ b/cmd/oras/root/blob/push_test.go @@ -51,7 +51,7 @@ func Test_pushBlobOptions_doPush(t *testing.T) { t.Fatal(err) } // validate - if err = testutils.OrderedMatch(pty, slave, "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + if err = testutils.MatchPty(pty, slave, "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } From 5f5a1d9a23ebdbf106ee7c8fee4e160eb31a8ef0 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 09:05:11 +0000 Subject: [PATCH 38/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status.go | 2 +- cmd/oras/internal/display/track/reader.go | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 3724a5df5..9e0d5c19a 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -54,7 +54,7 @@ func newStatus() *status { } // NewStatus generates a status. -func NewStatus(prompt string, descriptor ocispec.Descriptor, offset uint64) *status { +func NewStatus(prompt string, descriptor ocispec.Descriptor, offset int64) *status { return &status{ prompt: prompt, descriptor: descriptor, diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index cf995d209..b19784f60 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -18,7 +18,6 @@ package track import ( "io" "os" - "sync/atomic" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/progress" @@ -26,7 +25,7 @@ import ( type reader struct { base io.Reader - offset atomic.Uint64 + offset int64 actionPrompt string donePrompt string descriptor ocispec.Descriptor @@ -66,7 +65,7 @@ func (r *reader) StopManager() error { // Stop stops the status channel without closing the manager. func (r *reader) Stop() { - r.status <- progress.NewStatus(r.donePrompt, r.descriptor, uint64(r.descriptor.Size)) + r.status <- progress.NewStatus(r.donePrompt, r.descriptor, r.descriptor.Size) r.status <- progress.EndTiming() close(r.status) } @@ -83,17 +82,17 @@ func (r *reader) Read(p []byte) (int, error) { return n, err } - offset := r.offset.Add(uint64(n)) + r.offset = r.offset + int64(n) if err == io.EOF { - if offset != uint64(r.descriptor.Size) { + if r.offset != r.descriptor.Size { return n, io.ErrUnexpectedEOF } - r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) + r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, r.offset) } if len(r.status) < progress.BufferSize { // intermediate progress might be ignored if buffer is full - r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, offset) + r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, r.offset) } return n, err } From 75545349349ba143e0a69534ffff95cd31c47edc Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 09:50:46 +0000 Subject: [PATCH 39/98] NIT Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 9e0d5c19a..403e319ab 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -46,7 +46,7 @@ type status struct { lock sync.RWMutex } -// newStatus generates a base empty status +// newStatus generates a base empty status. func newStatus() *status { return &status{ offset: -1, From 76de650ccc1c24d3a34bb6f6219a20ade834902c Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 10:08:19 +0000 Subject: [PATCH 40/98] align timing and percentage Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 403e319ab..f603cee11 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -120,7 +120,7 @@ func (s *status) String(width int) (string, string) { // mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3 lenLeft += 3 + utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) - right := fmt.Sprintf(" %s/%s %6.2f%% %s", humanize.Bytes(uint64(s.offset)), humanize.Bytes(total), percent*100, s.durationString()) + right := fmt.Sprintf(" %s/%s %6.2f%% %6s", humanize.Bytes(uint64(s.offset)), humanize.Bytes(total), percent*100, s.durationString()) lenRight := utf8.RuneCountInString(right) lenMargin := width - lenLeft - lenRight if lenMargin < 0 { From 486a8edb5a45876a66ef8974860714acd4a40efa Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 14:22:14 +0000 Subject: [PATCH 41/98] fix unit test Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 3798a9e88..78cdab07b 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -47,7 +47,7 @@ func Test_status_String(t *testing.T) { } // partial name status, digest = s.String(console.MinWidth) - if err := testutils.OrderedMatch(status+digest, " [\x1b[7m\x1b[0m........................................]", s.prompt, "applicat.", "0 B/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + if err := testutils.OrderedMatch(status+digest, " [\x1b[7m\x1b[0m........................................]", s.prompt, "applic.", "0 B/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } From 4fe990e799f8300096365047f7fd13aa3e4c830d Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sat, 7 Oct 2023 14:31:47 +0000 Subject: [PATCH 42/98] increase coverage Signed-off-by: Billy Zha --- cmd/oras/internal/option/common_unix_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/oras/internal/option/common_unix_test.go b/cmd/oras/internal/option/common_unix_test.go index 9851d7629..fd9376eb8 100644 --- a/cmd/oras/internal/option/common_unix_test.go +++ b/cmd/oras/internal/option/common_unix_test.go @@ -30,9 +30,15 @@ func TestCommon_parseTTY(t *testing.T) { } defer slave.Close() var opts Common - // test + + // TTY output + if err := opts.parseTTY(slave); err != nil { + t.Errorf("unexpected error with TTY output: %v", err) + } + + // --debug opts.Debug = true if err := opts.parseTTY(slave); err == nil { - t.Fatal("expected error when debug is set to TTY output") + t.Error("expected error when debug is set with TTY output") } } From 3a7ed21f13858fd5ce7ded4ae42690d661bd2935 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 03:09:28 +0000 Subject: [PATCH 43/98] fix: won't show complete when aborted Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 12 ++++++--- cmd/oras/root/blob/fetch.go | 31 ++++++++++++++--------- cmd/oras/root/blob/push.go | 27 +++++++++++--------- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index b19784f60..f0be3d530 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -59,14 +59,18 @@ func managedReader(r io.Reader, descriptor ocispec.Descriptor, manager progress. } // StopManager stops the status channel and related manager. -func (r *reader) StopManager() error { - return r.manager.Close() +func (r *reader) StopManager() { + _ = r.manager.Close() } -// Stop stops the status channel without closing the manager. -func (r *reader) Stop() { +// Done sends message to mark the tracked progress as complete. +func (r *reader) Done() { r.status <- progress.NewStatus(r.donePrompt, r.descriptor, r.descriptor.Size) r.status <- progress.EndTiming() +} + +// Close closes the update channel. +func (r *reader) Close() { close(r.status) } diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 18e6c4282..8fd07af9b 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -169,25 +169,32 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg }() var r io.Reader = vr - if opts.TTY != nil { - trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ", opts.TTY) + switch opts.TTY { + case nil: + if _, err := io.Copy(file, r); err != nil { + return ocispec.Descriptor{}, err + } + if err := vr.Verify(); err != nil { + return ocispec.Descriptor{}, err + } + default: + trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ", opts.TTY) if err != nil { return ocispec.Descriptor{}, err } - defer func() { - trackedReader.Stop() - _ = trackedReader.StopManager() - }() + defer trackedReader.StopManager() + defer trackedReader.Close() trackedReader.Start() r = trackedReader + if _, err := io.Copy(file, r); err != nil { + return ocispec.Descriptor{}, err + } + if err := vr.Verify(); err != nil { + return ocispec.Descriptor{}, err + } + trackedReader.Done() } - if _, err := io.Copy(file, r); err != nil { - return ocispec.Descriptor{}, err - } - if err := vr.Verify(); err != nil { - return ocispec.Descriptor{}, err - } } return desc, nil } diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 207e4fc7d..2b78b35f1 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -143,29 +143,32 @@ func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { // doPush pushes a blob to a registry or an OCI image layout func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc ocispec.Descriptor, r io.Reader) error { - if opts.TTY == nil { + switch opts.TTY { + case nil: + // none tty output if err := display.PrintStatus(desc, "Uploading", opts.Verbose); err != nil { return err } - } else { + if err := t.Push(ctx, desc, r); err != nil { + return err + } + if err := display.PrintStatus(desc, "Uploaded ", opts.Verbose); err != nil { + return err + } + default: + // tty output trackedReader, err := track.NewReader(r, desc, "Uploading", "Uploaded ", opts.TTY) if err != nil { return err } - defer func() { - trackedReader.Stop() - _ = trackedReader.StopManager() - }() + defer trackedReader.StopManager() + defer trackedReader.Close() trackedReader.Start() r = trackedReader - } - if err := t.Push(ctx, desc, r); err != nil { - return err - } - if opts.TTY == nil { - if err := display.PrintStatus(desc, "Uploaded ", opts.Verbose); err != nil { + if err := t.Push(ctx, desc, r); err != nil { return err } + trackedReader.Done() } return nil } From 455ca0810e1f84d1fb0bf7e0e17ed71d42e334dd Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 03:14:21 +0000 Subject: [PATCH 44/98] fix: won't show complete when aborted Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index 2d0b93dcc..ca7805c2e 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -67,14 +67,12 @@ func (t *target) Push(ctx context.Context, expected ocispec.Descriptor, content if err != nil { return err } - defer r.Stop() + defer r.Close() r.Start() if err := t.Target.Push(ctx, expected, r); err != nil { return err } - - r.status <- progress.EndTiming() - r.status <- progress.NewStatus(t.donePrompt, expected, expected.Size) + r.Done() return nil } @@ -83,7 +81,7 @@ func (t *target) PushReference(ctx context.Context, expected ocispec.Descriptor, if err != nil { return err } - defer r.Stop() + defer r.Close() r.Start() if rp, ok := t.Target.(registry.ReferencePusher); ok { err = rp.PushReference(ctx, expected, r, reference) @@ -93,13 +91,10 @@ func (t *target) PushReference(ctx context.Context, expected ocispec.Descriptor, } err = t.Target.Tag(ctx, expected, reference) } - if err != nil { return err } - - r.status <- progress.EndTiming() - r.status <- progress.NewStatus(t.donePrompt, expected, expected.Size) + r.Done() return nil } From ff2c230fff77115798b92d9b26575403144bc879 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 03:23:27 +0000 Subject: [PATCH 45/98] code clean Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 4 ++-- cmd/oras/root/blob/push.go | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 8fd07af9b..deeb879d0 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -170,14 +170,14 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg var r io.Reader = vr switch opts.TTY { - case nil: + case nil: // none tty output if _, err := io.Copy(file, r); err != nil { return ocispec.Descriptor{}, err } if err := vr.Verify(); err != nil { return ocispec.Descriptor{}, err } - default: + default: // tty output trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ", opts.TTY) if err != nil { return ocispec.Descriptor{}, err diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 2b78b35f1..1fe9299bd 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -144,8 +144,7 @@ func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { // doPush pushes a blob to a registry or an OCI image layout func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc ocispec.Descriptor, r io.Reader) error { switch opts.TTY { - case nil: - // none tty output + case nil: // none tty output if err := display.PrintStatus(desc, "Uploading", opts.Verbose); err != nil { return err } @@ -155,8 +154,7 @@ func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc oci if err := display.PrintStatus(desc, "Uploaded ", opts.Verbose); err != nil { return err } - default: - // tty output + default: // tty output trackedReader, err := track.NewReader(r, desc, "Uploading", "Uploaded ", opts.TTY) if err != nil { return err From 9426facbfe9665ba975f01477f1cb6696c9c4ff6 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 03:24:22 +0000 Subject: [PATCH 46/98] code clean Signed-off-by: Billy Zha --- cmd/oras/root/blob/push.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 1fe9299bd..29b061aaf 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -141,7 +141,6 @@ func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { return nil } -// doPush pushes a blob to a registry or an OCI image layout func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc ocispec.Descriptor, r io.Reader) error { switch opts.TTY { case nil: // none tty output From 50c4181dc6f570bc6761a03dd32612807b9ac80c Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 03:30:03 +0000 Subject: [PATCH 47/98] code clean Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index deeb879d0..595505fa3 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -168,33 +168,27 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg } }() - var r io.Reader = vr switch opts.TTY { case nil: // none tty output - if _, err := io.Copy(file, r); err != nil { - return ocispec.Descriptor{}, err - } - if err := vr.Verify(); err != nil { + if _, err = io.Copy(file, vr); err != nil { return ocispec.Descriptor{}, err } default: // tty output - trackedReader, err := track.NewReader(r, desc, "Downloading", "Downloaded ", opts.TTY) + trackedReader, err := track.NewReader(vr, desc, "Downloading", "Downloaded ", opts.TTY) if err != nil { return ocispec.Descriptor{}, err } defer trackedReader.StopManager() defer trackedReader.Close() trackedReader.Start() - r = trackedReader - if _, err := io.Copy(file, r); err != nil { - return ocispec.Descriptor{}, err - } - if err := vr.Verify(); err != nil { + if _, err = io.Copy(file, trackedReader); err != nil { return ocispec.Descriptor{}, err } trackedReader.Done() } - + if err := vr.Verify(); err != nil { + return ocispec.Descriptor{}, err + } } return desc, nil } From aa5b8478dc26717f1ad133ce2ca4c4064cb2e436 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 03:36:22 +0000 Subject: [PATCH 48/98] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index f603cee11..745a97de4 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -28,7 +28,7 @@ import ( ) const ( - barMaxLength = 40 + barLength = 40 zeroDuration = "0s" // default zero value of time.Duration.String() zeroStatus = "loading status..." zeroDigest = " └─ loading digest..." @@ -102,23 +102,23 @@ func (s *status) String(width int) (string, string) { name = s.descriptor.MediaType } - // format: [left-------------------------------][margin][right-----------------------------] - // mark(1) bar(42) action(<10) name(126) size_per_size(19) percent(8) time(8) + // format: [left--------------------------------][margin][right-------------------------------] + // mark(1) bar(42) action(<10) name(<126) size_per_size(19) percent(8) time(>=6) // └─ digest(72) var left string - var lenLeft int + lenLeft := 0 if !s.done { - lenBar := int(percent * barMaxLength) - bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barMaxLength-lenBar)) + lenBar := int(percent * barLength) + bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barLength-lenBar)) mark := s.mark.symbol() left = fmt.Sprintf("%c %s %s %s", mark, bar, s.prompt, name) // bar + wrapper(2) + space(1) = len(bar) + 3 - lenLeft = barMaxLength + 3 + lenLeft = barLength + 3 } else { left = fmt.Sprintf("√ %s %s", s.prompt, name) } // mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3 - lenLeft += 3 + utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) + lenLeft += utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) + 3 right := fmt.Sprintf(" %s/%s %6.2f%% %6s", humanize.Bytes(uint64(s.offset)), humanize.Bytes(total), percent*100, s.durationString()) lenRight := utf8.RuneCountInString(right) From f49bdcb97e5c2f20d6f3f17e3e015637e1564317 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 06:12:21 +0000 Subject: [PATCH 49/98] align units of processed data to total size Signed-off-by: Billy Zha --- .../internal/display/progress/humanize.go | 51 +++++++++++++++++++ .../display/progress/humanize_test.go | 41 +++++++++++++++ cmd/oras/internal/display/progress/status.go | 29 +++++++---- .../internal/display/progress/status_test.go | 39 +++++++------- go.mod | 1 - go.sum | 2 - 6 files changed, 134 insertions(+), 29 deletions(-) create mode 100644 cmd/oras/internal/display/progress/humanize.go create mode 100644 cmd/oras/internal/display/progress/humanize_test.go diff --git a/cmd/oras/internal/display/progress/humanize.go b/cmd/oras/internal/display/progress/humanize.go new file mode 100644 index 000000000..3e80a20b5 --- /dev/null +++ b/cmd/oras/internal/display/progress/humanize.go @@ -0,0 +1,51 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package progress + +import ( + "math" +) + +var ( + units = []string{"B", "kB", "MB", "GB", "TB"} + base = 1000.0 +) + +type bytes struct { + size float64 + unit string +} + +// ToBytes converts size in bytes to human readable format. +func ToBytes(sizeInBytes int64) bytes { + f := float64(sizeInBytes) + if f < base { + return bytes{f, "B"} + } + e := math.Floor(math.Log(f) / math.Log(base)) + p := f / math.Pow(base, e) + return bytes{RoundTo(p), units[int(e)]} +} + +// RoundTo makes length of the size string to less than or equal to 4. +func RoundTo(size float64) float64 { + if size < 10 { + return math.Round(size*100) / 100 + } else if size < 100 { + return math.Round(size*10) / 10 + } + return math.Round(size) +} diff --git a/cmd/oras/internal/display/progress/humanize_test.go b/cmd/oras/internal/display/progress/humanize_test.go new file mode 100644 index 000000000..547b777bc --- /dev/null +++ b/cmd/oras/internal/display/progress/humanize_test.go @@ -0,0 +1,41 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package progress + +import "testing" + +func TestRoundTo(t *testing.T) { + type args struct { + quantity float64 + } + tests := []struct { + name string + args args + want float64 + }{ + {"round to 2 digit", args{1.223}, 1.22}, + {"round to 1 digit", args{12.23}, 12.2}, + {"round to no digit", args{122.6}, 123}, + {"round to no digit", args{1223.123}, 1223}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := RoundTo(tt.args.quantity); got != tt.want { + t.Errorf("RoundTo() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 745a97de4..1c00d5d33 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -22,7 +22,6 @@ import ( "time" "unicode/utf8" - "github.com/dustin/go-humanize" "github.com/morikuni/aec" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -40,10 +39,12 @@ type status struct { prompt string descriptor ocispec.Descriptor offset int64 - startTime time.Time - endTime time.Time - mark spinner - lock sync.RWMutex + total bytes + + startTime time.Time + endTime time.Time + mark spinner + lock sync.RWMutex } // newStatus generates a base empty status. @@ -58,7 +59,7 @@ func NewStatus(prompt string, descriptor ocispec.Descriptor, offset int64) *stat return &status{ prompt: prompt, descriptor: descriptor, - offset: int64(offset), + offset: offset, } } @@ -102,8 +103,8 @@ func (s *status) String(width int) (string, string) { name = s.descriptor.MediaType } - // format: [left--------------------------------][margin][right-------------------------------] - // mark(1) bar(42) action(<10) name(<126) size_per_size(19) percent(8) time(>=6) + // format: [left--------------------------------][margin][right---------------------------------] + // mark(1) bar(42) action(<10) name(<126) size_per_size(<=11) percent(8) time(>=6) // └─ digest(72) var left string lenLeft := 0 @@ -120,7 +121,14 @@ func (s *status) String(width int) (string, string) { // mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3 lenLeft += utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) + 3 - right := fmt.Sprintf(" %s/%s %6.2f%% %6s", humanize.Bytes(uint64(s.offset)), humanize.Bytes(total), percent*100, s.durationString()) + var offset string + switch percent { + case 1: // 100%, show exact size + offset = fmt.Sprint(s.total.size) + default: // 0% ~ 99%, show 2-digit precision + offset = fmt.Sprintf("%.2f", RoundTo(s.total.size*percent)) + } + right := fmt.Sprintf(" %s/%v %s %6.2f%% %6s", offset, s.total.size, s.total.unit, percent*100, s.durationString()) lenRight := utf8.RuneCountInString(right) lenMargin := width - lenLeft - lenRight if lenMargin < 0 { @@ -162,6 +170,9 @@ func (s *status) Update(n *status) { if n.offset >= 0 { s.offset = n.offset + if n.descriptor.Size != s.descriptor.Size { + s.total = ToBytes(n.descriptor.Size) + } s.descriptor = n.descriptor } if n.prompt != "" { diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 78cdab07b..03dac6585 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -32,31 +32,36 @@ func Test_status_String(t *testing.T) { } // not done - s.startTime = time.Now().Add(-time.Minute) - s.prompt = "test" - s.descriptor = ocispec.Descriptor{ - MediaType: "application/vnd.oci.empty.oras.test.v1+json", - Size: 2, - Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - } - s.offset = 0 + s.Update(&status{ + prompt: "test", + descriptor: ocispec.Descriptor{ + MediaType: "application/vnd.oci.empty.oras.test.v1+json", + Size: 2, + Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + }, + startTime: time.Now().Add(-time.Minute), + offset: 0, + total: ToBytes(2), + }) // full name - status, digest := s.String(120) - if err := testutils.OrderedMatch(status+digest, " [\x1b[7m\x1b[0m........................................]", s.prompt, s.descriptor.MediaType, "0 B/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + statusStr, digestStr := s.String(120) + if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m........................................]", s.prompt, s.descriptor.MediaType, "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } // partial name - status, digest = s.String(console.MinWidth) - if err := testutils.OrderedMatch(status+digest, " [\x1b[7m\x1b[0m........................................]", s.prompt, "applic.", "0 B/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + statusStr, digestStr = s.String(console.MinWidth) + if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m........................................]", s.prompt, "appli.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } // done - s.done = true - s.endTime = s.startTime.Add(time.Minute) - s.offset = s.descriptor.Size - status, digest = s.String(120) - if err := testutils.OrderedMatch(status+digest, "√", s.prompt, s.descriptor.MediaType, "2 B/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { + s.Update(&status{ + endTime: time.Now(), + offset: s.descriptor.Size, + descriptor: s.descriptor, + }) + statusStr, digestStr = s.String(120) + if err := testutils.OrderedMatch(statusStr+digestStr, "√", s.prompt, s.descriptor.MediaType, "2/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } } diff --git a/go.mod b/go.mod index ea357224b..dcd8cdf22 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.21 require ( github.com/containerd/console v1.0.3 - github.com/dustin/go-humanize v1.0.1 github.com/morikuni/aec v1.0.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc4 diff --git a/go.sum b/go.sum index 05f19e30e..9982f94c0 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= From 8bc5c37e8f6d8c230efd9bbc0402f31e37f6e6d3 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 07:33:29 +0000 Subject: [PATCH 50/98] add instant download speed Signed-off-by: Billy Zha --- .../internal/display/progress/humanize.go | 2 +- cmd/oras/internal/display/progress/manager.go | 2 +- cmd/oras/internal/display/progress/status.go | 69 ++++++++++++------- .../internal/display/progress/status_test.go | 4 +- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/cmd/oras/internal/display/progress/humanize.go b/cmd/oras/internal/display/progress/humanize.go index 3e80a20b5..d769c83a5 100644 --- a/cmd/oras/internal/display/progress/humanize.go +++ b/cmd/oras/internal/display/progress/humanize.go @@ -33,7 +33,7 @@ type bytes struct { func ToBytes(sizeInBytes int64) bytes { f := float64(sizeInBytes) if f < base { - return bytes{f, "B"} + return bytes{f, units[0]} } e := math.Floor(math.Log(f) / math.Log(base)) p := f / math.Pow(base, e) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index 7ecca7bcc..b8b4ffd59 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -38,7 +38,7 @@ type Manager interface { Close() error } -const bufFlushDuration = 100 * time.Millisecond +const bufFlushDuration = 500 * time.Millisecond type manager struct { status []*status diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 1c00d5d33..3aba785d5 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -27,7 +27,8 @@ import ( ) const ( - barLength = 40 + barLength = 20 + speedLength = 8 zeroDuration = "0s" // default zero value of time.Duration.String() zeroStatus = "loading status..." zeroDigest = " └─ loading digest..." @@ -35,22 +36,25 @@ const ( // status is used as message to update progress view. type status struct { - done bool // done is true when the end time is set - prompt string - descriptor ocispec.Descriptor - offset int64 - total bytes + done bool // done is true when the end time is set + prompt string + descriptor ocispec.Descriptor + offset int64 + total bytes + lastOffset int64 + lastRenderTime time.Time startTime time.Time endTime time.Time mark spinner - lock sync.RWMutex + lock sync.Mutex } // newStatus generates a base empty status. func newStatus() *status { return &status{ - offset: -1, + offset: -1, + lastRenderTime: time.Now(), } } @@ -85,8 +89,8 @@ func (s *status) isZero() bool { // String returns human-readable TTY strings of the status. func (s *status) String(width int) (string, string) { - s.lock.RLock() - defer s.lock.RUnlock() + s.lock.Lock() + defer s.lock.Unlock() if s.isZero() { return zeroStatus, zeroDigest @@ -103,33 +107,35 @@ func (s *status) String(width int) (string, string) { name = s.descriptor.MediaType } - // format: [left--------------------------------][margin][right---------------------------------] - // mark(1) bar(42) action(<10) name(<126) size_per_size(<=11) percent(8) time(>=6) + // format: [left--------------------------------------------][margin][right---------------------------------] + // mark(1) bar(22) speed(8) action(<=11) name(<126) size_per_size(<=13) percent(8) time(>=6) // └─ digest(72) + var offset string + switch percent { + case 1: // 100%, show exact size + offset = fmt.Sprint(s.total.size) + default: // 0% ~ 99%, show 2-digit precision + offset = fmt.Sprintf("%.2f", RoundTo(s.total.size*percent)) + } + right := fmt.Sprintf(" %s/%v %s %6.2f%% %6s", offset, s.total.size, s.total.unit, percent*100, s.durationString()) + lenRight := utf8.RuneCountInString(right) + var left string lenLeft := 0 if !s.done { lenBar := int(percent * barLength) bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barLength-lenBar)) - mark := s.mark.symbol() - left = fmt.Sprintf("%c %s %s %s", mark, bar, s.prompt, name) - // bar + wrapper(2) + space(1) = len(bar) + 3 - lenLeft = barLength + 3 + speed := s.calculateSpeed() + speedStr := fmt.Sprintf("%v%s/s", speed.size, speed.unit) + left = fmt.Sprintf("%c %s(%*s) %s %s", s.mark.symbol(), bar, speedLength, speedStr, s.prompt, name) + // bar + wrapper(2) + space(1) + speed + wrapper(2) = len(bar) + len(speed) + 5 + lenLeft = barLength + speedLength + 5 } else { left = fmt.Sprintf("√ %s %s", s.prompt, name) } // mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3 lenLeft += utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) + 3 - var offset string - switch percent { - case 1: // 100%, show exact size - offset = fmt.Sprint(s.total.size) - default: // 0% ~ 99%, show 2-digit precision - offset = fmt.Sprintf("%.2f", RoundTo(s.total.size*percent)) - } - right := fmt.Sprintf(" %s/%v %s %6.2f%% %6s", offset, s.total.size, s.total.unit, percent*100, s.durationString()) - lenRight := utf8.RuneCountInString(right) lenMargin := width - lenLeft - lenRight if lenMargin < 0 { // hide partial name with one space left @@ -139,6 +145,19 @@ func (s *status) String(width int) (string, string) { return fmt.Sprintf("%s%s%s", left, strings.Repeat(" ", lenMargin), right), fmt.Sprintf(" └─ %s", s.descriptor.Digest.String()) } +// calculateSpeed calculates the speed of the progress and update last status. +// caller must hold the lock. +func (s *status) calculateSpeed() bytes { + now := time.Now() + secondsTaken := now.Sub(s.lastRenderTime).Seconds() + bytes := float64(s.offset - s.lastOffset) + + s.lastOffset = s.offset + s.lastRenderTime = now + + return ToBytes(int64(bytes / secondsTaken)) +} + // durationString returns a viewable TTY string of the status with duration. func (s *status) durationString() string { if s.startTime.IsZero() { diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 03dac6585..25033beb6 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -45,12 +45,12 @@ func Test_status_String(t *testing.T) { }) // full name statusStr, digestStr := s.String(120) - if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m........................................]", s.prompt, s.descriptor.MediaType, "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, s.descriptor.MediaType, "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } // partial name statusStr, digestStr = s.String(console.MinWidth) - if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m........................................]", s.prompt, "appli.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, "application/vnd.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } From e1486cc4133e35fecef2d17654460e891d51e538 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 07:50:19 +0000 Subject: [PATCH 51/98] remove unnecessary file Signed-off-by: Billy Zha --- cmd/oras/internal/display/prompt/prompt.go | 60 ---------------------- 1 file changed, 60 deletions(-) delete mode 100644 cmd/oras/internal/display/prompt/prompt.go diff --git a/cmd/oras/internal/display/prompt/prompt.go b/cmd/oras/internal/display/prompt/prompt.go deleted file mode 100644 index 250b81638..000000000 --- a/cmd/oras/internal/display/prompt/prompt.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package prompt - -import ( - "fmt" - "reflect" -) - -var () - -type prompt struct { - Copying, Skipped, Exists, Copied, Downloading, Downloaded string -} - -func New() *prompt { - var p prompt - p.pad() - return &p -} - -// pad appends the prompt strings with spaces to make them the same length. -func (p *prompt) pad() { - v := reflect.ValueOf(p).Elem() - t := v.Type() - maxLen := 0 - - for i := 0; i < v.NumField(); i++ { - f := v.Field(i) - if f.IsValid() && f.CanSet() && f.Kind() == reflect.String { - sf := t.Field(i) - len := len(sf.Name) - if len > maxLen { - maxLen = len - } - } - - } - - for i := 0; i < v.NumField(); i++ { - f := v.Field(i) - if f.IsValid() && f.CanSet() && f.Kind() == reflect.String { - sf := t.Field(i) - f.SetString(fmt.Sprintf("%-*s", maxLen, sf.Name)) - } - } -} From 3193b482ca7b453a46b2bcccf24a3abadd7886b8 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 08:46:09 +0000 Subject: [PATCH 52/98] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/print.go | 17 --------- cmd/oras/internal/display/track/target.go | 42 ++++++++++------------ cmd/oras/root/cp.go | 44 ++++++++++++----------- 3 files changed, 41 insertions(+), 62 deletions(-) diff --git a/cmd/oras/internal/display/print.go b/cmd/oras/internal/display/print.go index 6bbbb757d..48374a327 100644 --- a/cmd/oras/internal/display/print.go +++ b/cmd/oras/internal/display/print.go @@ -25,7 +25,6 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry" - "oras.land/oras/cmd/oras/internal/display/track" ) var printLock sync.Mutex @@ -58,22 +57,6 @@ func PrintStatus(desc ocispec.Descriptor, status string, verbose bool) error { return Print(status, ShortDigest(desc), name) } -// PrintStatusWithoutTrackable prints transfer status if trackable is no set. -func PrintStatusWithoutTrackable(desc ocispec.Descriptor, status string, verbose bool, trackable track.Trackable) error { - if trackable != nil { - return nil - } - return PrintStatus(desc, status, verbose) -} - -// PrintStatusWithTrackable prints transfer status if trackable is set. -func PrintStatusWithTrackable(desc ocispec.Descriptor, status string, verbose bool, trackable track.Trackable) error { - if trackable == nil { - return PrintStatus(desc, status, verbose) - } - return trackable.Prompt(desc, status) -} - // PrintSuccessorStatus prints transfer status of successors. func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, status string, fetcher content.Fetcher, committed *sync.Map, verbose bool) error { successors, err := content.Successors(ctx, fetcher, desc) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index ca7805c2e..f3d3be6f3 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -26,35 +26,25 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry" + "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/display/progress" ) // Trackable can be tracked and supprots explicit prompting and stoping. -type Trackable interface { - Prompt(desc ocispec.Descriptor, prompt string) error - Close() error -} - -// Target is a wrapper for oras.Target with tracked pushing. -type Target interface { - oras.GraphTarget - Trackable -} - -type target struct { +type Target struct { oras.Target manager progress.Manager actionPrompt string donePrompt string } -func NewTarget(t oras.Target, actionPrompt, donePrompt string, tty *os.File) (Target, error) { +func NewTarget(t oras.Target, actionPrompt, donePrompt string, tty *os.File) (*Target, error) { manager, err := progress.NewManager(tty) if err != nil { return nil, err } - return &target{ + return &Target{ Target: t, manager: manager, actionPrompt: actionPrompt, @@ -62,7 +52,7 @@ func NewTarget(t oras.Target, actionPrompt, donePrompt string, tty *os.File) (Ta }, nil } -func (t *target) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { +func (t *Target) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) if err != nil { return err @@ -76,7 +66,7 @@ func (t *target) Push(ctx context.Context, expected ocispec.Descriptor, content return nil } -func (t *target) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { +func (t *Target) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) if err != nil { return err @@ -98,21 +88,25 @@ func (t *target) PushReference(ctx context.Context, expected ocispec.Descriptor, return nil } -func (t *target) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { +func (t *Target) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { if p, ok := t.Target.(content.PredecessorFinder); ok { return p.Predecessors(ctx, node) } - return nil, fmt.Errorf("target %v does not support Predecessors", reflect.TypeOf(t.Target)) + return nil, fmt.Errorf("Target %v does not support Predecessors", reflect.TypeOf(t.Target)) } -func (t *target) Close() error { - if err := t.manager.Close(); err != nil { - return err - } - return nil +// Close closes the Target to stop tracking. +func (t *Target) Close() error { + return t.manager.Close() } -func (t *target) Prompt(desc ocispec.Descriptor, prompt string) error { +// Prompt prompts the user with the provided prompt and descriptor. +// If Target is not set, only prints status. +func (t *Target) Prompt(desc ocispec.Descriptor, prompt string, verbose bool) error { + if t == nil { + display.PrintStatus(desc, prompt, verbose) + return nil + } status, err := t.manager.Add() if err != nil { return err diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 19bbd589f..2f4fafed0 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -143,17 +143,8 @@ func runCopy(ctx context.Context, opts copyOptions) error { } func doCopy(ctx context.Context, src option.ReadOnlyGraphTagFinderTarget, dst oras.GraphTarget, opts copyOptions) (ocispec.Descriptor, error) { - var trackable track.Trackable - if opts.TTY != nil { - target, err := track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) - if err != nil { - return ocispec.Descriptor{}, err - } - defer target.Close() - trackable = target - dst = target - } - + var tracked *track.Target + var err error // Prepare copy options committed := &sync.Map{} extendedCopyOptions := oras.DefaultExtendedCopyOptions @@ -161,24 +152,35 @@ func doCopy(ctx context.Context, src option.ReadOnlyGraphTagFinderTarget, dst or extendedCopyOptions.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { return graph.Referrers(ctx, src, desc, "") } - extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - return display.PrintStatusWithoutTrackable(desc, "Copying", opts.Verbose, trackable) - } extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - var err error - if err = display.PrintSuccessorStatus(ctx, desc, "Skipped", dst, committed, opts.Verbose); err != nil { - return err - } - return display.PrintStatusWithoutTrackable(desc, "Copied ", opts.Verbose, trackable) + return display.PrintSuccessorStatus(ctx, desc, "Skipped", dst, committed, opts.Verbose) } extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintStatusWithTrackable(desc, "Exists ", opts.Verbose, trackable) + return tracked.Prompt(desc, "Exists ", opts.Verbose) + } + if opts.TTY != nil { + tracked, err = track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) + if err != nil { + return ocispec.Descriptor{}, err + } + defer tracked.Close() + dst = tracked + } else { + extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + return display.PrintStatus(desc, "Copying", opts.Verbose) + } + postCopy := extendedCopyOptions.PostCopy + extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + if err := postCopy(ctx, desc); err != nil { + return err + } + return display.PrintStatus(desc, "Copied ", opts.Verbose) + } } var desc ocispec.Descriptor - var err error rOpts := oras.DefaultResolveOptions rOpts.TargetPlatform = opts.Platform.Platform if opts.recursive { From a46c218eb5aa2eb904453253fb1cb9903ebd4ec1 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 08:48:55 +0000 Subject: [PATCH 53/98] code clean Signed-off-by: Billy Zha --- cmd/oras/root/cp.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 2f4fafed0..a3d721299 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -160,14 +160,8 @@ func doCopy(ctx context.Context, src option.ReadOnlyGraphTagFinderTarget, dst or committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return tracked.Prompt(desc, "Exists ", opts.Verbose) } - if opts.TTY != nil { - tracked, err = track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) - if err != nil { - return ocispec.Descriptor{}, err - } - defer tracked.Close() - dst = tracked - } else { + switch opts.TTY { + case nil: // none tty output extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return display.PrintStatus(desc, "Copying", opts.Verbose) } @@ -178,6 +172,13 @@ func doCopy(ctx context.Context, src option.ReadOnlyGraphTagFinderTarget, dst or } return display.PrintStatus(desc, "Copied ", opts.Verbose) } + default: // tty output + tracked, err = track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) + if err != nil { + return ocispec.Descriptor{}, err + } + defer tracked.Close() + dst = tracked } var desc ocispec.Descriptor From 151ad0ad2c13c989c2f6cf036663dce38dcf65a9 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 8 Oct 2023 08:56:49 +0000 Subject: [PATCH 54/98] lint Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index f3d3be6f3..2b6b047a2 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -104,8 +104,7 @@ func (t *Target) Close() error { // If Target is not set, only prints status. func (t *Target) Prompt(desc ocispec.Descriptor, prompt string, verbose bool) error { if t == nil { - display.PrintStatus(desc, prompt, verbose) - return nil + return display.PrintStatus(desc, prompt, verbose) } status, err := t.manager.Add() if err != nil { From 7327022008785f8630c0bb5b5705cd37f48efee6 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 02:13:16 +0000 Subject: [PATCH 55/98] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 1 + cmd/oras/root/blob/fetch.go | 1 - cmd/oras/root/blob/push.go | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index f0be3d530..3047e8279 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -60,6 +60,7 @@ func managedReader(r io.Reader, descriptor ocispec.Descriptor, manager progress. // StopManager stops the status channel and related manager. func (r *reader) StopManager() { + r.Close() _ = r.manager.Close() } diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 595505fa3..3cadd444f 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -179,7 +179,6 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg return ocispec.Descriptor{}, err } defer trackedReader.StopManager() - defer trackedReader.Close() trackedReader.Start() if _, err = io.Copy(file, trackedReader); err != nil { return ocispec.Descriptor{}, err diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 29b061aaf..22f804079 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -159,7 +159,6 @@ func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc oci return err } defer trackedReader.StopManager() - defer trackedReader.Close() trackedReader.Start() r = trackedReader if err := t.Push(ctx, desc, r); err != nil { From 1abda95ddab68ff8e6d4431af89b6172798e7b62 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 02:15:19 +0000 Subject: [PATCH 56/98] change base for sizing Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/humanize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/progress/humanize.go b/cmd/oras/internal/display/progress/humanize.go index d769c83a5..78fd709c7 100644 --- a/cmd/oras/internal/display/progress/humanize.go +++ b/cmd/oras/internal/display/progress/humanize.go @@ -21,7 +21,7 @@ import ( var ( units = []string{"B", "kB", "MB", "GB", "TB"} - base = 1000.0 + base = 1024.0 ) type bytes struct { From 636f42439ff55140456a5806c55791c7a972fa0e Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 02:39:16 +0000 Subject: [PATCH 57/98] fix speed display Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/status.go | 6 +++--- cmd/oras/internal/display/progress/status_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 3aba785d5..2611ad581 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -28,7 +28,7 @@ import ( const ( barLength = 20 - speedLength = 8 + speedLength = 9 // speed_size(4) + space(1) + speed_unit(4) zeroDuration = "0s" // default zero value of time.Duration.String() zeroStatus = "loading status..." zeroDigest = " └─ loading digest..." @@ -108,7 +108,7 @@ func (s *status) String(width int) (string, string) { } // format: [left--------------------------------------------][margin][right---------------------------------] - // mark(1) bar(22) speed(8) action(<=11) name(<126) size_per_size(<=13) percent(8) time(>=6) + // mark(1) bar(22) speed(8) action(<=11) name(<=126) size_per_size(<=13) percent(8) time(>=6) // └─ digest(72) var offset string switch percent { @@ -126,7 +126,7 @@ func (s *status) String(width int) (string, string) { lenBar := int(percent * barLength) bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barLength-lenBar)) speed := s.calculateSpeed() - speedStr := fmt.Sprintf("%v%s/s", speed.size, speed.unit) + speedStr := fmt.Sprintf("%v %2s/s", speed.size, speed.unit) left = fmt.Sprintf("%c %s(%*s) %s %s", s.mark.symbol(), bar, speedLength, speedStr, s.prompt, name) // bar + wrapper(2) + space(1) + speed + wrapper(2) = len(bar) + len(speed) + 5 lenLeft = barLength + speedLength + 5 diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 25033beb6..6563a609d 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -50,7 +50,7 @@ func Test_status_String(t *testing.T) { } // partial name statusStr, digestStr = s.String(console.MinWidth) - if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, "application/vnd.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, "application/vn.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } From 2ea395d3129761e895a3326d3080f4cb8877de43 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 02:39:26 +0000 Subject: [PATCH 58/98] add test coverage Signed-off-by: Billy Zha --- .../display/progress/humanize_test.go | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/progress/humanize_test.go b/cmd/oras/internal/display/progress/humanize_test.go index 547b777bc..c38ca5608 100644 --- a/cmd/oras/internal/display/progress/humanize_test.go +++ b/cmd/oras/internal/display/progress/humanize_test.go @@ -15,7 +15,10 @@ limitations under the License. package progress -import "testing" +import ( + "reflect" + "testing" +) func TestRoundTo(t *testing.T) { type args struct { @@ -39,3 +42,31 @@ func TestRoundTo(t *testing.T) { }) } } + +func TestToBytes(t *testing.T) { + type args struct { + sizeInBytes int64 + } + tests := []struct { + name string + args args + want bytes + }{ + {"0 bytes", args{0}, bytes{0, "B"}}, + {"1023 bytes", args{1023}, bytes{1023, "B"}}, + {"1 kB", args{1024}, bytes{1, "kB"}}, + {"1.5 kB", args{1024 + 512}, bytes{1.5, "kB"}}, + {"12.5 kB", args{1024 * 12.5}, bytes{12.5, "kB"}}, + {"512.5 kB", args{1024 * 512.5}, bytes{513, "kB"}}, + {"1 MB", args{1024 * 1024}, bytes{1, "MB"}}, + {"1 GB", args{1024 * 1024 * 1024}, bytes{1, "GB"}}, + {"1 TB", args{1024 * 1024 * 1024 * 1024}, bytes{1, "TB"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ToBytes(tt.args.sizeInBytes); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ToBytes() = %v, want %v", got, tt.want) + } + }) + } +} From feb73b4244f4f7d22df0460654b922c041aac591 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 05:58:38 +0000 Subject: [PATCH 59/98] add test for copying Signed-off-by: Billy Zha --- cmd/oras/root/cp.go | 2 +- cmd/oras/root/cp_test.go | 77 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 cmd/oras/root/cp_test.go diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index a3d721299..a5d38919d 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -142,7 +142,7 @@ func runCopy(ctx context.Context, opts copyOptions) error { return nil } -func doCopy(ctx context.Context, src option.ReadOnlyGraphTagFinderTarget, dst oras.GraphTarget, opts copyOptions) (ocispec.Descriptor, error) { +func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTarget, opts copyOptions) (ocispec.Descriptor, error) { var tracked *track.Target var err error // Prepare copy options diff --git a/cmd/oras/root/cp_test.go b/cmd/oras/root/cp_test.go new file mode 100644 index 000000000..53161772c --- /dev/null +++ b/cmd/oras/root/cp_test.go @@ -0,0 +1,77 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package root + +import ( + "bytes" + "context" + "fmt" + "os" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content/memory" + "oras.land/oras/cmd/oras/internal/display/console/testutils" +) + +var ( + src *memory.Store + desc ocispec.Descriptor +) + +func TestMain(m *testing.M) { + src = memory.New() + content := []byte("test") + r := bytes.NewReader(content) + desc = ocispec.Descriptor{ + MediaType: "application/octet-stream", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + if err := src.Push(context.Background(), desc, r); err != nil { + fmt.Println("Setup failed:", err) + os.Exit(1) + } + if err := src.Tag(context.Background(), desc, desc.Digest.String()); err != nil { + fmt.Println("Setup failed:", err) + os.Exit(1) + } + m.Run() +} + +func Test_doCopy(t *testing.T) { + // prepare + pty, slave, err := testutils.NewPty() + if err != nil { + t.Fatal(err) + } + defer slave.Close() + var opts copyOptions + opts.TTY = slave + opts.Verbose = true + opts.From.Reference = desc.Digest.String() + dst := memory.New() + // test + _, err = doCopy(context.Background(), src, dst, opts) + if err != nil { + t.Fatal(err) + } + // validate + if err = testutils.MatchPty(pty, slave, "Copied", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + t.Fatal(err) + } +} From 4f7ac011584c3c5f8273f701b5f7b4b59bb6b080 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 05:59:26 +0000 Subject: [PATCH 60/98] bug fix Signed-off-by: Billy Zha --- cmd/oras/internal/display/console/testutils/testutils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/console/testutils/testutils.go b/cmd/oras/internal/display/console/testutils/testutils.go index 9a0eb556a..99abef4b4 100644 --- a/cmd/oras/internal/display/console/testutils/testutils.go +++ b/cmd/oras/internal/display/console/testutils/testutils.go @@ -55,7 +55,7 @@ func MatchPty(pty console.Console, slave *os.File, expected ...string) error { slave.Close() wg.Wait() - return OrderedMatch(buffer.String()) + return OrderedMatch(buffer.String(), expected...) } // OrderedMatch matches the got with the expected strings in order. From cfdfe46064cadb9b1a3710bb8b465f037d819ebf Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 06:04:35 +0000 Subject: [PATCH 61/98] code clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index 2b6b047a2..3805c6053 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -30,7 +30,7 @@ import ( "oras.land/oras/cmd/oras/internal/display/progress" ) -// Trackable can be tracked and supprots explicit prompting and stoping. +// Target tracks the progress of pushing ot underlying oras.Target. type Target struct { oras.Target manager progress.Manager @@ -38,6 +38,7 @@ type Target struct { donePrompt string } +// NewTarget creates a new tracked Target. func NewTarget(t oras.Target, actionPrompt, donePrompt string, tty *os.File) (*Target, error) { manager, err := progress.NewManager(tty) if err != nil { @@ -52,6 +53,7 @@ func NewTarget(t oras.Target, actionPrompt, donePrompt string, tty *os.File) (*T }, nil } +// Push pushes the content to the Target with tracking. func (t *Target) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) if err != nil { @@ -66,6 +68,7 @@ func (t *Target) Push(ctx context.Context, expected ocispec.Descriptor, content return nil } +// PushReference pushes the content to the Target with tracking. func (t *Target) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) if err != nil { @@ -88,6 +91,7 @@ func (t *Target) PushReference(ctx context.Context, expected ocispec.Descriptor, return nil } +// Predecessors returns the predecessors of the node if supported. func (t *Target) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { if p, ok := t.Target.(content.PredecessorFinder); ok { return p.Predecessors(ctx, node) @@ -95,7 +99,7 @@ func (t *Target) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]o return nil, fmt.Errorf("Target %v does not support Predecessors", reflect.TypeOf(t.Target)) } -// Close closes the Target to stop tracking. +// Close closes the tracking manager. func (t *Target) Close() error { return t.manager.Close() } From 8c717092e9d9b226ab12355fbaa9711e73d4dbf2 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 07:36:42 +0000 Subject: [PATCH 62/98] feat: support progress output in attach and push Signed-off-by: Billy Zha --- cmd/oras/root/attach.go | 15 +++++++++++--- cmd/oras/root/push.go | 43 +++++++++++++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 41a063d13..4b6c8fbe4 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -27,6 +27,7 @@ import ( "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/file" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras/cmd/oras/internal/display/track" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/graph" "oras.land/oras/internal/registryutil" @@ -131,6 +132,14 @@ func runAttach(ctx context.Context, opts attachOptions) error { } // prepare push + var tracked *track.Target + if opts.TTY != nil { + tracked, err = track.NewTarget(dst, "Uploading", "Uploaded ", opts.TTY) + if err != nil { + return err + } + dst = tracked + } packOpts := oras.PackManifestOptions{ Subject: &subject, ManifestAnnotations: annotations[option.AnnotationManifest], @@ -142,7 +151,7 @@ func runAttach(ctx context.Context, opts attachOptions) error { graphCopyOptions := oras.DefaultCopyGraphOptions graphCopyOptions.Concurrency = opts.concurrency - updateDisplayOption(&graphCopyOptions, store, opts.Verbose) + updateDisplayOption(&graphCopyOptions, store, opts.Verbose, tracked) copy := func(root ocispec.Descriptor) error { graphCopyOptions.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { if content.Equal(node, root) { @@ -161,11 +170,11 @@ func runAttach(ctx context.Context, opts attachOptions) error { return oras.CopyGraph(ctx, store, dst, root, graphCopyOptions) } - root, err := pushArtifact(dst, pack, copy) + // Attach + root, err := doPush(pack, copy, tracked) if err != nil { return err } - digest := subject.Digest.String() if !strings.HasSuffix(opts.RawReference, digest) { opts.RawReference = fmt.Sprintf("%s@%s", opts.Path, subject.Digest) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 071efb818..07c3f805f 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -30,6 +30,7 @@ import ( "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras/cmd/oras/internal/display" + "oras.land/oras/cmd/oras/internal/display/track" "oras.land/oras/cmd/oras/internal/fileref" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/contentutil" @@ -182,10 +183,18 @@ func runPush(ctx context.Context, opts pushOptions) error { if err != nil { return err } + var tracked *track.Target + if opts.TTY != nil { + tracked, err = track.NewTarget(dst, "Uploading", "Uploaded ", opts.TTY) + if err != nil { + return err + } + dst = tracked + } copyOptions := oras.DefaultCopyOptions copyOptions.Concurrency = opts.concurrency union := contentutil.MultiReadOnlyTarget(memoryStore, store) - updateDisplayOption(©Options.CopyGraphOptions, union, opts.Verbose) + updateDisplayOption(©Options.CopyGraphOptions, union, opts.Verbose, tracked) copy := func(root ocispec.Descriptor) error { // add both pull and push scope hints for dst repository // to save potential push-scope token requests during copy @@ -200,7 +209,7 @@ func runPush(ctx context.Context, opts pushOptions) error { } // Push - root, err := pushArtifact(dst, pack, copy) + root, err := doPush(pack, copy, tracked) if err != nil { return err } @@ -224,19 +233,33 @@ func runPush(ctx context.Context, opts pushOptions) error { return opts.ExportManifest(ctx, memoryStore, root) } -func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool) { +func doPush(pack packFunc, copy copyFunc, dst oras.Target) (ocispec.Descriptor, error) { + if tracked, ok := dst.(*track.Target); ok { + defer tracked.Close() + } + // Push + return pushArtifact(dst, pack, copy) +} + +func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool, tracked *track.Target) { committed := &sync.Map{} - opts.PreCopy = display.StatusPrinter("Uploading", verbose) - opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { + opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintStatus(desc, "Exists ", verbose) + return display.PrintSuccessorStatus(ctx, desc, "Skipped ", fetcher, committed, verbose) } - opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - if err := display.PrintSuccessorStatus(ctx, desc, "Skipped ", fetcher, committed, verbose); err != nil { - return err + return tracked.Prompt(desc, "Exists ", verbose) + } + if tracked == nil { + opts.PreCopy = display.StatusPrinter("Uploading", verbose) + postCopy := opts.PostCopy + opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + if err := postCopy(ctx, desc); err != nil { + return err + } + return display.PrintStatus(desc, "Uploaded ", verbose) } - return display.PrintStatus(desc, "Uploaded ", verbose) } } From b9c5becd953a4a43c46317c1abbc82db367a2522 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 07:51:45 +0000 Subject: [PATCH 63/98] fix nil pointer Signed-off-by: Billy Zha --- cmd/oras/root/attach.go | 2 +- cmd/oras/root/push.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 4b6c8fbe4..4d82a72c8 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -171,7 +171,7 @@ func runAttach(ctx context.Context, opts attachOptions) error { } // Attach - root, err := doPush(pack, copy, tracked) + root, err := doPush(pack, copy, dst) if err != nil { return err } diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 07c3f805f..2abc45b8a 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -209,7 +209,7 @@ func runPush(ctx context.Context, opts pushOptions) error { } // Push - root, err := doPush(pack, copy, tracked) + root, err := doPush(pack, copy, dst) if err != nil { return err } From c47f967748b686920bcc52dde44d8a4fd5a85f01 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 9 Oct 2023 08:04:06 +0000 Subject: [PATCH 64/98] reset render interval to 200ms Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index b8b4ffd59..d4fada78e 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -38,7 +38,7 @@ type Manager interface { Close() error } -const bufFlushDuration = 500 * time.Millisecond +const bufFlushDuration = 200 * time.Millisecond type manager struct { status []*status From 2489a614666f5fb165208451960a590082cacbe5 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 11:45:11 +0000 Subject: [PATCH 65/98] turn humanize into package Signed-off-by: Billy Zha --- .../{humanize.go => humanize/bytes.go} | 35 ++++++++++++------- .../bytes_test.go} | 22 ++++++------ cmd/oras/internal/display/progress/status.go | 26 +++++++------- .../internal/display/progress/status_test.go | 3 +- 4 files changed, 49 insertions(+), 37 deletions(-) rename cmd/oras/internal/display/progress/{humanize.go => humanize/bytes.go} (63%) rename cmd/oras/internal/display/progress/{humanize_test.go => humanize/bytes_test.go} (75%) diff --git a/cmd/oras/internal/display/progress/humanize.go b/cmd/oras/internal/display/progress/humanize/bytes.go similarity index 63% rename from cmd/oras/internal/display/progress/humanize.go rename to cmd/oras/internal/display/progress/humanize/bytes.go index 78fd709c7..a34c6ee94 100644 --- a/cmd/oras/internal/display/progress/humanize.go +++ b/cmd/oras/internal/display/progress/humanize/bytes.go @@ -13,31 +13,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -package progress +package humanize import ( + "fmt" "math" ) -var ( - units = []string{"B", "kB", "MB", "GB", "TB"} - base = 1024.0 -) +const base = 1024.0 + +var units = []string{"B", "kB", "MB", "GB", "TB"} -type bytes struct { - size float64 - unit string +type Bytes struct { + Size float64 + Unit string } // ToBytes converts size in bytes to human readable format. -func ToBytes(sizeInBytes int64) bytes { +func ToBytes(sizeInBytes int64) Bytes { f := float64(sizeInBytes) if f < base { - return bytes{f, units[0]} + return Bytes{f, units[0]} + } + e := int(math.Floor(math.Log(f) / math.Log(base))) + if e > len(units) { + // only support up to TB + e = len(units) } - e := math.Floor(math.Log(f) / math.Log(base)) - p := f / math.Pow(base, e) - return bytes{RoundTo(p), units[int(e)]} + p := f / math.Pow(base, float64(e)) + return Bytes{RoundTo(p), units[e]} +} + +// String returns the string representation of Bytes. +func (b Bytes) String() string { + return fmt.Sprintf("%v %2s", b.Size, b.Unit) } // RoundTo makes length of the size string to less than or equal to 4. diff --git a/cmd/oras/internal/display/progress/humanize_test.go b/cmd/oras/internal/display/progress/humanize/bytes_test.go similarity index 75% rename from cmd/oras/internal/display/progress/humanize_test.go rename to cmd/oras/internal/display/progress/humanize/bytes_test.go index c38ca5608..33545f480 100644 --- a/cmd/oras/internal/display/progress/humanize_test.go +++ b/cmd/oras/internal/display/progress/humanize/bytes_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package progress +package humanize import ( "reflect" @@ -50,17 +50,17 @@ func TestToBytes(t *testing.T) { tests := []struct { name string args args - want bytes + want Bytes }{ - {"0 bytes", args{0}, bytes{0, "B"}}, - {"1023 bytes", args{1023}, bytes{1023, "B"}}, - {"1 kB", args{1024}, bytes{1, "kB"}}, - {"1.5 kB", args{1024 + 512}, bytes{1.5, "kB"}}, - {"12.5 kB", args{1024 * 12.5}, bytes{12.5, "kB"}}, - {"512.5 kB", args{1024 * 512.5}, bytes{513, "kB"}}, - {"1 MB", args{1024 * 1024}, bytes{1, "MB"}}, - {"1 GB", args{1024 * 1024 * 1024}, bytes{1, "GB"}}, - {"1 TB", args{1024 * 1024 * 1024 * 1024}, bytes{1, "TB"}}, + {"0 bytes", args{0}, Bytes{0, "B"}}, + {"1023 bytes", args{1023}, Bytes{1023, "B"}}, + {"1 kB", args{1024}, Bytes{1, "kB"}}, + {"1.5 kB", args{1024 + 512}, Bytes{1.5, "kB"}}, + {"12.5 kB", args{1024 * 12.5}, Bytes{12.5, "kB"}}, + {"512.5 kB", args{1024 * 512.5}, Bytes{513, "kB"}}, + {"1 MB", args{1024 * 1024}, Bytes{1, "MB"}}, + {"1 GB", args{1024 * 1024 * 1024}, Bytes{1, "GB"}}, + {"1 TB", args{1024 * 1024 * 1024 * 1024}, Bytes{1, "TB"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index 2611ad581..b65cce6f3 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -17,6 +17,7 @@ package progress import ( "fmt" + "math" "strings" "sync" "time" @@ -24,11 +25,12 @@ import ( "github.com/morikuni/aec" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/progress/humanize" ) const ( barLength = 20 - speedLength = 9 // speed_size(4) + space(1) + speed_unit(4) + speedLength = 7 // speed_size(4) + space(1) + speed_unit(2) zeroDuration = "0s" // default zero value of time.Duration.String() zeroStatus = "loading status..." zeroDigest = " └─ loading digest..." @@ -40,7 +42,7 @@ type status struct { prompt string descriptor ocispec.Descriptor offset int64 - total bytes + total humanize.Bytes lastOffset int64 lastRenderTime time.Time @@ -113,11 +115,11 @@ func (s *status) String(width int) (string, string) { var offset string switch percent { case 1: // 100%, show exact size - offset = fmt.Sprint(s.total.size) + offset = fmt.Sprint(s.total.Size) default: // 0% ~ 99%, show 2-digit precision - offset = fmt.Sprintf("%.2f", RoundTo(s.total.size*percent)) + offset = fmt.Sprintf("%.2f", humanize.RoundTo(s.total.Size*percent)) } - right := fmt.Sprintf(" %s/%v %s %6.2f%% %6s", offset, s.total.size, s.total.unit, percent*100, s.durationString()) + right := fmt.Sprintf(" %s/%s %6.2f%% %6s", offset, s.total, percent*100, s.durationString()) lenRight := utf8.RuneCountInString(right) var left string @@ -126,10 +128,9 @@ func (s *status) String(width int) (string, string) { lenBar := int(percent * barLength) bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barLength-lenBar)) speed := s.calculateSpeed() - speedStr := fmt.Sprintf("%v %2s/s", speed.size, speed.unit) - left = fmt.Sprintf("%c %s(%*s) %s %s", s.mark.symbol(), bar, speedLength, speedStr, s.prompt, name) - // bar + wrapper(2) + space(1) + speed + wrapper(2) = len(bar) + len(speed) + 5 - lenLeft = barLength + speedLength + 5 + left = fmt.Sprintf("%c %s(%*s/s) %s %s", s.mark.symbol(), bar, speedLength, speed, s.prompt, name) + // bar + wrapper(2) + space(1) + speed + "/s"(2) + wrapper(2) = len(bar) + len(speed) + 7 + lenLeft = barLength + speedLength + 7 } else { left = fmt.Sprintf("√ %s %s", s.prompt, name) } @@ -147,15 +148,16 @@ func (s *status) String(width int) (string, string) { // calculateSpeed calculates the speed of the progress and update last status. // caller must hold the lock. -func (s *status) calculateSpeed() bytes { +func (s *status) calculateSpeed() humanize.Bytes { now := time.Now() secondsTaken := now.Sub(s.lastRenderTime).Seconds() + secondsTaken = math.Max(secondsTaken, float64(bufFlushDuration.Milliseconds())/1000) bytes := float64(s.offset - s.lastOffset) s.lastOffset = s.offset s.lastRenderTime = now - return ToBytes(int64(bytes / secondsTaken)) + return humanize.ToBytes(int64(bytes / secondsTaken)) } // durationString returns a viewable TTY string of the status with duration. @@ -190,7 +192,7 @@ func (s *status) Update(n *status) { if n.offset >= 0 { s.offset = n.offset if n.descriptor.Size != s.descriptor.Size { - s.total = ToBytes(n.descriptor.Size) + s.total = humanize.ToBytes(n.descriptor.Size) } s.descriptor = n.descriptor } diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 6563a609d..5a22195bd 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -22,6 +22,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/console" "oras.land/oras/cmd/oras/internal/display/console/testutils" + "oras.land/oras/cmd/oras/internal/display/progress/humanize" ) func Test_status_String(t *testing.T) { @@ -41,7 +42,7 @@ func Test_status_String(t *testing.T) { }, startTime: time.Now().Add(-time.Minute), offset: 0, - total: ToBytes(2), + total: humanize.ToBytes(2), }) // full name statusStr, digestStr := s.String(120) From 9e7daecb84f798adf229395699340acb60f6b3ad Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 11:45:56 +0000 Subject: [PATCH 66/98] bug fix Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/humanize/bytes.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/oras/internal/display/progress/humanize/bytes.go b/cmd/oras/internal/display/progress/humanize/bytes.go index a34c6ee94..4558dbdf3 100644 --- a/cmd/oras/internal/display/progress/humanize/bytes.go +++ b/cmd/oras/internal/display/progress/humanize/bytes.go @@ -36,9 +36,9 @@ func ToBytes(sizeInBytes int64) Bytes { return Bytes{f, units[0]} } e := int(math.Floor(math.Log(f) / math.Log(base))) - if e > len(units) { + if e >= len(units) { // only support up to TB - e = len(units) + e = len(units) - 1 } p := f / math.Pow(base, float64(e)) return Bytes{RoundTo(p), units[e]} From c4564fed9580bcc35cad243071380488314b2bd1 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 12:07:16 +0000 Subject: [PATCH 67/98] rename pts device Signed-off-by: Billy Zha --- .../internal/display/console/testutils/testutils.go | 12 ++++++------ cmd/oras/internal/display/progress/status_test.go | 6 +++--- cmd/oras/internal/option/common_unix_test.go | 8 ++++---- cmd/oras/root/blob/fetch_test.go | 8 ++++---- cmd/oras/root/blob/push_test.go | 8 ++++---- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/cmd/oras/internal/display/console/testutils/testutils.go b/cmd/oras/internal/display/console/testutils/testutils.go index 99abef4b4..92d65a00c 100644 --- a/cmd/oras/internal/display/console/testutils/testutils.go +++ b/cmd/oras/internal/display/console/testutils/testutils.go @@ -29,22 +29,22 @@ import ( ) // NewPty creates a new pty pair for testing, caller is responsible for closing -// the returned slave if err is not nil. +// the returned device file if err is not nil. func NewPty() (console.Console, *os.File, error) { - pty, slavePath, err := console.NewPty() + pty, devicePath, err := console.NewPty() if err != nil { return nil, nil, err } - slave, err := os.OpenFile(slavePath, os.O_RDWR, 0) + device, err := os.OpenFile(devicePath, os.O_RDWR, 0) if err != nil { return nil, nil, err } - return pty, slave, nil + return pty, device, nil } // MatchPty checks that the output matches the expected strings in specified // order. -func MatchPty(pty console.Console, slave *os.File, expected ...string) error { +func MatchPty(pty console.Console, device *os.File, expected ...string) error { var wg sync.WaitGroup wg.Add(1) var buffer bytes.Buffer @@ -52,7 +52,7 @@ func MatchPty(pty console.Console, slave *os.File, expected ...string) error { defer wg.Done() _, _ = io.Copy(&buffer, pty) }() - slave.Close() + device.Close() wg.Wait() return OrderedMatch(buffer.String(), expected...) diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 5a22195bd..10c38f9ad 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -46,12 +46,12 @@ func Test_status_String(t *testing.T) { }) // full name statusStr, digestStr := s.String(120) - if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, s.descriptor.MediaType, "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, s.descriptor.MediaType, "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } // partial name statusStr, digestStr = s.String(console.MinWidth) - if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, "application/vn.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { + if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, "application/v.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } @@ -62,7 +62,7 @@ func Test_status_String(t *testing.T) { descriptor: s.descriptor, }) statusStr, digestStr = s.String(120) - if err := testutils.OrderedMatch(statusStr+digestStr, "√", s.prompt, s.descriptor.MediaType, "2/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { + if err := testutils.OrderedMatch(statusStr+digestStr, "√", s.prompt, s.descriptor.MediaType, "2/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } } diff --git a/cmd/oras/internal/option/common_unix_test.go b/cmd/oras/internal/option/common_unix_test.go index fd9376eb8..2d4388385 100644 --- a/cmd/oras/internal/option/common_unix_test.go +++ b/cmd/oras/internal/option/common_unix_test.go @@ -24,21 +24,21 @@ import ( ) func TestCommon_parseTTY(t *testing.T) { - _, slave, err := testutils.NewPty() + _, device, err := testutils.NewPty() if err != nil { t.Fatal(err) } - defer slave.Close() + defer device.Close() var opts Common // TTY output - if err := opts.parseTTY(slave); err != nil { + if err := opts.parseTTY(device); err != nil { t.Errorf("unexpected error with TTY output: %v", err) } // --debug opts.Debug = true - if err := opts.parseTTY(slave); err == nil { + if err := opts.parseTTY(device); err == nil { t.Error("expected error when debug is set with TTY output") } } diff --git a/cmd/oras/root/blob/fetch_test.go b/cmd/oras/root/blob/fetch_test.go index d0b752a13..ba3363850 100644 --- a/cmd/oras/root/blob/fetch_test.go +++ b/cmd/oras/root/blob/fetch_test.go @@ -30,11 +30,11 @@ import ( func Test_fetchBlobOptions_doFetch(t *testing.T) { // prepare - pty, slave, err := testutils.NewPty() + pty, device, err := testutils.NewPty() if err != nil { t.Fatal(err) } - defer slave.Close() + defer device.Close() src := memory.New() content := []byte("test") r := bytes.NewReader(content) @@ -53,7 +53,7 @@ func Test_fetchBlobOptions_doFetch(t *testing.T) { } var opts fetchBlobOptions opts.Reference = tag - opts.Common.TTY = slave + opts.Common.TTY = device opts.outputPath = t.TempDir() + "/test" // test _, err = opts.doFetch(ctx, src) @@ -61,7 +61,7 @@ func Test_fetchBlobOptions_doFetch(t *testing.T) { t.Fatal(err) } // validate - if err = testutils.MatchPty(pty, slave, "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + if err = testutils.MatchPty(pty, device, "Downloaded ", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } diff --git a/cmd/oras/root/blob/push_test.go b/cmd/oras/root/blob/push_test.go index 12f462f9f..b47bcfbee 100644 --- a/cmd/oras/root/blob/push_test.go +++ b/cmd/oras/root/blob/push_test.go @@ -30,11 +30,11 @@ import ( func Test_pushBlobOptions_doPush(t *testing.T) { // prepare - pty, slave, err := testutils.NewPty() + pty, device, err := testutils.NewPty() if err != nil { t.Fatal(err) } - defer slave.Close() + defer device.Close() src := memory.New() content := []byte("test") r := bytes.NewReader(content) @@ -44,14 +44,14 @@ func Test_pushBlobOptions_doPush(t *testing.T) { Size: int64(len(content)), } var opts pushBlobOptions - opts.Common.TTY = slave + opts.Common.TTY = device // test err = opts.doPush(context.Background(), src, desc, r) if err != nil { t.Fatal(err) } // validate - if err = testutils.MatchPty(pty, slave, "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { + if err = testutils.MatchPty(pty, device, "Uploaded", desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } From 6c324d1ddb124e1d3101a1ec61928e94132bde9d Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 12:59:20 +0000 Subject: [PATCH 68/98] reduce buffer size Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 22 ++++++++++--------- cmd/oras/internal/display/track/reader.go | 10 ++++----- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index d4fada78e..d55e9fe05 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -24,13 +24,16 @@ import ( "oras.land/oras/cmd/oras/internal/display/console" ) -// BufferSize is the size of the status channel buffer. -const BufferSize = 20 +const ( + // BufferSize is the size of the status channel buffer. + BufferSize = 2 + bufFlushDuration = 200 * time.Millisecond +) var errManagerStopped = errors.New("progress output manage has already been stopped") // Status is print message channel -type Status chan<- *status +type Status chan *status // Manager is progress view master type Manager interface { @@ -38,8 +41,6 @@ type Manager interface { Close() error } -const bufFlushDuration = 200 * time.Millisecond - type manager struct { status []*status statusLock sync.RWMutex @@ -71,14 +72,12 @@ func (m *manager) start() { defer m.console.Restore() defer renderTicker.Stop() for { + m.render() select { case <-m.renderDone: - m.updating.Wait() - m.render() close(m.renderClosed) return case <-renderTicker.C: - m.render() } } }() @@ -136,10 +135,13 @@ func (m *manager) Close() error { if m.closed() { return errManagerStopped } - // 1. stop periodic rendering + // 1. wait for update to stop + m.updating.Wait() + // 2. stop periodic rendering close(m.renderDone) - // 2. wait for the render stop + // 3. wait for the render stop <-m.renderClosed + m.render() return nil } diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 3047e8279..e79320d99 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -92,12 +92,12 @@ func (r *reader) Read(p []byte) (int, error) { if r.offset != r.descriptor.Size { return n, io.ErrUnexpectedEOF } - r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, r.offset) } - - if len(r.status) < progress.BufferSize { - // intermediate progress might be ignored if buffer is full - r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, r.offset) + select { + case r.status <- progress.NewStatus(r.donePrompt, r.descriptor, r.offset): + default: + // dismiss the oldest unconsumed status + <-r.status } return n, err } From d4298c1241f87bd4e2be4a6fab94b564d685f08c Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 13:00:30 +0000 Subject: [PATCH 69/98] fix typo Signed-off-by: Billy Zha --- cmd/oras/internal/option/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 4b0c4b154..477429ade 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -39,7 +39,7 @@ type Common struct { func (opts *Common) ApplyFlags(fs *pflag.FlagSet) { fs.BoolVarP(&opts.Debug, "debug", "d", false, "debug mode") fs.BoolVarP(&opts.Verbose, "verbose", "v", false, "verbose output") - fs.BoolVarP(&opts.noTTY, "no-tty", "", false, "[Preview] avoid using stdout as a terminal") + fs.BoolVarP(&opts.noTTY, "no-tty", "", false, "[Preview] avoid using stderr as a terminal") } // WithContext returns a new FieldLogger and an associated Context derived from ctx. From 67574ddd748f9497dd5d54da3b738f0fa82c5b23 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 13:01:27 +0000 Subject: [PATCH 70/98] change switch to if-else Signed-off-by: Billy Zha --- cmd/oras/root/blob/push.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/oras/root/blob/push.go b/cmd/oras/root/blob/push.go index 22f804079..f47c6bae0 100644 --- a/cmd/oras/root/blob/push.go +++ b/cmd/oras/root/blob/push.go @@ -142,8 +142,8 @@ func pushBlob(ctx context.Context, opts pushBlobOptions) (err error) { } func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc ocispec.Descriptor, r io.Reader) error { - switch opts.TTY { - case nil: // none tty output + if opts.TTY == nil { + // none tty output if err := display.PrintStatus(desc, "Uploading", opts.Verbose); err != nil { return err } @@ -153,7 +153,8 @@ func (opts *pushBlobOptions) doPush(ctx context.Context, t oras.Target, desc oci if err := display.PrintStatus(desc, "Uploaded ", opts.Verbose); err != nil { return err } - default: // tty output + } else { + // tty output trackedReader, err := track.NewReader(r, desc, "Uploading", "Uploaded ", opts.TTY) if err != nil { return err From 25d8935ad50072ad8a0994c19b06eb4b806110f8 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 13:07:26 +0000 Subject: [PATCH 71/98] bug fix Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index e79320d99..d04b966ee 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -95,9 +95,9 @@ func (r *reader) Read(p []byte) (int, error) { } select { case r.status <- progress.NewStatus(r.donePrompt, r.descriptor, r.offset): - default: - // dismiss the oldest unconsumed status - <-r.status + // purge the channel until successfully pushed + break + case <-r.status: } return n, err } From c76c4647e00fd40615cff3187252212c7a0aeeef Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 13:08:31 +0000 Subject: [PATCH 72/98] change switch to if-else Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 3cadd444f..2d22161f6 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -168,12 +168,13 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg } }() - switch opts.TTY { - case nil: // none tty output + if opts.TTY == nil { + // none tty output if _, err = io.Copy(file, vr); err != nil { return ocispec.Descriptor{}, err } - default: // tty output + } else { + // tty output trackedReader, err := track.NewReader(vr, desc, "Downloading", "Downloaded ", opts.TTY) if err != nil { return ocispec.Descriptor{}, err From 9403ccabca80cf5643ca518c465d0540e5f31d3c Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 13:34:26 +0000 Subject: [PATCH 73/98] revert rendering Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 4 ++-- cmd/oras/internal/display/track/reader.go | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index d55e9fe05..1bb23076c 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -72,12 +72,13 @@ func (m *manager) start() { defer m.console.Restore() defer renderTicker.Stop() for { - m.render() select { case <-m.renderDone: + m.render() close(m.renderClosed) return case <-renderTicker.C: + m.render() } } }() @@ -141,7 +142,6 @@ func (m *manager) Close() error { close(m.renderDone) // 3. wait for the render stop <-m.renderClosed - m.render() return nil } diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index d04b966ee..0600dd4c5 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -93,11 +93,12 @@ func (r *reader) Read(p []byte) (int, error) { return n, io.ErrUnexpectedEOF } } - select { - case r.status <- progress.NewStatus(r.donePrompt, r.descriptor, r.offset): - // purge the channel until successfully pushed - break - case <-r.status: + for { + select { + case r.status <- progress.NewStatus(r.donePrompt, r.descriptor, r.offset): + // purge the channel until successfully pushed + return n, err + case <-r.status: + } } - return n, err } From 07413962b2053e3a1e6b06631964a1f524d910dc Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 10 Oct 2023 13:40:16 +0000 Subject: [PATCH 74/98] reduce buffer size to 1 Signed-off-by: Billy Zha --- cmd/oras/internal/display/progress/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/progress/manager.go b/cmd/oras/internal/display/progress/manager.go index 1bb23076c..0ee57cfc0 100644 --- a/cmd/oras/internal/display/progress/manager.go +++ b/cmd/oras/internal/display/progress/manager.go @@ -26,7 +26,7 @@ import ( const ( // BufferSize is the size of the status channel buffer. - BufferSize = 2 + BufferSize = 1 bufFlushDuration = 200 * time.Millisecond ) From 8d6c017ed38bfeb34403992e15a57230855dbf0d Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 Oct 2023 04:42:44 +0000 Subject: [PATCH 75/98] fix: enable pipe when output to stdout Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 2d22161f6..70d8d7d26 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -147,30 +147,23 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg vr := content.NewVerifyReader(rc, desc) // outputs blob content if "--output -" is used - if opts.outputPath == "-" { - if _, err := io.Copy(os.Stdout, vr); err != nil { - return ocispec.Descriptor{}, err - } - if err := vr.Verify(); err != nil { + writer := os.Stdout + if opts.outputPath != "-" { + // save blob content into the local file if the output path is provided + file, err := os.Create(opts.outputPath) + if err != nil { return ocispec.Descriptor{}, err } - return desc, nil - } - - // save blob content into the local file if the output path is provided - file, err := os.Create(opts.outputPath) - if err != nil { - return ocispec.Descriptor{}, err + defer func() { + if err := file.Close(); fetchErr == nil { + fetchErr = err + } + }() } - defer func() { - if err := file.Close(); fetchErr == nil { - fetchErr = err - } - }() if opts.TTY == nil { // none tty output - if _, err = io.Copy(file, vr); err != nil { + if _, err = io.Copy(writer, vr); err != nil { return ocispec.Descriptor{}, err } } else { @@ -181,7 +174,7 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg } defer trackedReader.StopManager() trackedReader.Start() - if _, err = io.Copy(file, trackedReader); err != nil { + if _, err = io.Copy(writer, trackedReader); err != nil { return ocispec.Descriptor{}, err } trackedReader.Done() From c5ab1eef9f2cd2b9b0cc24f6da30f81bd4b51194 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 Oct 2023 04:42:44 +0000 Subject: [PATCH 76/98] fix: enable pipe when output to stdout Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 2d22161f6..70d8d7d26 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -147,30 +147,23 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg vr := content.NewVerifyReader(rc, desc) // outputs blob content if "--output -" is used - if opts.outputPath == "-" { - if _, err := io.Copy(os.Stdout, vr); err != nil { - return ocispec.Descriptor{}, err - } - if err := vr.Verify(); err != nil { + writer := os.Stdout + if opts.outputPath != "-" { + // save blob content into the local file if the output path is provided + file, err := os.Create(opts.outputPath) + if err != nil { return ocispec.Descriptor{}, err } - return desc, nil - } - - // save blob content into the local file if the output path is provided - file, err := os.Create(opts.outputPath) - if err != nil { - return ocispec.Descriptor{}, err + defer func() { + if err := file.Close(); fetchErr == nil { + fetchErr = err + } + }() } - defer func() { - if err := file.Close(); fetchErr == nil { - fetchErr = err - } - }() if opts.TTY == nil { // none tty output - if _, err = io.Copy(file, vr); err != nil { + if _, err = io.Copy(writer, vr); err != nil { return ocispec.Descriptor{}, err } } else { @@ -181,7 +174,7 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg } defer trackedReader.StopManager() trackedReader.Start() - if _, err = io.Copy(file, trackedReader); err != nil { + if _, err = io.Copy(writer, trackedReader); err != nil { return ocispec.Descriptor{}, err } trackedReader.Done() From 6611186b78eef090c51e158db3491b256f5b84b3 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 Oct 2023 05:03:02 +0000 Subject: [PATCH 77/98] fix display bug Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 0600dd4c5..0ecff1bc1 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -88,14 +88,16 @@ func (r *reader) Read(p []byte) (int, error) { } r.offset = r.offset + int64(n) + prompt := r.actionPrompt if err == io.EOF { if r.offset != r.descriptor.Size { return n, io.ErrUnexpectedEOF } + prompt = r.donePrompt } for { select { - case r.status <- progress.NewStatus(r.donePrompt, r.descriptor, r.offset): + case r.status <- progress.NewStatus(prompt, r.descriptor, r.offset): // purge the channel until successfully pushed return n, err case <-r.status: From e858d1a9e9e3bf34e163ed769d3323cf639a0ebe Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 Oct 2023 05:09:55 +0000 Subject: [PATCH 78/98] revert change Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/reader.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/oras/internal/display/track/reader.go b/cmd/oras/internal/display/track/reader.go index 0ecff1bc1..97bb3883e 100644 --- a/cmd/oras/internal/display/track/reader.go +++ b/cmd/oras/internal/display/track/reader.go @@ -88,16 +88,14 @@ func (r *reader) Read(p []byte) (int, error) { } r.offset = r.offset + int64(n) - prompt := r.actionPrompt if err == io.EOF { if r.offset != r.descriptor.Size { return n, io.ErrUnexpectedEOF } - prompt = r.donePrompt } for { select { - case r.status <- progress.NewStatus(prompt, r.descriptor, r.offset): + case r.status <- progress.NewStatus(r.actionPrompt, r.descriptor, r.offset): // purge the channel until successfully pushed return n, err case <-r.status: From e4d44390ed63b4ca8443631f8fc3a030d0b517be Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Wed, 11 Oct 2023 05:25:13 +0000 Subject: [PATCH 79/98] fix bug Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 70d8d7d26..1c9da87d5 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -154,6 +154,7 @@ func (opts *fetchBlobOptions) doFetch(ctx context.Context, src oras.ReadOnlyTarg if err != nil { return ocispec.Descriptor{}, err } + writer = file defer func() { if err := file.Close(); fetchErr == nil { fetchErr = err From 7c7510f269364b8a952aaa2f02f194c1aa499063 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 13 Oct 2023 07:37:25 +0000 Subject: [PATCH 80/98] add skipped status Signed-off-by: Billy Zha --- cmd/oras/internal/display/print.go | 10 ++++++---- cmd/oras/root/cp.go | 15 +++++++++++---- cmd/oras/root/push.go | 20 +++++++++++++------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/cmd/oras/internal/display/print.go b/cmd/oras/internal/display/print.go index 48374a327..7d4524b72 100644 --- a/cmd/oras/internal/display/print.go +++ b/cmd/oras/internal/display/print.go @@ -29,6 +29,8 @@ import ( var printLock sync.Mutex +type PrintFunc func(ocispec.Descriptor) error + // Print objects to display concurrent-safely func Print(a ...any) error { printLock.Lock() @@ -38,8 +40,8 @@ func Print(a ...any) error { } // StatusPrinter returns a tracking function for transfer status. -func StatusPrinter(status string, verbose bool) func(context.Context, ocispec.Descriptor) error { - return func(ctx context.Context, desc ocispec.Descriptor) error { +func StatusPrinter(status string, verbose bool) PrintFunc { + return func(desc ocispec.Descriptor) error { return PrintStatus(desc, status, verbose) } } @@ -58,7 +60,7 @@ func PrintStatus(desc ocispec.Descriptor, status string, verbose bool) error { } // PrintSuccessorStatus prints transfer status of successors. -func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, status string, fetcher content.Fetcher, committed *sync.Map, verbose bool) error { +func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print PrintFunc) error { successors, err := content.Successors(ctx, fetcher, desc) if err != nil { return err @@ -67,7 +69,7 @@ func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, status s name := s.Annotations[ocispec.AnnotationTitle] if v, ok := committed.Load(s.Digest.String()); ok && v != name { // Reprint status for deduplicated content - if err := PrintStatus(s, status, verbose); err != nil { + if err := print(s); err != nil { return err } } diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index a5d38919d..99275d4a7 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -152,16 +152,19 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar extendedCopyOptions.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { return graph.Referrers(ctx, src, desc, "") } + successorPrinter := display.StatusPrinter("Skipped", opts.Verbose) extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintSuccessorStatus(ctx, desc, "Skipped", dst, committed, opts.Verbose) + return display.PrintSuccessorStatus(ctx, desc, dst, committed, func(ocispec.Descriptor) error { + return successorPrinter(desc) + }) } extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return tracked.Prompt(desc, "Exists ", opts.Verbose) } - switch opts.TTY { - case nil: // none tty output + if opts.TTY == nil { + // none tty output extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return display.PrintStatus(desc, "Copying", opts.Verbose) } @@ -172,7 +175,11 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar } return display.PrintStatus(desc, "Copied ", opts.Verbose) } - default: // tty output + } else { + // tty output + successorPrinter = func(desc ocispec.Descriptor) error { + return tracked.Prompt(desc, "Skipped", opts.Verbose) + } tracked, err = track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) if err != nil { return ocispec.Descriptor{}, err diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 2abc45b8a..c01d71b8a 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -243,23 +243,29 @@ func doPush(pack packFunc, copy copyFunc, dst oras.Target) (ocispec.Descriptor, func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool, tracked *track.Target) { committed := &sync.Map{} - opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintSuccessorStatus(ctx, desc, "Skipped ", fetcher, committed, verbose) - } opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return tracked.Prompt(desc, "Exists ", verbose) } if tracked == nil { - opts.PreCopy = display.StatusPrinter("Uploading", verbose) - postCopy := opts.PostCopy + opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + return display.PrintStatus(desc, "Uploading", verbose) + } opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - if err := postCopy(ctx, desc); err != nil { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + if err := display.PrintSuccessorStatus(ctx, desc, fetcher, committed, display.StatusPrinter("Skipped ", verbose)); err != nil { return err } return display.PrintStatus(desc, "Uploaded ", verbose) } + } else { + opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + return display.PrintSuccessorStatus(ctx, desc, fetcher, committed, func(d ocispec.Descriptor) error { + return tracked.Prompt(desc, "Skipped ", verbose) + }) + } + } } From eba5761e1f7f9f3f3654dd8a654888a94c02d579 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 13 Oct 2023 08:58:00 +0000 Subject: [PATCH 81/98] fix merging Signed-off-by: Billy Zha --- cmd/oras/root/blob/fetch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/oras/root/blob/fetch.go b/cmd/oras/root/blob/fetch.go index 00cfdfcbb..7bae58092 100644 --- a/cmd/oras/root/blob/fetch.go +++ b/cmd/oras/root/blob/fetch.go @@ -118,7 +118,7 @@ func fetchBlob(ctx context.Context, opts fetchBlobOptions) (fetchErr error) { if opts.OutputDescriptor { descJSON, err := opts.Marshal(desc) if err != nil { - return ocispec.Descriptor{}, err + return err } if err := opts.Output(os.Stdout, descJSON); err != nil { return err From 6a9775773ea851167e8844912a6dad036225b457 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 13 Oct 2023 09:43:58 +0000 Subject: [PATCH 82/98] fix pushing Signed-off-by: Billy Zha --- cmd/oras/root/push.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index c01d71b8a..be462c930 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -248,6 +248,7 @@ func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, v return tracked.Prompt(desc, "Exists ", verbose) } if tracked == nil { + // non TTY opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return display.PrintStatus(desc, "Uploading", verbose) } @@ -258,14 +259,14 @@ func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, v } return display.PrintStatus(desc, "Uploaded ", verbose) } - } else { - opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintSuccessorStatus(ctx, desc, fetcher, committed, func(d ocispec.Descriptor) error { - return tracked.Prompt(desc, "Skipped ", verbose) - }) - } - + return + } + // TTY + opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + return display.PrintSuccessorStatus(ctx, desc, fetcher, committed, func(d ocispec.Descriptor) error { + return tracked.Prompt(d, "Skipped ", verbose) + }) } } From 1a4c49028e9e2dc20e8a4ef0b14d635e628af8f6 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Fri, 13 Oct 2023 09:55:02 +0000 Subject: [PATCH 83/98] fix cp Signed-off-by: Billy Zha --- cmd/oras/root/cp.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 20d3ed786..e18283602 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -158,8 +158,8 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar successorPrinter := display.StatusPrinter("Skipped", opts.Verbose) extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintSuccessorStatus(ctx, desc, dst, committed, func(ocispec.Descriptor) error { - return successorPrinter(desc) + return display.PrintSuccessorStatus(ctx, desc, dst, committed, func(d ocispec.Descriptor) error { + return successorPrinter(d) }) } extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { @@ -167,7 +167,7 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar return tracked.Prompt(desc, "Exists ", opts.Verbose) } if opts.TTY == nil { - // none tty output + // none TTY output extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return display.PrintStatus(desc, "Copying", opts.Verbose) } @@ -179,7 +179,7 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar return display.PrintStatus(desc, "Copied ", opts.Verbose) } } else { - // tty output + // TTY output successorPrinter = func(desc ocispec.Descriptor) error { return tracked.Prompt(desc, "Skipped", opts.Verbose) } From 781874f6304063e53404dd80af80dda295343b56 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 17 Oct 2023 02:01:16 +0000 Subject: [PATCH 84/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/print.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/oras/internal/display/print.go b/cmd/oras/internal/display/print.go index 7d4524b72..ef1e8c835 100644 --- a/cmd/oras/internal/display/print.go +++ b/cmd/oras/internal/display/print.go @@ -29,7 +29,7 @@ import ( var printLock sync.Mutex -type PrintFunc func(ocispec.Descriptor) error +type printFunc func(ocispec.Descriptor) error // Print objects to display concurrent-safely func Print(a ...any) error { @@ -40,7 +40,7 @@ func Print(a ...any) error { } // StatusPrinter returns a tracking function for transfer status. -func StatusPrinter(status string, verbose bool) PrintFunc { +func StatusPrinter(status string, verbose bool) printFunc { return func(desc ocispec.Descriptor) error { return PrintStatus(desc, status, verbose) } @@ -60,7 +60,7 @@ func PrintStatus(desc ocispec.Descriptor, status string, verbose bool) error { } // PrintSuccessorStatus prints transfer status of successors. -func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print PrintFunc) error { +func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print printFunc) error { successors, err := content.Successors(ctx, fetcher, desc) if err != nil { return err From 0dd031a07f0fb646daa99e15350d545eb63c797f Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 17 Oct 2023 02:10:16 +0000 Subject: [PATCH 85/98] change target to graph target Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index 3805c6053..81c086855 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -17,14 +17,11 @@ package track import ( "context" - "fmt" "io" "os" - "reflect" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" - "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry" "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/display/progress" @@ -32,21 +29,21 @@ import ( // Target tracks the progress of pushing ot underlying oras.Target. type Target struct { - oras.Target + oras.GraphTarget manager progress.Manager actionPrompt string donePrompt string } // NewTarget creates a new tracked Target. -func NewTarget(t oras.Target, actionPrompt, donePrompt string, tty *os.File) (*Target, error) { +func NewTarget(t oras.GraphTarget, actionPrompt, donePrompt string, tty *os.File) (*Target, error) { manager, err := progress.NewManager(tty) if err != nil { return nil, err } return &Target{ - Target: t, + GraphTarget: t, manager: manager, actionPrompt: actionPrompt, donePrompt: donePrompt, @@ -61,7 +58,7 @@ func (t *Target) Push(ctx context.Context, expected ocispec.Descriptor, content } defer r.Close() r.Start() - if err := t.Target.Push(ctx, expected, r); err != nil { + if err := t.GraphTarget.Push(ctx, expected, r); err != nil { return err } r.Done() @@ -76,13 +73,13 @@ func (t *Target) PushReference(ctx context.Context, expected ocispec.Descriptor, } defer r.Close() r.Start() - if rp, ok := t.Target.(registry.ReferencePusher); ok { + if rp, ok := t.GraphTarget.(registry.ReferencePusher); ok { err = rp.PushReference(ctx, expected, r, reference) } else { - if err := t.Target.Push(ctx, expected, r); err != nil { + if err := t.GraphTarget.Push(ctx, expected, r); err != nil { return err } - err = t.Target.Tag(ctx, expected, reference) + err = t.GraphTarget.Tag(ctx, expected, reference) } if err != nil { return err @@ -93,10 +90,7 @@ func (t *Target) PushReference(ctx context.Context, expected ocispec.Descriptor, // Predecessors returns the predecessors of the node if supported. func (t *Target) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { - if p, ok := t.Target.(content.PredecessorFinder); ok { - return p.Predecessors(ctx, node) - } - return nil, fmt.Errorf("Target %v does not support Predecessors", reflect.TypeOf(t.Target)) + return t.GraphTarget.Predecessors(ctx, node) } // Close closes the tracking manager. From d2c8f16e645e098d5e469510f3d798c981e6681c Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 17 Oct 2023 02:18:43 +0000 Subject: [PATCH 86/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/root/attach.go | 2 +- cmd/oras/root/push.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 4d82a72c8..6588c7925 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -171,7 +171,7 @@ func runAttach(ctx context.Context, opts attachOptions) error { } // Attach - root, err := doPush(pack, copy, dst) + root, err := doPush(dst, pack, copy) if err != nil { return err } diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index be462c930..ab86098d3 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -209,7 +209,7 @@ func runPush(ctx context.Context, opts pushOptions) error { } // Push - root, err := doPush(pack, copy, dst) + root, err := doPush(dst, pack, copy) if err != nil { return err } @@ -233,7 +233,7 @@ func runPush(ctx context.Context, opts pushOptions) error { return opts.ExportManifest(ctx, memoryStore, root) } -func doPush(pack packFunc, copy copyFunc, dst oras.Target) (ocispec.Descriptor, error) { +func doPush(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { if tracked, ok := dst.(*track.Target); ok { defer tracked.Close() } From 0de9997e41122ed12e2c322ff1fefe62c6222013 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 17 Oct 2023 02:48:43 +0000 Subject: [PATCH 87/98] remove printStatus output target.Prompt Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 5 +++-- cmd/oras/root/cp.go | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index 81c086855..2b841999e 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -17,13 +17,13 @@ package track import ( "context" + "errors" "io" "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/registry" - "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/display/progress" ) @@ -102,7 +102,8 @@ func (t *Target) Close() error { // If Target is not set, only prints status. func (t *Target) Prompt(desc ocispec.Descriptor, prompt string, verbose bool) error { if t == nil { - return display.PrintStatus(desc, prompt, verbose) + // this should not happen + return errors.New("cannot output progress with nil tracked target") } status, err := t.manager.Add() if err != nil { diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index e18283602..42ebab7b1 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -164,6 +164,9 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar } extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + if tracked == nil { + return display.PrintStatus(desc, "Skipped", opts.Verbose) + } return tracked.Prompt(desc, "Exists ", opts.Verbose) } if opts.TTY == nil { From e0f810a876e65459de3daf61695b1abc76f1b303 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 17 Oct 2023 02:59:01 +0000 Subject: [PATCH 88/98] fix attach Signed-off-by: Billy Zha --- cmd/oras/root/push.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index ab86098d3..63a9a9a90 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -245,6 +245,9 @@ func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, v committed := &sync.Map{} opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + if tracked == nil { + return display.PrintStatus(desc, "Exists ", verbose) + } return tracked.Prompt(desc, "Exists ", verbose) } if tracked == nil { From 060940bc8b9e85f965d7a95308b682dc5542d1ef Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 17 Oct 2023 07:58:57 +0000 Subject: [PATCH 89/98] refactor target Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 50 ++++++++++++++--------- cmd/oras/root/attach.go | 2 +- cmd/oras/root/cp.go | 2 +- cmd/oras/root/push.go | 6 +-- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index 2b841999e..be62ac8c6 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -27,31 +27,48 @@ import ( "oras.land/oras/cmd/oras/internal/display/progress" ) -// Target tracks the progress of pushing ot underlying oras.Target. -type Target struct { +type Trackable interface { + oras.GraphTarget + Close() error + Prompt(ocispec.Descriptor, string, bool) error +} + +type graphTarget struct { oras.GraphTarget manager progress.Manager actionPrompt string donePrompt string } +type referenceGraphTarget struct { + *graphTarget + registry.ReferencePusher +} + // NewTarget creates a new tracked Target. -func NewTarget(t oras.GraphTarget, actionPrompt, donePrompt string, tty *os.File) (*Target, error) { +func NewTarget(t oras.GraphTarget, actionPrompt, donePrompt string, tty *os.File) (Trackable, error) { manager, err := progress.NewManager(tty) if err != nil { return nil, err } - - return &Target{ + gt := &graphTarget{ GraphTarget: t, manager: manager, actionPrompt: actionPrompt, donePrompt: donePrompt, - }, nil + } + + if refPusher, ok := t.(registry.ReferencePusher); ok { + return &referenceGraphTarget{ + graphTarget: gt, + ReferencePusher: refPusher, + }, nil + } + return gt, nil } // Push pushes the content to the Target with tracking. -func (t *Target) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { +func (t *graphTarget) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) if err != nil { return err @@ -66,21 +83,14 @@ func (t *Target) Push(ctx context.Context, expected ocispec.Descriptor, content } // PushReference pushes the content to the Target with tracking. -func (t *Target) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { - r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) +func (rgt *referenceGraphTarget) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { + r, err := managedReader(content, expected, rgt.manager, rgt.actionPrompt, rgt.donePrompt) if err != nil { return err } defer r.Close() r.Start() - if rp, ok := t.GraphTarget.(registry.ReferencePusher); ok { - err = rp.PushReference(ctx, expected, r, reference) - } else { - if err := t.GraphTarget.Push(ctx, expected, r); err != nil { - return err - } - err = t.GraphTarget.Tag(ctx, expected, reference) - } + err = rgt.ReferencePusher.PushReference(ctx, expected, r, reference) if err != nil { return err } @@ -89,18 +99,18 @@ func (t *Target) PushReference(ctx context.Context, expected ocispec.Descriptor, } // Predecessors returns the predecessors of the node if supported. -func (t *Target) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { +func (t *graphTarget) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { return t.GraphTarget.Predecessors(ctx, node) } // Close closes the tracking manager. -func (t *Target) Close() error { +func (t *graphTarget) Close() error { return t.manager.Close() } // Prompt prompts the user with the provided prompt and descriptor. // If Target is not set, only prints status. -func (t *Target) Prompt(desc ocispec.Descriptor, prompt string, verbose bool) error { +func (t *graphTarget) Prompt(desc ocispec.Descriptor, prompt string, verbose bool) error { if t == nil { // this should not happen return errors.New("cannot output progress with nil tracked target") diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 6588c7925..1398ec9d8 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -132,7 +132,7 @@ func runAttach(ctx context.Context, opts attachOptions) error { } // prepare push - var tracked *track.Target + var tracked track.Trackable if opts.TTY != nil { tracked, err = track.NewTarget(dst, "Uploading", "Uploaded ", opts.TTY) if err != nil { diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 42ebab7b1..165d94893 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -146,7 +146,7 @@ func runCopy(ctx context.Context, opts copyOptions) error { } func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTarget, opts copyOptions) (ocispec.Descriptor, error) { - var tracked *track.Target + var tracked track.Trackable var err error // Prepare copy options committed := &sync.Map{} diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 63a9a9a90..ac1284b94 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -183,7 +183,7 @@ func runPush(ctx context.Context, opts pushOptions) error { if err != nil { return err } - var tracked *track.Target + var tracked track.Trackable if opts.TTY != nil { tracked, err = track.NewTarget(dst, "Uploading", "Uploaded ", opts.TTY) if err != nil { @@ -234,14 +234,14 @@ func runPush(ctx context.Context, opts pushOptions) error { } func doPush(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { - if tracked, ok := dst.(*track.Target); ok { + if tracked, ok := dst.(track.Trackable); ok { defer tracked.Close() } // Push return pushArtifact(dst, pack, copy) } -func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool, tracked *track.Target) { +func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool, tracked track.Trackable) { committed := &sync.Map{} opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) From 692fd3160c2d7a6a0fe3a9865525af66e540f0fc Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 17 Oct 2023 08:53:24 +0000 Subject: [PATCH 90/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 23 +++++++----------- cmd/oras/root/attach.go | 18 +++++++------- cmd/oras/root/cp.go | 2 +- cmd/oras/root/push.go | 29 ++++++++++++++++------- 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index be62ac8c6..fa3e3b579 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -27,10 +27,11 @@ import ( "oras.land/oras/cmd/oras/internal/display/progress" ) -type Trackable interface { +// GraphTarget is a tracked oras.GraphTarget. +type GraphTarget interface { oras.GraphTarget - Close() error - Prompt(ocispec.Descriptor, string, bool) error + io.Closer + Prompt(desc ocispec.Descriptor, prompt string, verbose bool) error } type graphTarget struct { @@ -42,11 +43,10 @@ type graphTarget struct { type referenceGraphTarget struct { *graphTarget - registry.ReferencePusher } // NewTarget creates a new tracked Target. -func NewTarget(t oras.GraphTarget, actionPrompt, donePrompt string, tty *os.File) (Trackable, error) { +func NewTarget(t oras.GraphTarget, actionPrompt, donePrompt string, tty *os.File) (GraphTarget, error) { manager, err := progress.NewManager(tty) if err != nil { return nil, err @@ -58,10 +58,9 @@ func NewTarget(t oras.GraphTarget, actionPrompt, donePrompt string, tty *os.File donePrompt: donePrompt, } - if refPusher, ok := t.(registry.ReferencePusher); ok { + if _, ok := t.(registry.ReferencePusher); ok { return &referenceGraphTarget{ - graphTarget: gt, - ReferencePusher: refPusher, + graphTarget: gt, }, nil } return gt, nil @@ -90,7 +89,7 @@ func (rgt *referenceGraphTarget) PushReference(ctx context.Context, expected oci } defer r.Close() r.Start() - err = rgt.ReferencePusher.PushReference(ctx, expected, r, reference) + err = rgt.GraphTarget.(registry.ReferencePusher).PushReference(ctx, expected, r, reference) if err != nil { return err } @@ -98,18 +97,12 @@ func (rgt *referenceGraphTarget) PushReference(ctx context.Context, expected oci return nil } -// Predecessors returns the predecessors of the node if supported. -func (t *graphTarget) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { - return t.GraphTarget.Predecessors(ctx, node) -} - // Close closes the tracking manager. func (t *graphTarget) Close() error { return t.manager.Close() } // Prompt prompts the user with the provided prompt and descriptor. -// If Target is not set, only prints status. func (t *graphTarget) Prompt(desc ocispec.Descriptor, prompt string, verbose bool) error { if t == nil { // this should not happen diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 1398ec9d8..e9c1ba7c7 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -132,14 +132,15 @@ func runAttach(ctx context.Context, opts attachOptions) error { } // prepare push - var tracked track.Trackable - if opts.TTY != nil { - tracked, err = track.NewTarget(dst, "Uploading", "Uploaded ", opts.TTY) - if err != nil { - return err - } - dst = tracked + var tracked track.GraphTarget + dst, tracked, err = getTrackedTarget(dst, opts.TTY) + if err != nil { + return err } + graphCopyOptions := oras.DefaultCopyGraphOptions + graphCopyOptions.Concurrency = opts.concurrency + updateDisplayOption(&graphCopyOptions, store, opts.Verbose, tracked) + packOpts := oras.PackManifestOptions{ Subject: &subject, ManifestAnnotations: annotations[option.AnnotationManifest], @@ -149,9 +150,6 @@ func runAttach(ctx context.Context, opts attachOptions) error { return oras.PackManifest(ctx, store, oras.PackManifestVersion1_1_RC4, opts.artifactType, packOpts) } - graphCopyOptions := oras.DefaultCopyGraphOptions - graphCopyOptions.Concurrency = opts.concurrency - updateDisplayOption(&graphCopyOptions, store, opts.Verbose, tracked) copy := func(root ocispec.Descriptor) error { graphCopyOptions.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { if content.Equal(node, root) { diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 165d94893..4f61d279c 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -146,7 +146,7 @@ func runCopy(ctx context.Context, opts copyOptions) error { } func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTarget, opts copyOptions) (ocispec.Descriptor, error) { - var tracked track.Trackable + var tracked track.GraphTarget var err error // Prepare copy options committed := &sync.Map{} diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index ac1284b94..386e69429 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -19,6 +19,7 @@ import ( "context" "errors" "fmt" + "os" "strings" "sync" @@ -183,13 +184,10 @@ func runPush(ctx context.Context, opts pushOptions) error { if err != nil { return err } - var tracked track.Trackable - if opts.TTY != nil { - tracked, err = track.NewTarget(dst, "Uploading", "Uploaded ", opts.TTY) - if err != nil { - return err - } - dst = tracked + var tracked track.GraphTarget + dst, tracked, err = getTrackedTarget(dst, opts.TTY) + if err != nil { + return err } copyOptions := oras.DefaultCopyOptions copyOptions.Concurrency = opts.concurrency @@ -234,14 +232,14 @@ func runPush(ctx context.Context, opts pushOptions) error { } func doPush(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { - if tracked, ok := dst.(track.Trackable); ok { + if tracked, ok := dst.(track.GraphTarget); ok { defer tracked.Close() } // Push return pushArtifact(dst, pack, copy) } -func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool, tracked track.Trackable) { +func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool, tracked track.GraphTarget) { committed := &sync.Map{} opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) @@ -276,6 +274,19 @@ func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, v type packFunc func() (ocispec.Descriptor, error) type copyFunc func(desc ocispec.Descriptor) error +func getTrackedTarget(gt oras.GraphTarget, tty *os.File) (oras.GraphTarget, track.GraphTarget, error) { + var tracked track.GraphTarget + var err error + if tty != nil { + tracked, err = track.NewTarget(gt, "Uploading", "Uploaded ", tty) + if err != nil { + return nil, nil, err + } + gt = tracked + } + return gt, tracked, nil +} + func pushArtifact(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { root, err := pack() if err != nil { From 8a52cc94126e2cbb13e24be56c4a085096a287a2 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Tue, 17 Oct 2023 08:54:16 +0000 Subject: [PATCH 91/98] doc clean Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index fa3e3b579..a92e73650 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -66,7 +66,7 @@ func NewTarget(t oras.GraphTarget, actionPrompt, donePrompt string, tty *os.File return gt, nil } -// Push pushes the content to the Target with tracking. +// Push pushes the content to the base oras.GraphTarget with tracking. func (t *graphTarget) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { r, err := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) if err != nil { @@ -81,7 +81,7 @@ func (t *graphTarget) Push(ctx context.Context, expected ocispec.Descriptor, con return nil } -// PushReference pushes the content to the Target with tracking. +// PushReference pushes the content to the base oras.GraphTarget with tracking. func (rgt *referenceGraphTarget) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { r, err := managedReader(content, expected, rgt.manager, rgt.actionPrompt, rgt.donePrompt) if err != nil { From 826872f862f4bff819c360be8a001d2ab5662eed Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 19 Oct 2023 03:16:06 +0000 Subject: [PATCH 92/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/internal/display/print.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/oras/internal/display/print.go b/cmd/oras/internal/display/print.go index ef1e8c835..470410fc9 100644 --- a/cmd/oras/internal/display/print.go +++ b/cmd/oras/internal/display/print.go @@ -29,9 +29,10 @@ import ( var printLock sync.Mutex -type printFunc func(ocispec.Descriptor) error +// PrintFunc is the function type returned by StatusPrinter. +type PrintFunc func(ocispec.Descriptor) error -// Print objects to display concurrent-safely +// Print objects to display concurrent-safely. func Print(a ...any) error { printLock.Lock() defer printLock.Unlock() @@ -40,7 +41,7 @@ func Print(a ...any) error { } // StatusPrinter returns a tracking function for transfer status. -func StatusPrinter(status string, verbose bool) printFunc { +func StatusPrinter(status string, verbose bool) PrintFunc { return func(desc ocispec.Descriptor) error { return PrintStatus(desc, status, verbose) } @@ -60,7 +61,7 @@ func PrintStatus(desc ocispec.Descriptor, status string, verbose bool) error { } // PrintSuccessorStatus prints transfer status of successors. -func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print printFunc) error { +func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print PrintFunc) error { successors, err := content.Successors(ctx, fetcher, desc) if err != nil { return err From 26cb0b7e292e3ffdb0833f1a0da484a8f5983d96 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 19 Oct 2023 03:30:40 +0000 Subject: [PATCH 93/98] resolve comment for push Signed-off-by: Billy Zha --- cmd/oras/root/push.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 386e69429..c53a3b9b5 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -241,15 +241,16 @@ func doPush(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool, tracked track.GraphTarget) { committed := &sync.Map{} - opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { - committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - if tracked == nil { - return display.PrintStatus(desc, "Exists ", verbose) - } - return tracked.Prompt(desc, "Exists ", verbose) - } + if tracked == nil { // non TTY + opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + if tracked == nil { + return display.PrintStatus(desc, "Exists ", verbose) + } + return tracked.Prompt(desc, "Exists ", verbose) + } opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return display.PrintStatus(desc, "Uploading", verbose) } @@ -263,6 +264,10 @@ func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, v return } // TTY + opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + return tracked.Prompt(desc, "Exists ", verbose) + } opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return display.PrintSuccessorStatus(ctx, desc, fetcher, committed, func(d ocispec.Descriptor) error { @@ -275,16 +280,14 @@ type packFunc func() (ocispec.Descriptor, error) type copyFunc func(desc ocispec.Descriptor) error func getTrackedTarget(gt oras.GraphTarget, tty *os.File) (oras.GraphTarget, track.GraphTarget, error) { - var tracked track.GraphTarget - var err error - if tty != nil { - tracked, err = track.NewTarget(gt, "Uploading", "Uploaded ", tty) - if err != nil { - return nil, nil, err - } - gt = tracked + if tty == nil { + return gt, nil, nil + } + tracked, err := track.NewTarget(gt, "Uploading", "Uploaded ", tty) + if err != nil { + return nil, nil, err } - return gt, tracked, nil + return tracked, tracked, nil } func pushArtifact(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { From d648b94dad0c09a73ec46a45d122e481cc7a1239 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 19 Oct 2023 03:56:24 +0000 Subject: [PATCH 94/98] resolve comment Signed-off-by: Billy Zha --- cmd/oras/root/cp.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 4f61d279c..e6ba6485b 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -146,8 +146,6 @@ func runCopy(ctx context.Context, opts copyOptions) error { } func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTarget, opts copyOptions) (ocispec.Descriptor, error) { - var tracked track.GraphTarget - var err error // Prepare copy options committed := &sync.Map{} extendedCopyOptions := oras.DefaultExtendedCopyOptions @@ -162,15 +160,13 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar return successorPrinter(d) }) } - extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { - committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - if tracked == nil { - return display.PrintStatus(desc, "Skipped", opts.Verbose) - } - return tracked.Prompt(desc, "Exists ", opts.Verbose) - } + if opts.TTY == nil { // none TTY output + extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + return display.PrintStatus(desc, "Exists ", opts.Verbose) + } extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return display.PrintStatus(desc, "Copying", opts.Verbose) } @@ -183,18 +179,24 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar } } else { // TTY output - successorPrinter = func(desc ocispec.Descriptor) error { - return tracked.Prompt(desc, "Skipped", opts.Verbose) - } - tracked, err = track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) + + tracked, err := track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) if err != nil { return ocispec.Descriptor{}, err } defer tracked.Close() dst = tracked + successorPrinter = func(desc ocispec.Descriptor) error { + return tracked.Prompt(desc, "Skipped", opts.Verbose) + } + extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + return tracked.Prompt(desc, "Exists ", opts.Verbose) + } } var desc ocispec.Descriptor + var err error rOpts := oras.DefaultResolveOptions rOpts.TargetPlatform = opts.Platform.Platform if opts.recursive { From f6ab85d5cefed4a6bcd2137de6e35996d056f6f1 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 19 Oct 2023 04:11:08 +0000 Subject: [PATCH 95/98] remove unnecessary code Signed-off-by: Billy Zha --- cmd/oras/internal/display/track/target.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cmd/oras/internal/display/track/target.go b/cmd/oras/internal/display/track/target.go index a92e73650..5b640062f 100644 --- a/cmd/oras/internal/display/track/target.go +++ b/cmd/oras/internal/display/track/target.go @@ -17,7 +17,6 @@ package track import ( "context" - "errors" "io" "os" @@ -104,10 +103,6 @@ func (t *graphTarget) Close() error { // Prompt prompts the user with the provided prompt and descriptor. func (t *graphTarget) Prompt(desc ocispec.Descriptor, prompt string, verbose bool) error { - if t == nil { - // this should not happen - return errors.New("cannot output progress with nil tracked target") - } status, err := t.manager.Add() if err != nil { return err From 4841c1cacbe787eb7dadec893a6b2841e046b9bb Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 23 Oct 2023 02:58:19 +0000 Subject: [PATCH 96/98] resolve comments Signed-off-by: Billy Zha --- cmd/oras/root/cp.go | 15 ++++++++++++--- cmd/oras/root/push.go | 5 +---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index e6ba6485b..8131c2c9f 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -170,16 +170,15 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return display.PrintStatus(desc, "Copying", opts.Verbose) } - postCopy := extendedCopyOptions.PostCopy extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - if err := postCopy(ctx, desc); err != nil { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + if err := display.PrintSuccessorStatus(ctx, desc, dst, committed, display.StatusPrinter("Skipped", opts.Verbose)); err != nil { return err } return display.PrintStatus(desc, "Copied ", opts.Verbose) } } else { // TTY output - tracked, err := track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) if err != nil { return ocispec.Descriptor{}, err @@ -193,6 +192,16 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return tracked.Prompt(desc, "Exists ", opts.Verbose) } + extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + successorPrinter = func(desc ocispec.Descriptor) error { + return tracked.Prompt(desc, "Skipped", opts.Verbose) + } + if err := display.PrintSuccessorStatus(ctx, desc, tracked, committed, successorPrinter); err != nil { + return err + } + return display.PrintStatus(desc, "Copied ", opts.Verbose) + } } var desc ocispec.Descriptor diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index c53a3b9b5..f45714570 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -246,10 +246,7 @@ func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, v // non TTY opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - if tracked == nil { - return display.PrintStatus(desc, "Exists ", verbose) - } - return tracked.Prompt(desc, "Exists ", verbose) + return display.PrintStatus(desc, "Exists ", verbose) } opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return display.PrintStatus(desc, "Uploading", verbose) From f30a5d526342e4f5cbbfcc3953d56c33a8f687ef Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 23 Oct 2023 06:18:08 +0000 Subject: [PATCH 97/98] remove unused code Signed-off-by: Billy Zha --- cmd/oras/root/cp.go | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 8131c2c9f..cfce93219 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -153,13 +153,6 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar extendedCopyOptions.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { return graph.Referrers(ctx, src, desc, "") } - successorPrinter := display.StatusPrinter("Skipped", opts.Verbose) - extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintSuccessorStatus(ctx, desc, dst, committed, func(d ocispec.Descriptor) error { - return successorPrinter(d) - }) - } if opts.TTY == nil { // none TTY output @@ -185,22 +178,16 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar } defer tracked.Close() dst = tracked - successorPrinter = func(desc ocispec.Descriptor) error { - return tracked.Prompt(desc, "Skipped", opts.Verbose) - } extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return tracked.Prompt(desc, "Exists ", opts.Verbose) } extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - successorPrinter = func(desc ocispec.Descriptor) error { + successorPrinter := func(desc ocispec.Descriptor) error { return tracked.Prompt(desc, "Skipped", opts.Verbose) } - if err := display.PrintSuccessorStatus(ctx, desc, tracked, committed, successorPrinter); err != nil { - return err - } - return display.PrintStatus(desc, "Copied ", opts.Verbose) + return display.PrintSuccessorStatus(ctx, desc, tracked, committed, successorPrinter) } } From 0406738002b9fc6345c40511d24930ff3179b3b4 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 23 Oct 2023 06:19:58 +0000 Subject: [PATCH 98/98] merge declaration Signed-off-by: Billy Zha --- cmd/oras/root/cp.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index cfce93219..4437c3e35 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -184,10 +184,9 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar } extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - successorPrinter := func(desc ocispec.Descriptor) error { + return display.PrintSuccessorStatus(ctx, desc, tracked, committed, func(desc ocispec.Descriptor) error { return tracked.Prompt(desc, "Skipped", opts.Verbose) - } - return display.PrintSuccessorStatus(ctx, desc, tracked, committed, successorPrinter) + }) } }