diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 2f7428585..99b7af561 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -175,6 +175,9 @@ func NewManifestPushHandler(printer *output.Printer) metadata.ManifestPushHandle } // NewCopyHandler returns copy handlers. -func NewCopyHandler(printer *output.Printer, fetcher fetcher.Fetcher) (status.CopyHandler, metadata.CopyHandler) { +func NewCopyHandler(printer *output.Printer, tty *os.File, fetcher fetcher.Fetcher) (status.CopyHandler, metadata.CopyHandler) { + if tty != nil { + return status.NewTTYCopyHandler(tty, fetcher), text.NewCopyHandler(printer) + } return status.NewTextCopyHandler(printer, fetcher), text.NewCopyHandler(printer) } diff --git a/cmd/oras/internal/display/status/interface.go b/cmd/oras/internal/display/status/interface.go index c2f0bd8b8..791dcb620 100644 --- a/cmd/oras/internal/display/status/interface.go +++ b/cmd/oras/internal/display/status/interface.go @@ -60,4 +60,6 @@ type CopyHandler interface { PreCopy(ctx context.Context, desc ocispec.Descriptor) error PostCopy(ctx context.Context, desc ocispec.Descriptor) error OnMounted(ctx context.Context, desc ocispec.Descriptor) error + StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) + StopTracking() } diff --git a/cmd/oras/internal/display/status/text.go b/cmd/oras/internal/display/status/text.go index 2ac3a6235..6b6c74eee 100644 --- a/cmd/oras/internal/display/status/text.go +++ b/cmd/oras/internal/display/status/text.go @@ -147,6 +147,15 @@ func NewTextCopyHandler(printer *output.Printer, fetcher content.Fetcher) CopyHa } } +// StartTracking starts a tracked target from a graph target. +func (ch *TextCopyHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { + return gt, nil +} + +// StopTracking ends the copy tracking for the target. +func (ch *TextCopyHandler) StopTracking() { +} + // OnCopySkipped is called when an object already exists. func (ch *TextCopyHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) diff --git a/cmd/oras/internal/display/status/tty.go b/cmd/oras/internal/display/status/tty.go index 78f639076..769f98b73 100644 --- a/cmd/oras/internal/display/status/tty.go +++ b/cmd/oras/internal/display/status/tty.go @@ -143,3 +143,64 @@ func (ph *TTYPullHandler) TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, St ph.tracked = tracked return tracked, tracked.Close, nil } + +// TTYCopyHandler handles tty status output for copy events. +type TTYCopyHandler struct { + tty *os.File + committed *sync.Map + tracked track.GraphTarget + fetcher content.Fetcher +} + +// NewTTYCopyHandler returns a new handler for copy command. +func NewTTYCopyHandler(tty *os.File, fetcher content.Fetcher) CopyHandler { + return &TTYCopyHandler{ + tty: tty, + fetcher: fetcher, + committed: &sync.Map{}, + } +} + +// StartTracking returns a tracked target from a graph target. +func (ch *TTYCopyHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { + tracked, err := track.NewTarget(gt, copyPromptCopying, copyPromptCopied, ch.tty) + ch.tracked = tracked + return tracked, err +} + +// StopTracking ends the copy tracking for the target. +func (ch *TTYCopyHandler) StopTracking() { + _ = ch.tracked.Close() +} + +// OnCopySkipped is called when an object already exists. +func (ch *TTYCopyHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { + ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + return ch.tracked.Prompt(desc, copyPromptExists) +} + +// PreCopy implements PreCopy of CopyHandler. +func (ch *TTYCopyHandler) PreCopy(_ context.Context, _ ocispec.Descriptor) error { + return nil +} + +// PostCopy implements PostCopy of CopyHandler. +func (ch *TTYCopyHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { + ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + successors, err := graph.FilteredSuccessors(ctx, desc, ch.tracked, DeduplicatedFilter(ch.committed)) + if err != nil { + return err + } + for _, successor := range successors { + if err = ch.tracked.Prompt(successor, copyPromptSkipped); err != nil { + return err + } + } + return nil +} + +// OnMounted implements OnMounted of CopyHandler. +func (ch *TTYCopyHandler) OnMounted(_ context.Context, desc ocispec.Descriptor) error { + ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) + return ch.tracked.Prompt(desc, copyPromptMounted) +} diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 9f0fd6fa4..184907c86 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -19,11 +19,9 @@ import ( "context" "encoding/json" "fmt" - "oras.land/oras/cmd/oras/internal/display/status" "slices" "strings" - "sync" - + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" @@ -35,7 +33,7 @@ import ( "oras.land/oras/cmd/oras/internal/argument" "oras.land/oras/cmd/oras/internal/command" "oras.land/oras/cmd/oras/internal/display" - "oras.land/oras/cmd/oras/internal/display/status/track" + "oras.land/oras/cmd/oras/internal/display/status" oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/docker" @@ -127,7 +125,7 @@ func runCopy(cmd *cobra.Command, opts *copyOptions) error { return err } ctx = registryutil.WithScopeHint(ctx, dst, auth.ActionPull, auth.ActionPush) - copyHandler, handler := display.NewCopyHandler(opts.Printer, dst) + copyHandler, handler := display.NewCopyHandler(opts.Printer, opts.TTY, dst) desc, err := doCopy(ctx, copyHandler, src, dst, opts) if err != nil { @@ -154,22 +152,14 @@ func runCopy(cmd *cobra.Command, opts *copyOptions) error { return nil } -func doCopy(ctx context.Context, copyHandler status.CopyHandler, src oras.ReadOnlyGraphTarget, dst oras.GraphTarget, opts *copyOptions) (ocispec.Descriptor, error) { +func doCopy(ctx context.Context, copyHandler status.CopyHandler, src oras.ReadOnlyGraphTarget, dst oras.GraphTarget, opts *copyOptions) (desc ocispec.Descriptor, err error) { // Prepare copy options - committed := &sync.Map{} extendedCopyOptions := oras.DefaultExtendedCopyOptions extendedCopyOptions.Concurrency = opts.concurrency extendedCopyOptions.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { return registry.Referrers(ctx, src, desc, "") } - const ( - promptExists = "Exists " - promptCopying = "Copying" - promptCopied = "Copied " - promptSkipped = "Skipped" - promptMounted = "Mounted" - ) srcRepo, srcIsRemote := src.(*remote.Repository) dstRepo, dstIsRemote := dst.(*remote.Repository) if srcIsRemote && dstIsRemote && srcRepo.Reference.Registry == dstRepo.Reference.Registry { @@ -177,45 +167,16 @@ func doCopy(ctx context.Context, copyHandler status.CopyHandler, src oras.ReadOn return []string{srcRepo.Reference.Repository}, nil } } - if opts.TTY == nil { - // no TTY output - extendedCopyOptions.OnCopySkipped = copyHandler.OnCopySkipped - extendedCopyOptions.PreCopy = copyHandler.PreCopy - extendedCopyOptions.PostCopy = copyHandler.PostCopy - extendedCopyOptions.OnMounted = copyHandler.OnMounted - } else { - // TTY output - tracked, err := track.NewTarget(dst, promptCopying, promptCopied, opts.TTY) - if err != nil { - return ocispec.Descriptor{}, err - } - defer tracked.Close() - dst = tracked - extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { - committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return tracked.Prompt(desc, promptExists) - } - extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - successors, err := graph.FilteredSuccessors(ctx, desc, tracked, status.DeduplicatedFilter(committed)) - if err != nil { - return err - } - for _, successor := range successors { - if err = tracked.Prompt(successor, promptSkipped); err != nil { - return err - } - } - return nil - } - extendedCopyOptions.OnMounted = func(ctx context.Context, desc ocispec.Descriptor) error { - committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return tracked.Prompt(desc, promptMounted) - } + dst, err = copyHandler.StartTracking(dst) + if err != nil { + return desc, err } + defer copyHandler.StopTracking() + extendedCopyOptions.OnCopySkipped = copyHandler.OnCopySkipped + extendedCopyOptions.PreCopy = copyHandler.PreCopy + extendedCopyOptions.PostCopy = copyHandler.PostCopy + extendedCopyOptions.OnMounted = copyHandler.OnMounted - var desc ocispec.Descriptor - var err error rOpts := oras.DefaultResolveOptions rOpts.TargetPlatform = opts.Platform.Platform if opts.recursive {