From 5a112d8fbae6fcf9c88000e24dc32f39fbb5cfc1 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 14 Feb 2026 22:17:31 +0100 Subject: [PATCH] docker cp: report both content size and transferred size When copying files to a container, the `docker cp` command would print a message reporting the size of files copied. However, this size was based on the size of the TAR stream sent, which includes the size of the TAR headers. docker container create --name my-container busybox touch empty-file docker cp empty-file my-container:/empty-file Successfully copied 1.54kB to my-container:/empty-file This patch adds a `calcTARContentSize` utility, which uses the TAR headers to calculate the size of files included. With this patch applied, the content size is reported, instead of the size of the TAR stream used for transport; docker container create --name my-container busybox touch empty-file docker cp empty-file my-container:/empty-file Successfully copied 0B (transferred 1.54kB) to my-container:/empty-file mkdir empty-dir docker cp ./empty-dir my-container:/somewhere/ Successfully copied 0B (transferred 1.54kB) to my-container:/somewhere/ docker cp ./cli/command my-container:/files/ Successfully copied 2.01MB (transferred 2.53MB) to my-container:/files/ Signed-off-by: Sebastiaan van Stijn --- cli/command/container/cp.go | 58 +++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go index 5121cb88a594..98f3519ed4bb 100644 --- a/cli/command/container/cp.go +++ b/cli/command/container/cp.go @@ -1,6 +1,7 @@ package container import ( + "archive/tar" "bytes" "context" "errors" @@ -354,6 +355,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo content io.ReadCloser resolvedDstPath string copiedSize int64 + contentSize int64 ) if srcPath == "-" { @@ -387,10 +389,11 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo // extracted. This function also infers from the source and destination // info which directory to extract to, which may be the parent of the // destination that the user specified. - dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) + dstDir, preparedArchive1, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) if err != nil { return err } + preparedArchive := calcTARContentSize(preparedArchive1, &contentSize) defer preparedArchive.Close() resolvedDstPath = dstDir @@ -421,8 +424,9 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo cancel() <-done restore() - _, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path) - + _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully copied %s (transferred %s) to %s:%s\n", + progressHumanSize(contentSize), progressHumanSize(copiedSize), copyConfig.container, dstInfo.Path, + ) return err } @@ -469,3 +473,51 @@ func splitCpArg(arg string) (ctr, path string) { func isAbs(path string) bool { return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator)) } + +// calcTARContentSize calculates the total size of files transferred in +// the TAR archive based on information in the TAR header. This allows +// presenting the data copied, excluding the TAR header. +func calcTARContentSize(srcContent io.ReadCloser, size *int64) io.ReadCloser { + if size == nil { + return srcContent + } + + r, w := io.Pipe() + + go func() { + var total int64 + defer func() { + _ = srcContent.Close() + atomic.StoreInt64(size, total) + }() + + tee := io.TeeReader(srcContent, w) + tr := tar.NewReader(tee) + + for { + hdr, err := tr.Next() + if err != nil { + if errors.Is(err, io.EOF) { + _ = w.Close() + return + } + _ = w.CloseWithError(err) + return + } + + switch hdr.Typeflag { + case tar.TypeReg: + total += hdr.Size + } + + // Drain entry payload (tee forwards bytes to w). + //nolint:gosec // G110: see RebaseArchiveEntries rationale + if _, err := io.Copy(io.Discard, tr); err != nil { + _ = w.CloseWithError(err) + return + } + } + }() + + return r +}