client/container_copy: Wrap options and result struct

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2025-10-29 13:05:23 +01:00
parent c755b9635d
commit 1cc2ab16ce
11 changed files with 160 additions and 92 deletions

View File

@@ -67,7 +67,7 @@ type ContainerAPIClient interface {
ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error)
ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error)
ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error)
ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error)
ContainerStatPath(ctx context.Context, container string, options ContainerStatPathOptions) (ContainerStatPathResult, error)
ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error)
ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error)
ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error)
@@ -75,8 +75,8 @@ type ContainerAPIClient interface {
ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error)
ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error)
ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult
CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error)
CopyToContainer(ctx context.Context, container, path string, content io.Reader, options CopyToContainerOptions) error
CopyFromContainer(ctx context.Context, container string, options CopyFromContainerOptions) (CopyFromContainerResult, error)
CopyToContainer(ctx context.Context, container string, options CopyToContainerOptions) (CopyToContainerResult, error)
ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error)
}

View File

@@ -14,41 +14,57 @@ import (
"github.com/moby/moby/api/types/container"
)
type ContainerStatPathOptions struct {
Path string
}
type ContainerStatPathResult struct {
Stat container.PathStat
}
// ContainerStatPath returns stat information about a path inside the container filesystem.
func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) {
func (cli *Client) ContainerStatPath(ctx context.Context, containerID string, options ContainerStatPathOptions) (ContainerStatPathResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return container.PathStat{}, err
return ContainerStatPathResult{}, err
}
query := url.Values{}
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return container.PathStat{}, err
return ContainerStatPathResult{}, err
}
return getContainerPathStatFromHeader(resp.Header)
stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil {
return ContainerStatPathResult{}, err
}
return ContainerStatPathResult{Stat: stat}, nil
}
// CopyToContainerOptions holds information
// about files to copy into a container
type CopyToContainerOptions struct {
DestinationPath string
Content io.Reader
AllowOverwriteDirWithFile bool
CopyUIDGID bool
}
type CopyToContainerResult struct{}
// CopyToContainer copies content into the container filesystem.
// Note that `content` must be a Reader for a TAR archive
func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options CopyToContainerOptions) error {
func (cli *Client) CopyToContainer(ctx context.Context, containerID string, options CopyToContainerOptions) (CopyToContainerResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return err
return CopyToContainerResult{}, err
}
query := url.Values{}
query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
query.Set("path", filepath.ToSlash(options.DestinationPath)) // Normalize the paths used in the API.
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
if !options.AllowOverwriteDirWithFile {
query.Set("noOverwriteDirNonDir", "true")
@@ -58,29 +74,38 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str
query.Set("copyUIDGID", "true")
}
response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil)
response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, options.Content, nil)
defer ensureReaderClosed(response)
if err != nil {
return err
return CopyToContainerResult{}, err
}
return nil
return CopyToContainerResult{}, nil
}
type CopyFromContainerOptions struct {
SourcePath string
}
type CopyFromContainerResult struct {
Content io.ReadCloser
Stat container.PathStat
}
// CopyFromContainer gets the content from the container and returns it as a Reader
// for a TAR archive to manipulate it in the host. It's up to the caller to close the reader.
func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
func (cli *Client) CopyFromContainer(ctx context.Context, containerID string, options CopyFromContainerOptions) (CopyFromContainerResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return nil, container.PathStat{}, err
return CopyFromContainerResult{}, err
}
query := make(url.Values, 1)
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
query.Set("path", filepath.ToSlash(options.SourcePath)) // Normalize the paths used in the API.
resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil)
if err != nil {
return nil, container.PathStat{}, err
return CopyFromContainerResult{}, err
}
// 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
// can be when copying a file/dir from one location to another file/dir.
stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil {
return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
ensureReaderClosed(resp)
return CopyFromContainerResult{Stat: stat}, fmt.Errorf("unable to get resource stat from response: %s", err)
}
return resp.Body, stat, err
return CopyFromContainerResult{Content: resp.Body, Stat: stat}, nil
}
func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {

View File

@@ -24,14 +24,14 @@ func TestContainerStatPathError(t *testing.T) {
)
assert.NilError(t, err)
_, err = client.ContainerStatPath(context.Background(), "container_id", "path")
_, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, err = client.ContainerStatPath(context.Background(), "", "path")
_, err = client.ContainerStatPath(context.Background(), "", ContainerStatPathOptions{Path: "path"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, err = client.ContainerStatPath(context.Background(), " ", "path")
_, err = client.ContainerStatPath(context.Background(), " ", ContainerStatPathOptions{Path: "path"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -42,7 +42,7 @@ func TestContainerStatPathNotFoundError(t *testing.T) {
)
assert.NilError(t, err)
_, err = client.ContainerStatPath(context.Background(), "container_id", "path")
_, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
}
@@ -52,7 +52,7 @@ func TestContainerStatPathNoHeaderError(t *testing.T) {
)
assert.NilError(t, err)
_, err = client.ContainerStatPath(context.Background(), "container_id", "path/to/file")
_, err = client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: "path/to/file"})
assert.Check(t, err != nil, "expected an error, got nothing")
}
@@ -86,10 +86,10 @@ func TestContainerStatPath(t *testing.T) {
}),
)
assert.NilError(t, err)
stat, err := client.ContainerStatPath(context.Background(), "container_id", expectedPath)
res, err := client.ContainerStatPath(context.Background(), "container_id", ContainerStatPathOptions{Path: expectedPath})
assert.NilError(t, err)
assert.Check(t, is.Equal(stat.Name, "name"))
assert.Check(t, is.Equal(stat.Mode, os.FileMode(0o700)))
assert.Check(t, is.Equal(res.Stat.Name, "name"))
assert.Check(t, is.Equal(res.Stat.Mode, os.FileMode(0o700)))
}
func TestCopyToContainerError(t *testing.T) {
@@ -98,14 +98,23 @@ func TestCopyToContainerError(t *testing.T) {
)
assert.NilError(t, err)
err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
_, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.CopyToContainer(context.Background(), "", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
_, err = client.CopyToContainer(context.Background(), "", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.CopyToContainer(context.Background(), " ", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
_, err = client.CopyToContainer(context.Background(), " ", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -116,7 +125,10 @@ func TestCopyToContainerNotFoundError(t *testing.T) {
)
assert.NilError(t, err)
err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
_, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
}
@@ -128,7 +140,10 @@ func TestCopyToContainerEmptyResponse(t *testing.T) {
)
assert.NilError(t, err)
err = client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), CopyToContainerOptions{})
_, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
DestinationPath: "path/to/file",
Content: bytes.NewReader([]byte("")),
})
assert.NilError(t, err)
}
@@ -168,7 +183,9 @@ func TestCopyToContainer(t *testing.T) {
)
assert.NilError(t, err)
err = client.CopyToContainer(context.Background(), "container_id", expectedPath, bytes.NewReader([]byte("content")), CopyToContainerOptions{
_, err = client.CopyToContainer(context.Background(), "container_id", CopyToContainerOptions{
DestinationPath: expectedPath,
Content: bytes.NewReader([]byte("content")),
AllowOverwriteDirWithFile: false,
})
assert.NilError(t, err)
@@ -180,14 +197,14 @@ func TestCopyFromContainerError(t *testing.T) {
)
assert.NilError(t, err)
_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
_, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, _, err = client.CopyFromContainer(context.Background(), "", "path/to/file")
_, err = client.CopyFromContainer(context.Background(), "", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, _, err = client.CopyFromContainer(context.Background(), " ", "path/to/file")
_, err = client.CopyFromContainer(context.Background(), " ", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -198,7 +215,7 @@ func TestCopyFromContainerNotFoundError(t *testing.T) {
)
assert.NilError(t, err)
_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
_, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
}
@@ -223,7 +240,7 @@ func TestCopyFromContainerEmptyResponse(t *testing.T) {
)
assert.NilError(t, err)
_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
_, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.NilError(t, err)
}
@@ -233,7 +250,7 @@ func TestCopyFromContainerNoHeaderError(t *testing.T) {
)
assert.NilError(t, err)
_, _, err = client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
_, err = client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: "path/to/file"})
assert.Check(t, err != nil, "expected an error, got nothing")
}
@@ -268,13 +285,13 @@ func TestCopyFromContainer(t *testing.T) {
}),
)
assert.NilError(t, err)
r, stat, err := client.CopyFromContainer(context.Background(), "container_id", expectedPath)
res2, err := client.CopyFromContainer(context.Background(), "container_id", CopyFromContainerOptions{SourcePath: expectedPath})
assert.NilError(t, err)
assert.Check(t, is.Equal(stat.Name, "name"))
assert.Check(t, is.Equal(stat.Mode, os.FileMode(0o700)))
assert.Check(t, is.Equal(res2.Stat.Name, "name"))
assert.Check(t, is.Equal(res2.Stat.Mode, os.FileMode(0o700)))
content, err := io.ReadAll(r)
content, err := io.ReadAll(res2.Content)
assert.NilError(t, err)
assert.Check(t, is.Equal(string(content), "content"))
assert.NilError(t, r.Close())
assert.NilError(t, res2.Content.Close())
}

View File

@@ -991,7 +991,7 @@ func (s *DockerAPISuite) TestPutContainerArchiveErrSymlinkInVolumeToReadOnlyRoot
apiClient, err := client.NewClientWithOpts(client.FromEnv)
assert.NilError(c, err)
err = apiClient.CopyToContainer(testutil.GetContext(c), cID, "/vol2/symlinkToAbsDir", nil, client.CopyToContainerOptions{})
_, err = apiClient.CopyToContainer(testutil.GetContext(c), cID, client.CopyToContainerOptions{DestinationPath: "/vol2/symlinkToAbsDir"})
assert.ErrorContains(c, err, "container rootfs is marked read-only")
}

View File

@@ -30,7 +30,7 @@ func TestCopyFromContainerPathDoesNotExist(t *testing.T) {
apiClient := testEnv.APIClient()
cid := container.Create(ctx, t, apiClient)
_, _, err := apiClient.CopyFromContainer(ctx, cid, "/dne")
_, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/dne"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid))
}
@@ -58,7 +58,7 @@ func TestCopyFromContainerPathIsNotDir(t *testing.T) {
"The filename, directory name, or volume label syntax is incorrect.", // ERROR_INVALID_NAME
}
}
_, _, err := apiClient.CopyFromContainer(ctx, cid, existingFile)
_, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: existingFile})
var found bool
for _, expErr := range expected {
if err != nil && strings.Contains(err.Error(), expErr) {
@@ -75,7 +75,7 @@ func TestCopyToContainerPathDoesNotExist(t *testing.T) {
apiClient := testEnv.APIClient()
cid := container.Create(ctx, t, apiClient)
err := apiClient.CopyToContainer(ctx, cid, "/dne", nil, client.CopyToContainerOptions{})
_, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: "/dne", Content: bytes.NewReader([]byte(""))})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
assert.Check(t, is.ErrorContains(err, "Could not find the file /dne in container "+cid))
}
@@ -88,23 +88,22 @@ func TestCopyEmptyFile(t *testing.T) {
// empty content
dstDir, _ := makeEmptyArchive(t)
err := apiClient.CopyToContainer(ctx, cid, dstDir, bytes.NewReader([]byte("")), client.CopyToContainerOptions{})
_, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: bytes.NewReader([]byte(""))})
assert.NilError(t, err)
// tar with empty file
dstDir, preparedArchive := makeEmptyArchive(t)
err = apiClient.CopyToContainer(ctx, cid, dstDir, preparedArchive, client.CopyToContainerOptions{})
_, err = apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive})
assert.NilError(t, err)
// tar with empty file archive mode
dstDir, preparedArchive = makeEmptyArchive(t)
err = apiClient.CopyToContainer(ctx, cid, dstDir, preparedArchive, client.CopyToContainerOptions{
CopyUIDGID: true,
})
_, err = apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive, CopyUIDGID: true})
assert.NilError(t, err)
// copy from empty file
rdr, _, err := apiClient.CopyFromContainer(ctx, cid, dstDir)
res, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: dstDir})
rdr := res.Content
assert.NilError(t, err)
defer rdr.Close()
}
@@ -189,9 +188,7 @@ func TestCopyToContainerCopyUIDGID(t *testing.T) {
// tar with empty file
dstDir, preparedArchive := makeEmptyArchive(t)
err := apiClient.CopyToContainer(ctx, cID, dstDir, preparedArchive, client.CopyToContainerOptions{
CopyUIDGID: true,
})
_, err := apiClient.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive, CopyUIDGID: true})
assert.NilError(t, err)
res, err := container.Exec(ctx, apiClient, cID, []string{"stat", "-c", "%u:%g", "/empty-file.txt"})
@@ -264,7 +261,7 @@ func TestCopyToContainerPathIsNotDir(t *testing.T) {
if testEnv.DaemonInfo.OSType == "windows" {
path = "c:/windows/system32/drivers/etc/hosts/"
}
err := apiClient.CopyToContainer(ctx, cid, path, nil, client.CopyToContainerOptions{})
_, err := apiClient.CopyToContainer(ctx, cid, client.CopyToContainerOptions{DestinationPath: path})
assert.Check(t, is.ErrorContains(err, "not a directory"))
}
@@ -327,7 +324,8 @@ func TestCopyFromContainer(t *testing.T) {
{"bar/notarget", map[string]string{"notarget": ""}},
} {
t.Run(x.src, func(t *testing.T) {
rdr, _, err := apiClient.CopyFromContainer(ctx, cid, x.src)
res, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: x.src})
rdr := res.Content
assert.NilError(t, err)
defer rdr.Close()

View File

@@ -480,7 +480,7 @@ func TestContainerCopyLeaksMounts(t *testing.T) {
mountsBefore := getMounts()
_, _, err := apiClient.CopyFromContainer(ctx, cid, "/etc/passwd")
_, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/etc/passwd"})
assert.NilError(t, err)
mountsAfter := getMounts()

View File

@@ -42,13 +42,14 @@ func TestNoOverlayfsWarningsAboutUndefinedBehaviors(t *testing.T) {
{name: "cp to container", operation: func(t *testing.T) error {
archiveReader, err := archive.Generate("new-file", "hello-world")
assert.NilError(t, err, "failed to create a temporary archive")
return apiClient.CopyToContainer(ctx, cID, "/", archiveReader, client.CopyToContainerOptions{})
_, err = apiClient.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: "/", Content: archiveReader})
return err
}},
{name: "cp from container", operation: func(*testing.T) error {
rc, _, err := apiClient.CopyFromContainer(ctx, cID, "/file")
res, err := apiClient.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/file"})
if err == nil {
defer rc.Close()
_, err = io.Copy(io.Discard, rc)
defer res.Content.Close()
_, err = io.Copy(io.Discard, res.Content)
}
return err

View File

@@ -607,7 +607,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
// Wait until container creates a file in the volume.
poll.WaitOn(t, func(t poll.LogT) poll.Result {
stat, err := c.ContainerStatPath(ctx, cID, "/foo/test.txt")
res, err := c.ContainerStatPath(ctx, cID, client.ContainerStatPathOptions{Path: "/foo/test.txt"})
if err != nil {
if cerrdefs.IsNotFound(err) {
return poll.Continue("file doesn't yet exist")
@@ -615,8 +615,8 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
return poll.Error(err)
}
if int(stat.Size) != len(testContent)+1 {
return poll.Error(fmt.Errorf("unexpected test file size: %d", stat.Size))
if int(res.Stat.Size) != len(testContent)+1 {
return poll.Error(fmt.Errorf("unexpected test file size: %d", res.Stat.Size))
}
return poll.Success()
@@ -680,7 +680,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
defer c.ContainerRemove(ctx, cID, client.ContainerRemoveOptions{Force: true})
waitFn := func(t poll.LogT) poll.Result {
_, err := c.ContainerStatPath(ctx, cID, "/image/hello")
_, err := c.ContainerStatPath(ctx, cID, client.ContainerStatPathOptions{Path: "/image/hello"})
if err != nil {
if cerrdefs.IsNotFound(err) {
return poll.Continue("file doesn't yet exist")

View File

@@ -415,12 +415,12 @@ func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"})
assert.NilError(t, err)
err = c.CopyToContainer(ctx, cID, dstDir, preparedArchive, client.CopyToContainerOptions{})
_, err = c.CopyToContainer(ctx, cID, client.CopyToContainerOptions{DestinationPath: dstDir, Content: preparedArchive})
assert.NilError(t, err)
rdr, _, err := c.CopyFromContainer(ctx, cID, "/test")
res, err := c.CopyFromContainer(ctx, cID, client.CopyFromContainerOptions{SourcePath: "/test"})
assert.NilError(t, err)
_, err = io.Copy(io.Discard, rdr)
_, err = io.Copy(io.Discard, res.Content)
assert.NilError(t, err)
}

View File

@@ -67,7 +67,7 @@ type ContainerAPIClient interface {
ContainerRename(ctx context.Context, container string, options ContainerRenameOptions) (ContainerRenameResult, error)
ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) (ContainerResizeResult, error)
ContainerRestart(ctx context.Context, container string, options ContainerRestartOptions) (ContainerRestartResult, error)
ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error)
ContainerStatPath(ctx context.Context, container string, options ContainerStatPathOptions) (ContainerStatPathResult, error)
ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error)
ContainerStart(ctx context.Context, container string, options ContainerStartOptions) (ContainerStartResult, error)
ContainerStop(ctx context.Context, container string, options ContainerStopOptions) (ContainerStopResult, error)
@@ -75,8 +75,8 @@ type ContainerAPIClient interface {
ContainerUnpause(ctx context.Context, container string, options ContainerUnpauseOptions) (ContainerUnpauseResult, error)
ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error)
ContainerWait(ctx context.Context, container string, options ContainerWaitOptions) ContainerWaitResult
CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error)
CopyToContainer(ctx context.Context, container, path string, content io.Reader, options CopyToContainerOptions) error
CopyFromContainer(ctx context.Context, container string, options CopyFromContainerOptions) (CopyFromContainerResult, error)
CopyToContainer(ctx context.Context, container string, options CopyToContainerOptions) (CopyToContainerResult, error)
ContainersPrune(ctx context.Context, opts ContainerPruneOptions) (ContainerPruneResult, error)
}

View File

@@ -14,41 +14,57 @@ import (
"github.com/moby/moby/api/types/container"
)
type ContainerStatPathOptions struct {
Path string
}
type ContainerStatPathResult struct {
Stat container.PathStat
}
// ContainerStatPath returns stat information about a path inside the container filesystem.
func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) {
func (cli *Client) ContainerStatPath(ctx context.Context, containerID string, options ContainerStatPathOptions) (ContainerStatPathResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return container.PathStat{}, err
return ContainerStatPathResult{}, err
}
query := url.Values{}
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return container.PathStat{}, err
return ContainerStatPathResult{}, err
}
return getContainerPathStatFromHeader(resp.Header)
stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil {
return ContainerStatPathResult{}, err
}
return ContainerStatPathResult{Stat: stat}, nil
}
// CopyToContainerOptions holds information
// about files to copy into a container
type CopyToContainerOptions struct {
DestinationPath string
Content io.Reader
AllowOverwriteDirWithFile bool
CopyUIDGID bool
}
type CopyToContainerResult struct{}
// CopyToContainer copies content into the container filesystem.
// Note that `content` must be a Reader for a TAR archive
func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options CopyToContainerOptions) error {
func (cli *Client) CopyToContainer(ctx context.Context, containerID string, options CopyToContainerOptions) (CopyToContainerResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return err
return CopyToContainerResult{}, err
}
query := url.Values{}
query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
query.Set("path", filepath.ToSlash(options.DestinationPath)) // Normalize the paths used in the API.
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
if !options.AllowOverwriteDirWithFile {
query.Set("noOverwriteDirNonDir", "true")
@@ -58,29 +74,38 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str
query.Set("copyUIDGID", "true")
}
response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil)
response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, options.Content, nil)
defer ensureReaderClosed(response)
if err != nil {
return err
return CopyToContainerResult{}, err
}
return nil
return CopyToContainerResult{}, nil
}
type CopyFromContainerOptions struct {
SourcePath string
}
type CopyFromContainerResult struct {
Content io.ReadCloser
Stat container.PathStat
}
// CopyFromContainer gets the content from the container and returns it as a Reader
// for a TAR archive to manipulate it in the host. It's up to the caller to close the reader.
func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
func (cli *Client) CopyFromContainer(ctx context.Context, containerID string, options CopyFromContainerOptions) (CopyFromContainerResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return nil, container.PathStat{}, err
return CopyFromContainerResult{}, err
}
query := make(url.Values, 1)
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
query.Set("path", filepath.ToSlash(options.SourcePath)) // Normalize the paths used in the API.
resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil)
if err != nil {
return nil, container.PathStat{}, err
return CopyFromContainerResult{}, err
}
// 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
// can be when copying a file/dir from one location to another file/dir.
stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil {
return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
ensureReaderClosed(resp)
return CopyFromContainerResult{Stat: stat}, fmt.Errorf("unable to get resource stat from response: %s", err)
}
return resp.Body, stat, err
return CopyFromContainerResult{Content: resp.Body, Stat: stat}, nil
}
func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {