Skip to content

Commit 1b2b91b

Browse files
committed
Implement docker cp with standalone client lib.
Signed-off-by: David Calavera <david.calavera@gmail.com>
1 parent 8c9ad7b commit 1b2b91b

File tree

2 files changed

+116
-77
lines changed

2 files changed

+116
-77
lines changed

api/client/cp.go

Lines changed: 13 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
package client
22

33
import (
4-
"encoding/base64"
5-
"encoding/json"
64
"fmt"
75
"io"
8-
"net/http"
9-
"net/url"
106
"os"
117
"path/filepath"
128
"strings"
139

10+
"github.com/docker/docker/api/client/lib"
1411
"github.com/docker/docker/api/types"
1512
Cli "github.com/docker/docker/cli"
1613
"github.com/docker/docker/pkg/archive"
@@ -129,38 +126,7 @@ func splitCpArg(arg string) (container, path string) {
129126
}
130127

131128
func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) {
132-
var stat types.ContainerPathStat
133-
134-
query := make(url.Values, 1)
135-
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
136-
137-
urlStr := fmt.Sprintf("/containers/%s/archive?%s", containerName, query.Encode())
138-
139-
response, err := cli.call("HEAD", urlStr, nil, nil)
140-
if err != nil {
141-
return stat, err
142-
}
143-
defer response.body.Close()
144-
145-
if response.statusCode != http.StatusOK {
146-
return stat, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
147-
}
148-
149-
return getContainerPathStatFromHeader(response.header)
150-
}
151-
152-
func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
153-
var stat types.ContainerPathStat
154-
155-
encodedStat := header.Get("X-Docker-Container-Path-Stat")
156-
statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
157-
158-
err := json.NewDecoder(statDecoder).Decode(&stat)
159-
if err != nil {
160-
err = fmt.Errorf("unable to decode container path stat header: %s", err)
161-
}
162-
163-
return stat, err
129+
return cli.client.StatContainerPath(containerName, path)
164130
}
165131

166132
func resolveLocalPath(localPath string) (absPath string, err error) {
@@ -200,39 +166,19 @@ func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, c
200166

201167
}
202168

203-
query := make(url.Values, 1)
204-
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
205-
206-
urlStr := fmt.Sprintf("/containers/%s/archive?%s", srcContainer, query.Encode())
207-
208-
response, err := cli.call("GET", urlStr, nil, nil)
169+
content, stat, err := cli.client.CopyFromContainer(srcContainer, srcPath)
209170
if err != nil {
210171
return err
211172
}
212-
defer response.body.Close()
213-
214-
if response.statusCode != http.StatusOK {
215-
return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
216-
}
173+
defer content.Close()
217174

218175
if dstPath == "-" {
219176
// Send the response to STDOUT.
220-
_, err = io.Copy(os.Stdout, response.body)
177+
_, err = io.Copy(os.Stdout, content)
221178

222179
return err
223180
}
224181

225-
// In order to get the copy behavior right, we need to know information
226-
// about both the source and the destination. The response headers include
227-
// stat info about the source that we can use in deciding exactly how to
228-
// copy it locally. Along with the stat info about the local destination,
229-
// we have everything we need to handle the multiple possibilities there
230-
// can be when copying a file/dir from one location to another file/dir.
231-
stat, err := getContainerPathStatFromHeader(response.header)
232-
if err != nil {
233-
return fmt.Errorf("unable to get resource stat from response: %s", err)
234-
}
235-
236182
// Prepare source copy info.
237183
srcInfo := archive.CopyInfo{
238184
Path: srcPath,
@@ -241,10 +187,10 @@ func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, c
241187
RebaseName: rebaseName,
242188
}
243189

244-
preArchive := response.body
190+
preArchive := content
245191
if len(srcInfo.RebaseName) != 0 {
246192
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
247-
preArchive = archive.RebaseArchiveEntries(response.body, srcBase, srcInfo.RebaseName)
193+
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
248194
}
249195
// See comments in the implementation of `archive.CopyTo` for exactly what
250196
// goes into deciding how and whether the source archive needs to be
@@ -340,22 +286,12 @@ func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string, cpP
340286
content = preparedArchive
341287
}
342288

343-
query := make(url.Values, 2)
344-
query.Set("path", filepath.ToSlash(resolvedDstPath)) // Normalize the paths used in the API.
345-
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
346-
query.Set("noOverwriteDirNonDir", "true")
347-
348-
urlStr := fmt.Sprintf("/containers/%s/archive?%s", dstContainer, query.Encode())
349-
350-
response, err := cli.stream("PUT", urlStr, &streamOpts{in: content})
351-
if err != nil {
352-
return err
353-
}
354-
defer response.body.Close()
355-
356-
if response.statusCode != http.StatusOK {
357-
return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
289+
options := lib.CopyToContainerOptions{
290+
ContainerID: dstContainer,
291+
Path: resolvedDstPath,
292+
Content: content,
293+
AllowOverwriteDirWithFile: false,
358294
}
359295

360-
return nil
296+
return cli.client.CopyToContainer(options)
361297
}

api/client/lib/copy.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package lib
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"path/filepath"
11+
"strings"
12+
13+
"github.com/docker/docker/api/types"
14+
)
15+
16+
// CopyToContainerOptions holds information
17+
// about files to copy into a container
18+
type CopyToContainerOptions struct {
19+
ContainerID string
20+
Path string
21+
Content io.Reader
22+
AllowOverwriteDirWithFile bool
23+
}
24+
25+
// StatContainerPath returns Stat information about a path inside the container filesystem.
26+
func (cli *Client) StatContainerPath(containerID, path string) (types.ContainerPathStat, error) {
27+
query := make(url.Values, 1)
28+
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
29+
30+
urlStr := fmt.Sprintf("/containers/%s/archive", containerID)
31+
response, err := cli.HEAD(urlStr, query, nil)
32+
if err != nil {
33+
return types.ContainerPathStat{}, err
34+
}
35+
defer ensureReaderClosed(response)
36+
return getContainerPathStatFromHeader(response.header)
37+
}
38+
39+
// CopyToContainer copies content into the container filesystem.
40+
func (cli *Client) CopyToContainer(options CopyToContainerOptions) error {
41+
var query url.Values
42+
query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
43+
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
44+
if !options.AllowOverwriteDirWithFile {
45+
query.Set("noOverwriteDirNonDir", "true")
46+
}
47+
48+
path := fmt.Sprintf("/containers/%s/archive", options.ContainerID)
49+
50+
response, err := cli.PUT(path, query, options.Content, nil)
51+
if err != nil {
52+
return err
53+
}
54+
55+
if response.statusCode != http.StatusOK {
56+
return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
57+
}
58+
59+
return nil
60+
}
61+
62+
// CopyFromContainer get the content from the container and return it as a Reader
63+
// to manipulate it in the host. It's up to the caller to close the reader.
64+
func (cli *Client) CopyFromContainer(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
65+
query := make(url.Values, 1)
66+
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
67+
68+
apiPath := fmt.Sprintf("/containers/%s/archive", containerID)
69+
response, err := cli.GET(apiPath, query, nil)
70+
if err != nil {
71+
return nil, types.ContainerPathStat{}, err
72+
}
73+
74+
if response.statusCode != http.StatusOK {
75+
return nil, types.ContainerPathStat{}, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
76+
}
77+
78+
// In order to get the copy behavior right, we need to know information
79+
// about both the source and the destination. The response headers include
80+
// stat info about the source that we can use in deciding exactly how to
81+
// copy it locally. Along with the stat info about the local destination,
82+
// we have everything we need to handle the multiple possibilities there
83+
// can be when copying a file/dir from one location to another file/dir.
84+
stat, err := getContainerPathStatFromHeader(response.header)
85+
if err != nil {
86+
return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
87+
}
88+
return response.body, stat, err
89+
}
90+
91+
func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
92+
var stat types.ContainerPathStat
93+
94+
encodedStat := header.Get("X-Docker-Container-Path-Stat")
95+
statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
96+
97+
err := json.NewDecoder(statDecoder).Decode(&stat)
98+
if err != nil {
99+
err = fmt.Errorf("unable to decode container path stat header: %s", err)
100+
}
101+
102+
return stat, err
103+
}

0 commit comments

Comments
 (0)