Browse code

client/container_copy: Wrap options and result struct

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>

Paweł Gronowski authored on 2025/10/29 21:05:23
Showing 11 changed files
... ...
@@ -67,7 +67,7 @@ type ContainerAPIClient interface {
67 67
 	ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error)
68 68
 	ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error)
69 69
 	ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error)
70
-	ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error)
70
+	ContainerStatPath(ctx context.Context, container string, options ContainerStatPathOptions) (ContainerStatPathResult, error)
71 71
 	ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error)
72 72
 	ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error)
73 73
 	ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error)
... ...
@@ -75,8 +75,8 @@ type ContainerAPIClient interface {
75 75
 	ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error)
76 76
 	ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error)
77 77
 	ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult
78
-	CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error)
79
-	CopyToContainer(ctx context.Context, container, path string, content io.Reader, options CopyToContainerOptions) error
78
+	CopyFromContainer(ctx context.Context, container string, options CopyFromContainerOptions) (CopyFromContainerResult, error)
79
+	CopyToContainer(ctx context.Context, container string, options CopyToContainerOptions) (CopyToContainerResult, error)
80 80
 	ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error)
81 81
 }
82 82
 
... ...
@@ -14,41 +14,57 @@ import (
14 14
 	"github.com/moby/moby/api/types/container"
15 15
 )
16 16
 
17
+type ContainerStatPathOptions struct {
18
+	Path string
19
+}
20
+
21
+type ContainerStatPathResult struct {
22
+	Stat container.PathStat
23
+}
24
+
17 25
 // ContainerStatPath returns stat information about a path inside the container filesystem.
18
-func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) {
26
+func (cli *Client) ContainerStatPath(ctx context.Context, containerID string, options ContainerStatPathOptions) (ContainerStatPathResult, error) {
19 27
 	containerID, err := trimID("container", containerID)
20 28
 	if err != nil {
21
-		return container.PathStat{}, err
29
+		return ContainerStatPathResult{}, err
22 30
 	}
23 31
 
24 32
 	query := url.Values{}
25
-	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
33
+	query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
26 34
 
27 35
 	resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil)
28 36
 	defer ensureReaderClosed(resp)
29 37
 	if err != nil {
30
-		return container.PathStat{}, err
38
+		return ContainerStatPathResult{}, err
39
+	}
40
+	stat, err := getContainerPathStatFromHeader(resp.Header)
41
+	if err != nil {
42
+		return ContainerStatPathResult{}, err
31 43
 	}
32
-	return getContainerPathStatFromHeader(resp.Header)
44
+	return ContainerStatPathResult{Stat: stat}, nil
33 45
 }
34 46
 
35 47
 // CopyToContainerOptions holds information
36 48
 // about files to copy into a container
37 49
 type CopyToContainerOptions struct {
50
+	DestinationPath           string
51
+	Content                   io.Reader
38 52
 	AllowOverwriteDirWithFile bool
39 53
 	CopyUIDGID                bool
40 54
 }
41 55
 
56
+type CopyToContainerResult struct{}
57
+
42 58
 // CopyToContainer copies content into the container filesystem.
43 59
 // Note that `content` must be a Reader for a TAR archive
44
-func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options CopyToContainerOptions) error {
60
+func (cli *Client) CopyToContainer(ctx context.Context, containerID string, options CopyToContainerOptions) (CopyToContainerResult, error) {
45 61
 	containerID, err := trimID("container", containerID)
46 62
 	if err != nil {
47
-		return err
63
+		return CopyToContainerResult{}, err
48 64
 	}
49 65
 
50 66
 	query := url.Values{}
51
-	query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
67
+	query.Set("path", filepath.ToSlash(options.DestinationPath)) // Normalize the paths used in the API.
52 68
 	// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
53 69
 	if !options.AllowOverwriteDirWithFile {
54 70
 		query.Set("noOverwriteDirNonDir", "true")
... ...
@@ -58,29 +74,38 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str
58 58
 		query.Set("copyUIDGID", "true")
59 59
 	}
60 60
 
61
-	response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil)
61
+	response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, options.Content, nil)
62 62
 	defer ensureReaderClosed(response)
63 63
 	if err != nil {
64
-		return err
64
+		return CopyToContainerResult{}, err
65 65
 	}
66 66
 
67
-	return nil
67
+	return CopyToContainerResult{}, nil
68
+}
69
+
70
+type CopyFromContainerOptions struct {
71
+	SourcePath string
72
+}
73
+
74
+type CopyFromContainerResult struct {
75
+	Content io.ReadCloser
76
+	Stat    container.PathStat
68 77
 }
69 78
 
70 79
 // CopyFromContainer gets the content from the container and returns it as a Reader
71 80
 // for a TAR archive to manipulate it in the host. It's up to the caller to close the reader.
72
-func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
81
+func (cli *Client) CopyFromContainer(ctx context.Context, containerID string, options CopyFromContainerOptions) (CopyFromContainerResult, error) {
73 82
 	containerID, err := trimID("container", containerID)
74 83
 	if err != nil {
75
-		return nil, container.PathStat{}, err
84
+		return CopyFromContainerResult{}, err
76 85
 	}
77 86
 
78 87
 	query := make(url.Values, 1)
79
-	query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
88
+	query.Set("path", filepath.ToSlash(options.SourcePath)) // Normalize the paths used in the API.
80 89
 
81 90
 	resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil)
82 91
 	if err != nil {
83
-		return nil, container.PathStat{}, err
92
+		return CopyFromContainerResult{}, err
84 93
 	}
85 94
 
86 95
 	// In order to get the copy behavior right, we need to know information
... ...
@@ -91,9 +116,10 @@ func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath s
91 91
 	// can be when copying a file/dir from one location to another file/dir.
92 92
 	stat, err := getContainerPathStatFromHeader(resp.Header)
93 93
 	if err != nil {
94
-		return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
94
+		ensureReaderClosed(resp)
95
+		return CopyFromContainerResult{Stat: stat}, fmt.Errorf("unable to get resource stat from response: %s", err)
95 96
 	}
96
-	return resp.Body, stat, err
97
+	return CopyFromContainerResult{Content: resp.Body, Stat: stat}, nil
97 98
 }
98 99
 
99 100
 func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {
... ...
@@ -24,14 +24,14 @@ func TestContainerStatPathError(t *testing.T) {
24 24
 	)
25 25
 	assert.NilError(t, err)
26 26
 
27
-	_, err = client.ContainerStatPath(context.Background(), "container_id", "path")
27
+	_, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path"})
28 28
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
29 29
 
30
-	_, err = client.ContainerStatPath(context.Background(), "", "path")
30
+	_, err = client.ContainerStatPath(context.Background(), "", ContainerStatPathOptions{Path: "path"})
31 31
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
32 32
 	assert.Check(t, is.ErrorContains(err, "value is empty"))
33 33
 
34
-	_, err = client.ContainerStatPath(context.Background(), "    ", "path")
34
+	_, err = client.ContainerStatPath(context.Background(), "    ", ContainerStatPathOptions{Path: "path"})
35 35
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
36 36
 	assert.Check(t, is.ErrorContains(err, "value is empty"))
37 37
 }
... ...
@@ -42,7 +42,7 @@ func TestContainerStatPathNotFoundError(t *testing.T) {
42 42
 	)
43 43
 	assert.NilError(t, err)
44 44
 
45
-	_, err = client.ContainerStatPath(context.Background(), "container_id", "path")
45
+	_, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path"})
46 46
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
47 47
 }
48 48
 
... ...
@@ -52,7 +52,7 @@ func TestContainerStatPathNoHeaderError(t *testing.T) {
52 52
 	)
53 53
 	assert.NilError(t, err)
54 54
 
55
-	_, err = client.ContainerStatPath(context.Background(), "container_id", "path/to/file")
55
+	_, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path/to/file"})
56 56
 	assert.Check(t, err != nil, "expected an error, got nothing")
57 57
 }
58 58
 
... ...
@@ -86,10 +86,10 @@ func TestContainerStatPath(t *testing.T) {
86 86
 		}),
87 87
 	)
88 88
 	assert.NilError(t, err)
89
-	stat, err := client.ContainerStatPath(context.Background(), "container_id", expectedPath)
89
+	res, err := client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: expectedPath})
90 90
 	assert.NilError(t, err)
91
-	assert.Check(t, is.Equal(stat.Name, "name"))
92
-	assert.Check(t, is.Equal(stat.Mode, os.FileMode(0o700)))
91
+	assert.Check(t, is.Equal(res.Stat.Name, "name"))
92
+	assert.Check(t, is.Equal(res.Stat.Mode, os.FileMode(0o700)))
93 93
 }
94 94
 
95 95
 func TestCopyToContainerError(t *testing.T) {
... ...
@@ -98,14 +98,23 @@ func TestCopyToContainerError(t *testing.T) {
98 98
 	)
99 99
 	assert.NilError(t, err)
100 100
 
101
-	err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
101
+	_, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
102
+		DestinationPath: "path/to/file",
103
+		Content:         bytes.NewReader([]byte("")),
104
+	})
102 105
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
103 106
 
104
-	err = client.CopyToContainer(context.Background(), "", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
107
+	_, err = client.CopyToContainer(context.Background(), "", CopyToContainerOptions{
108
+		DestinationPath: "path/to/file",
109
+		Content:         bytes.NewReader([]byte("")),
110
+	})
105 111
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
106 112
 	assert.Check(t, is.ErrorContains(err, "value is empty"))
107 113
 
108
-	err = client.CopyToContainer(context.Background(), "    ", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
114
+	_, err = client.CopyToContainer(context.Background(), "    ", CopyToContainerOptions{
115
+		DestinationPath: "path/to/file",
116
+		Content:         bytes.NewReader([]byte("")),
117
+	})
109 118
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
110 119
 	assert.Check(t, is.ErrorContains(err, "value is empty"))
111 120
 }
... ...
@@ -116,7 +125,10 @@ func TestCopyToContainerNotFoundError(t *testing.T) {
116 116
 	)
117 117
 	assert.NilError(t, err)
118 118
 
119
-	err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
119
+	_, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
120
+		DestinationPath: "path/to/file",
121
+		Content:         bytes.NewReader([]byte("")),
122
+	})
120 123
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
121 124
 }
122 125
 
... ...
@@ -128,7 +140,10 @@ func TestCopyToContainerEmptyResponse(t *testing.T) {
128 128
 	)
129 129
 	assert.NilError(t, err)
130 130
 
131
-	err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
131
+	_, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
132
+		DestinationPath: "path/to/file",
133
+		Content:         bytes.NewReader([]byte("")),
134
+	})
132 135
 	assert.NilError(t, err)
133 136
 }
134 137
 
... ...
@@ -168,7 +183,9 @@ func TestCopyToContainer(t *testing.T) {
168 168
 	)
169 169
 	assert.NilError(t, err)
170 170
 
171
-	err = client.CopyToContainer(context.Background(), "container_id", expectedPath, bytes.NewReader([]byte("content")), CopyToContainerOptions{
171
+	_, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
172
+		DestinationPath:           expectedPath,
173
+		Content:                   bytes.NewReader([]byte("content")),
172 174
 		AllowOverwriteDirWithFile: false,
173 175
 	})
174 176
 	assert.NilError(t, err)
... ...
@@ -180,14 +197,14 @@ func TestCopyFromContainerError(t *testing.T) {
180 180
 	)
181 181
 	assert.NilError(t, err)
182 182
 
183
-	_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
183
+	_, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
184 184
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
185 185
 
186
-	_, _, err = client.CopyFromContainer(context.Background(), "", "path/to/file")
186
+	_, err = client.CopyFromContainer(context.Background(), "", CopyFromContainerOptions{SourcePath: "path/to/file"})
187 187
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
188 188
 	assert.Check(t, is.ErrorContains(err, "value is empty"))
189 189
 
190
-	_, _, err = client.CopyFromContainer(context.Background(), "    ", "path/to/file")
190
+	_, err = client.CopyFromContainer(context.Background(), "    ", CopyFromContainerOptions{SourcePath: "path/to/file"})
191 191
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
192 192
 	assert.Check(t, is.ErrorContains(err, "value is empty"))
193 193
 }
... ...
@@ -198,7 +215,7 @@ func TestCopyFromContainerNotFoundError(t *testing.T) {
198 198
 	)
199 199
 	assert.NilError(t, err)
200 200
 
201
-	_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
201
+	_, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
202 202
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
203 203
 }
204 204
 
... ...
@@ -223,7 +240,7 @@ func TestCopyFromContainerEmptyResponse(t *testing.T) {
223 223
 	)
224 224
 	assert.NilError(t, err)
225 225
 
226
-	_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
226
+	_, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
227 227
 	assert.NilError(t, err)
228 228
 }
229 229
 
... ...
@@ -233,7 +250,7 @@ func TestCopyFromContainerNoHeaderError(t *testing.T) {
233 233
 	)
234 234
 	assert.NilError(t, err)
235 235
 
236
-	_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
236
+	_, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
237 237
 	assert.Check(t, err != nil, "expected an error, got nothing")
238 238
 }
239 239
 
... ...
@@ -268,13 +285,13 @@ func TestCopyFromContainer(t *testing.T) {
268 268
 		}),
269 269
 	)
270 270
 	assert.NilError(t, err)
271
-	r, stat, err := client.CopyFromContainer(context.Background(), "container_id", expectedPath)
271
+	res2, err := client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: expectedPath})
272 272
 	assert.NilError(t, err)
273
-	assert.Check(t, is.Equal(stat.Name, "name"))
274
-	assert.Check(t, is.Equal(stat.Mode, os.FileMode(0o700)))
273
+	assert.Check(t, is.Equal(res2.Stat.Name, "name"))
274
+	assert.Check(t, is.Equal(res2.Stat.Mode, os.FileMode(0o700)))
275 275
 
276
-	content, err := io.ReadAll(r)
276
+	content, err := io.ReadAll(res2.Content)
277 277
 	assert.NilError(t, err)
278 278
 	assert.Check(t, is.Equal(string(content), "content"))
279
-	assert.NilError(t, r.Close())
279
+	assert.NilError(t, res2.Content.Close())
280 280
 }
... ...
@@ -991,7 +991,7 @@ func (s *DockerAPISuite) TestPutContainerArchiveErrSymlinkInVolumeToReadOnlyRoot
991 991
 	apiClient, err := client.NewClientWithOpts(client.FromEnv)
992 992
 	assert.NilError(c, err)
993 993
 
994
-	err = apiClient.CopyToContainer(testutil.GetContext(c), cID, "/vol2/symlinkToAbsDir", nil, client.CopyToContainerOptions{})
994
+	_, err = apiClient.CopyToContainer(testutil.GetContext(c), cID, client.CopyToContainerOptions{DestinationPath: "/vol2/symlinkToAbsDir"})
995 995
 	assert.ErrorContains(c, err, "container rootfs is marked read-only")
996 996
 }
997 997
 
... ...
@@ -30,7 +30,7 @@ func TestCopyFromContainerPathDoesNotExist(t *testing.T) {
30 30
 	apiClient := testEnv.APIClient()
31 31
 	cid := container.Create(ctx, t, apiClient)
32 32
 
33
-	_, _, err := apiClient.CopyFromContainer(ctx, cid, "/dne")
33
+	_, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/dne"})
34 34
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
35 35
 	assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid))
36 36
 }
... ...
@@ -58,7 +58,7 @@ func TestCopyFromContainerPathIsNotDir(t *testing.T) {
58 58
 			"The filename, directory name, or volume label syntax is incorrect.", // ERROR_INVALID_NAME
59 59
 		}
60 60
 	}
61
-	_, _, err := apiClient.CopyFromContainer(ctx, cid, existingFile)
61
+	_, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: existingFile})
62 62
 	var found bool
63 63
 	for _, expErr := range expected {
64 64
 		if err != nil && strings.Contains(err.Error(), expErr) {
... ...
@@ -75,7 +75,7 @@ func TestCopyToContainerPathDoesNotExist(t *testing.T) {
75 75
 	apiClient := testEnv.APIClient()
76 76
 	cid := container.Create(ctx, t, apiClient)
77 77
 
78
-	err := apiClient.CopyToContainer(ctx, cid, "/dne", nil, client.CopyToContainerOptions{})
78
+	_, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: "/dne", Content: bytes.NewReader([]byte(""))})
79 79
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
80 80
 	assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid))
81 81
 }
... ...
@@ -88,23 +88,22 @@ func TestCopyEmptyFile(t *testing.T) {
88 88
 
89 89
 	// empty content
90 90
 	dstDir, _ := makeEmptyArchive(t)
91
-	err := apiClient.CopyToContainer(ctx, cid, dstDir, bytes.NewReader([]byte("")), client.CopyToContainerOptions{})
91
+	_, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: bytes.NewReader([]byte(""))})
92 92
 	assert.NilError(t, err)
93 93
 
94 94
 	// tar with empty file
95 95
 	dstDir, preparedArchive := makeEmptyArchive(t)
96
-	err = apiClient.CopyToContainer(ctx, cid, dstDir, preparedArchive, client.CopyToContainerOptions{})
96
+	_, err = apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive})
97 97
 	assert.NilError(t, err)
98 98
 
99 99
 	// tar with empty file archive mode
100 100
 	dstDir, preparedArchive = makeEmptyArchive(t)
101
-	err = apiClient.CopyToContainer(ctx, cid, dstDir, preparedArchive, client.CopyToContainerOptions{
102
-		CopyUIDGID: true,
103
-	})
101
+	_, err = apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive, CopyUIDGID: true})
104 102
 	assert.NilError(t, err)
105 103
 
106 104
 	// copy from empty file
107
-	rdr, _, err := apiClient.CopyFromContainer(ctx, cid, dstDir)
105
+	res, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: dstDir})
106
+	rdr := res.Content
108 107
 	assert.NilError(t, err)
109 108
 	defer rdr.Close()
110 109
 }
... ...
@@ -189,9 +188,7 @@ func TestCopyToContainerCopyUIDGID(t *testing.T) {
189 189
 
190 190
 			// tar with empty file
191 191
 			dstDir, preparedArchive := makeEmptyArchive(t)
192
-			err := apiClient.CopyToContainer(ctx, cID, dstDir, preparedArchive, client.CopyToContainerOptions{
193
-				CopyUIDGID: true,
194
-			})
192
+			_, err := apiClient.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive, CopyUIDGID: true})
195 193
 			assert.NilError(t, err)
196 194
 
197 195
 			res, err := container.Exec(ctx, apiClient, cID, []string{"stat", "-c", "%u:%g", "/empty-file.txt"})
... ...
@@ -264,7 +261,7 @@ func TestCopyToContainerPathIsNotDir(t *testing.T) {
264 264
 	if testEnv.DaemonInfo.OSType == "windows" {
265 265
 		path = "c:/windows/system32/drivers/etc/hosts/"
266 266
 	}
267
-	err := apiClient.CopyToContainer(ctx, cid, path, nil, client.CopyToContainerOptions{})
267
+	_, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: path})
268 268
 	assert.Check(t, is.ErrorContains(err, "not a directory"))
269 269
 }
270 270
 
... ...
@@ -327,7 +324,8 @@ func TestCopyFromContainer(t *testing.T) {
327 327
 		{"bar/notarget", map[string]string{"notarget": ""}},
328 328
 	} {
329 329
 		t.Run(x.src, func(t *testing.T) {
330
-			rdr, _, err := apiClient.CopyFromContainer(ctx, cid, x.src)
330
+			res, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: x.src})
331
+			rdr := res.Content
331 332
 			assert.NilError(t, err)
332 333
 			defer rdr.Close()
333 334
 
... ...
@@ -480,7 +480,7 @@ func TestContainerCopyLeaksMounts(t *testing.T) {
480 480
 
481 481
 	mountsBefore := getMounts()
482 482
 
483
-	_, _, err := apiClient.CopyFromContainer(ctx, cid, "/etc/passwd")
483
+	_, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/etc/passwd"})
484 484
 	assert.NilError(t, err)
485 485
 
486 486
 	mountsAfter := getMounts()
... ...
@@ -42,13 +42,14 @@ func TestNoOverlayfsWarningsAboutUndefinedBehaviors(t *testing.T) {
42 42
 		{name: "cp to container", operation: func(t *testing.T) error {
43 43
 			archiveReader, err := archive.Generate("new-file", "hello-world")
44 44
 			assert.NilError(t, err, "failed to create a temporary archive")
45
-			return apiClient.CopyToContainer(ctx, cID, "/", archiveReader, client.CopyToContainerOptions{})
45
+			_, err = apiClient.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: "/", Content: archiveReader})
46
+			return err
46 47
 		}},
47 48
 		{name: "cp from container", operation: func(*testing.T) error {
48
-			rc, _, err := apiClient.CopyFromContainer(ctx, cID, "/file")
49
+			res, err := apiClient.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/file"})
49 50
 			if err == nil {
50
-				defer rc.Close()
51
-				_, err = io.Copy(io.Discard, rc)
51
+				defer res.Content.Close()
52
+				_, err = io.Copy(io.Discard, res.Content)
52 53
 			}
53 54
 
54 55
 			return err
... ...
@@ -607,7 +607,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
607 607
 
608 608
 		// Wait until container creates a file in the volume.
609 609
 		poll.WaitOn(t, func(t poll.LogT) poll.Result {
610
-			stat, err := c.ContainerStatPath(ctx, cID, "/foo/test.txt")
610
+			res, err := c.ContainerStatPath(ctx, cID, client.ContainerStatPathOptions{Path: "/foo/test.txt"})
611 611
 			if err != nil {
612 612
 				if cerrdefs.IsNotFound(err) {
613 613
 					return poll.Continue("file doesn't yet exist")
... ...
@@ -615,8 +615,8 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
615 615
 				return poll.Error(err)
616 616
 			}
617 617
 
618
-			if int(stat.Size) != len(testContent)+1 {
619
-				return poll.Error(fmt.Errorf("unexpected test file size: %d", stat.Size))
618
+			if int(res.Stat.Size) != len(testContent)+1 {
619
+				return poll.Error(fmt.Errorf("unexpected test file size: %d", res.Stat.Size))
620 620
 			}
621 621
 
622 622
 			return poll.Success()
... ...
@@ -680,7 +680,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
680 680
 		defer c.ContainerRemove(ctx, cID, client.ContainerRemoveOptions{Force: true})
681 681
 
682 682
 		waitFn := func(t poll.LogT) poll.Result {
683
-			_, err := c.ContainerStatPath(ctx, cID, "/image/hello")
683
+			_, err := c.ContainerStatPath(ctx, cID, client.ContainerStatPathOptions{Path: "/image/hello"})
684 684
 			if err != nil {
685 685
 				if cerrdefs.IsNotFound(err) {
686 686
 					return poll.Continue("file doesn't yet exist")
... ...
@@ -415,12 +415,12 @@ func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
415 415
 	dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"})
416 416
 	assert.NilError(t, err)
417 417
 
418
-	err = c.CopyToContainer(ctx, cID, dstDir, preparedArchive, client.CopyToContainerOptions{})
418
+	_, err = c.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive})
419 419
 	assert.NilError(t, err)
420 420
 
421
-	rdr, _, err := c.CopyFromContainer(ctx, cID, "/test")
421
+	res, err := c.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/test"})
422 422
 	assert.NilError(t, err)
423
-	_, err = io.Copy(io.Discard, rdr)
423
+	_, err = io.Copy(io.Discard, res.Content)
424 424
 	assert.NilError(t, err)
425 425
 }
426 426
 
... ...
@@ -67,7 +67,7 @@ type ContainerAPIClient interface {
67 67
 	ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error)
68 68
 	ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error)
69 69
 	ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error)
70
-	ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error)
70
+	ContainerStatPath(ctx context.Context, container string, options ContainerStatPathOptions) (ContainerStatPathResult, error)
71 71
 	ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error)
72 72
 	ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error)
73 73
 	ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error)
... ...
@@ -75,8 +75,8 @@ type ContainerAPIClient interface {
75 75
 	ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error)
76 76
 	ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error)
77 77
 	ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult
78
-	CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error)
79
-	CopyToContainer(ctx context.Context, container, path string, content io.Reader, options CopyToContainerOptions) error
78
+	CopyFromContainer(ctx context.Context, container string, options CopyFromContainerOptions) (CopyFromContainerResult, error)
79
+	CopyToContainer(ctx context.Context, container string, options CopyToContainerOptions) (CopyToContainerResult, error)
80 80
 	ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error)
81 81
 }
82 82
 
... ...
@@ -14,41 +14,57 @@ import (
14 14
 	"github.com/moby/moby/api/types/container"
15 15
 )
16 16
 
17
+type ContainerStatPathOptions struct {
18
+	Path string
19
+}
20
+
21
+type ContainerStatPathResult struct {
22
+	Stat container.PathStat
23
+}
24
+
17 25
 // ContainerStatPath returns stat information about a path inside the container filesystem.
18
-func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) {
26
+func (cli *Client) ContainerStatPath(ctx context.Context, containerID string, options ContainerStatPathOptions) (ContainerStatPathResult, error) {
19 27
 	containerID, err := trimID("container", containerID)
20 28
 	if err != nil {
21
-		return container.PathStat{}, err
29
+		return ContainerStatPathResult{}, err
22 30
 	}
23 31
 
24 32
 	query := url.Values{}
25
-	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
33
+	query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
26 34
 
27 35
 	resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil)
28 36
 	defer ensureReaderClosed(resp)
29 37
 	if err != nil {
30
-		return container.PathStat{}, err
38
+		return ContainerStatPathResult{}, err
39
+	}
40
+	stat, err := getContainerPathStatFromHeader(resp.Header)
41
+	if err != nil {
42
+		return ContainerStatPathResult{}, err
31 43
 	}
32
-	return getContainerPathStatFromHeader(resp.Header)
44
+	return ContainerStatPathResult{Stat: stat}, nil
33 45
 }
34 46
 
35 47
 // CopyToContainerOptions holds information
36 48
 // about files to copy into a container
37 49
 type CopyToContainerOptions struct {
50
+	DestinationPath           string
51
+	Content                   io.Reader
38 52
 	AllowOverwriteDirWithFile bool
39 53
 	CopyUIDGID                bool
40 54
 }
41 55
 
56
+type CopyToContainerResult struct{}
57
+
42 58
 // CopyToContainer copies content into the container filesystem.
43 59
 // Note that `content` must be a Reader for a TAR archive
44
-func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options CopyToContainerOptions) error {
60
+func (cli *Client) CopyToContainer(ctx context.Context, containerID string, options CopyToContainerOptions) (CopyToContainerResult, error) {
45 61
 	containerID, err := trimID("container", containerID)
46 62
 	if err != nil {
47
-		return err
63
+		return CopyToContainerResult{}, err
48 64
 	}
49 65
 
50 66
 	query := url.Values{}
51
-	query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
67
+	query.Set("path", filepath.ToSlash(options.DestinationPath)) // Normalize the paths used in the API.
52 68
 	// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
53 69
 	if !options.AllowOverwriteDirWithFile {
54 70
 		query.Set("noOverwriteDirNonDir", "true")
... ...
@@ -58,29 +74,38 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str
58 58
 		query.Set("copyUIDGID", "true")
59 59
 	}
60 60
 
61
-	response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil)
61
+	response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, options.Content, nil)
62 62
 	defer ensureReaderClosed(response)
63 63
 	if err != nil {
64
-		return err
64
+		return CopyToContainerResult{}, err
65 65
 	}
66 66
 
67
-	return nil
67
+	return CopyToContainerResult{}, nil
68
+}
69
+
70
+type CopyFromContainerOptions struct {
71
+	SourcePath string
72
+}
73
+
74
+type CopyFromContainerResult struct {
75
+	Content io.ReadCloser
76
+	Stat    container.PathStat
68 77
 }
69 78
 
70 79
 // CopyFromContainer gets the content from the container and returns it as a Reader
71 80
 // for a TAR archive to manipulate it in the host. It's up to the caller to close the reader.
72
-func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
81
+func (cli *Client) CopyFromContainer(ctx context.Context, containerID string, options CopyFromContainerOptions) (CopyFromContainerResult, error) {
73 82
 	containerID, err := trimID("container", containerID)
74 83
 	if err != nil {
75
-		return nil, container.PathStat{}, err
84
+		return CopyFromContainerResult{}, err
76 85
 	}
77 86
 
78 87
 	query := make(url.Values, 1)
79
-	query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
88
+	query.Set("path", filepath.ToSlash(options.SourcePath)) // Normalize the paths used in the API.
80 89
 
81 90
 	resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil)
82 91
 	if err != nil {
83
-		return nil, container.PathStat{}, err
92
+		return CopyFromContainerResult{}, err
84 93
 	}
85 94
 
86 95
 	// In order to get the copy behavior right, we need to know information
... ...
@@ -91,9 +116,10 @@ func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath s
91 91
 	// can be when copying a file/dir from one location to another file/dir.
92 92
 	stat, err := getContainerPathStatFromHeader(resp.Header)
93 93
 	if err != nil {
94
-		return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
94
+		ensureReaderClosed(resp)
95
+		return CopyFromContainerResult{Stat: stat}, fmt.Errorf("unable to get resource stat from response: %s", err)
95 96
 	}
96
-	return resp.Body, stat, err
97
+	return CopyFromContainerResult{Content: resp.Body, Stat: stat}, nil
97 98
 }
98 99
 
99 100
 func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {