Skip to content

Commit 4e6606a

Browse files
committed
Report accurate file sizes in docker cp success messages
Signed-off-by: Artem Lytkin <iprintercanon@gmail.com>
1 parent 7b93d61 commit 4e6606a

File tree

3 files changed

+134
-2
lines changed

3 files changed

+134
-2
lines changed

cli/command/container/client_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type fakeClient struct {
3636
infoFunc func() (client.SystemInfoResult, error)
3737
containerStatPathFunc func(containerID, path string) (client.ContainerStatPathResult, error)
3838
containerCopyFromFunc func(containerID, srcPath string) (client.CopyFromContainerResult, error)
39+
containerCopyToFunc func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error)
3940
logFunc func(string, client.ContainerLogsOptions) (client.ContainerLogsResult, error)
4041
waitFunc func(string) client.ContainerWaitResult
4142
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
@@ -128,6 +129,13 @@ func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, op
128129
return client.CopyFromContainerResult{}, nil
129130
}
130131

132+
func (f *fakeClient) CopyToContainer(_ context.Context, containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
133+
if f.containerCopyToFunc != nil {
134+
return f.containerCopyToFunc(containerID, options)
135+
}
136+
return client.CopyToContainerResult{}, nil
137+
}
138+
131139
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
132140
if f.logFunc != nil {
133141
return f.logFunc(containerID, options)

cli/command/container/cp.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,34 @@ func progressHumanSize(n int64) string {
168168
return units.HumanSizeWithPrecision(float64(n), 3)
169169
}
170170

171+
// localContentSize returns the total size of regular file content at path.
172+
// For a regular file it returns the file size. For a directory it walks
173+
// the tree and sums sizes of all regular files.
174+
func localContentSize(path string) (int64, error) {
175+
fi, err := os.Lstat(path)
176+
if err != nil {
177+
return -1, err
178+
}
179+
if !fi.IsDir() {
180+
return fi.Size(), nil
181+
}
182+
var total int64
183+
err = filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error {
184+
if err != nil {
185+
return err
186+
}
187+
if d.Type().IsRegular() {
188+
info, err := d.Info()
189+
if err != nil {
190+
return err
191+
}
192+
total += info.Size()
193+
}
194+
return nil
195+
})
196+
return total, err
197+
}
198+
171199
func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error {
172200
srcContainer, srcPath := splitCpArg(opts.source)
173201
destContainer, destPath := splitCpArg(opts.destination)
@@ -295,7 +323,11 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
295323
cancel()
296324
<-done
297325
restore()
298-
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
326+
reportedSize := copiedSize
327+
if !cpRes.Stat.Mode.IsDir() {
328+
reportedSize = cpRes.Stat.Size
329+
}
330+
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(reportedSize), "to", dstPath)
299331

300332
return res
301333
}
@@ -354,6 +386,8 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
354386
content io.ReadCloser
355387
resolvedDstPath string
356388
copiedSize int64
389+
contentSize int64
390+
sizeErr error
357391
)
358392

359393
if srcPath == "-" {
@@ -369,6 +403,8 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
369403
return err
370404
}
371405

406+
contentSize, sizeErr = localContentSize(srcInfo.Path)
407+
372408
srcArchive, err := archive.TarResource(srcInfo)
373409
if err != nil {
374410
return err
@@ -421,7 +457,11 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
421457
cancel()
422458
<-done
423459
restore()
424-
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
460+
reportedSize := copiedSize
461+
if sizeErr == nil {
462+
reportedSize = contentSize
463+
}
464+
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(reportedSize), "to", copyConfig.container+":"+dstInfo.Path)
425465

426466
return err
427467
}

cli/command/container/cp_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/docker/cli/internal/test"
1212
"github.com/moby/go-archive"
1313
"github.com/moby/go-archive/compression"
14+
"github.com/moby/moby/api/types/container"
1415
"github.com/moby/moby/client"
1516
"gotest.tools/v3/assert"
1617
is "gotest.tools/v3/assert/cmp"
@@ -211,3 +212,86 @@ func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
211212
expected := `"/dev/random" must be a directory or a regular file`
212213
assert.ErrorContains(t, err, expected)
213214
}
215+
216+
func TestCopyFromContainerReportsFileSize(t *testing.T) {
217+
// The file content is "hello" (5 bytes), but the TAR archive wrapping
218+
// it is much larger due to headers and padding. The success message
219+
// should report the actual file size (5B), not the TAR stream size.
220+
srcDir := fs.NewDir(t, "cp-test-from",
221+
fs.WithFile("file1", "hello"))
222+
223+
destDir := fs.NewDir(t, "cp-test-from-dest")
224+
225+
const fileSize int64 = 5
226+
fakeCli := test.NewFakeCli(&fakeClient{
227+
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
228+
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
229+
return client.CopyFromContainerResult{
230+
Content: readCloser,
231+
Stat: container.PathStat{
232+
Name: "file1",
233+
Size: fileSize,
234+
},
235+
}, err
236+
},
237+
})
238+
err := runCopy(context.TODO(), fakeCli, copyOptions{
239+
source: "container:/file1",
240+
destination: destDir.Path(),
241+
})
242+
assert.NilError(t, err)
243+
assert.Check(t, is.Contains(fakeCli.ErrBuffer().String(), "5B"))
244+
}
245+
246+
func TestCopyToContainerReportsFileSize(t *testing.T) {
247+
// Create a temp file with known content ("hello" = 5 bytes).
248+
// The TAR archive sent to the container is larger, but the success
249+
// message should report the actual content size.
250+
srcFile := fs.NewFile(t, "cp-test-to", fs.WithContent("hello"))
251+
252+
fakeCli := test.NewFakeCli(&fakeClient{
253+
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
254+
return client.ContainerStatPathResult{
255+
Stat: container.PathStat{
256+
Name: "tmp",
257+
Mode: os.ModeDir | 0o755,
258+
},
259+
}, nil
260+
},
261+
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
262+
_, _ = io.Copy(io.Discard, options.Content)
263+
return client.CopyToContainerResult{}, nil
264+
},
265+
})
266+
err := runCopy(context.TODO(), fakeCli, copyOptions{
267+
source: srcFile.Path(),
268+
destination: "container:/tmp",
269+
})
270+
assert.NilError(t, err)
271+
assert.Check(t, is.Contains(fakeCli.ErrBuffer().String(), "5B"))
272+
}
273+
274+
func TestCopyToContainerReportsEmptyFileSize(t *testing.T) {
275+
srcFile := fs.NewFile(t, "cp-test-empty", fs.WithContent(""))
276+
277+
fakeCli := test.NewFakeCli(&fakeClient{
278+
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
279+
return client.ContainerStatPathResult{
280+
Stat: container.PathStat{
281+
Name: "tmp",
282+
Mode: os.ModeDir | 0o755,
283+
},
284+
}, nil
285+
},
286+
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
287+
_, _ = io.Copy(io.Discard, options.Content)
288+
return client.CopyToContainerResult{}, nil
289+
},
290+
})
291+
err := runCopy(context.TODO(), fakeCli, copyOptions{
292+
source: srcFile.Path(),
293+
destination: "container:/tmp",
294+
})
295+
assert.NilError(t, err)
296+
assert.Check(t, is.Contains(fakeCli.ErrBuffer().String(), "0B"))
297+
}

0 commit comments

Comments
 (0)