Browse code

Implement docker cp with standalone client lib.

Signed-off-by: David Calavera <david.calavera@gmail.com>

David Calavera authored on 2015/12/03 13:56:03
Showing 2 changed files
... ...
@@ -1,16 +1,13 @@
1 1
 package client
2 2
 
3 3
 import (
4
-	"encoding/base64"
5
-	"encoding/json"
6 4
 	"fmt"
7 5
 	"io"
8
-	"net/http"
9
-	"net/url"
10 6
 	"os"
11 7
 	"path/filepath"
12 8
 	"strings"
13 9
 
10
+	"github.com/docker/docker/api/client/lib"
14 11
 	"github.com/docker/docker/api/types"
15 12
 	Cli "github.com/docker/docker/cli"
16 13
 	"github.com/docker/docker/pkg/archive"
... ...
@@ -129,38 +126,7 @@ func splitCpArg(arg string) (container, path string) {
129 129
 }
130 130
 
131 131
 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
132
+	return cli.client.StatContainerPath(containerName, path)
164 133
 }
165 134
 
166 135
 func resolveLocalPath(localPath string) (absPath string, err error) {
... ...
@@ -200,39 +166,19 @@ func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, c
200 200
 
201 201
 	}
202 202
 
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)
203
+	content, stat, err := cli.client.CopyFromContainer(srcContainer, srcPath)
209 204
 	if err != nil {
210 205
 		return err
211 206
 	}
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
-	}
207
+	defer content.Close()
217 208
 
218 209
 	if dstPath == "-" {
219 210
 		// Send the response to STDOUT.
220
-		_, err = io.Copy(os.Stdout, response.body)
211
+		_, err = io.Copy(os.Stdout, content)
221 212
 
222 213
 		return err
223 214
 	}
224 215
 
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
-
236 216
 	// Prepare source copy info.
237 217
 	srcInfo := archive.CopyInfo{
238 218
 		Path:       srcPath,
... ...
@@ -241,10 +187,10 @@ func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, c
241 241
 		RebaseName: rebaseName,
242 242
 	}
243 243
 
244
-	preArchive := response.body
244
+	preArchive := content
245 245
 	if len(srcInfo.RebaseName) != 0 {
246 246
 		_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
247
-		preArchive = archive.RebaseArchiveEntries(response.body, srcBase, srcInfo.RebaseName)
247
+		preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
248 248
 	}
249 249
 	// See comments in the implementation of `archive.CopyTo` for exactly what
250 250
 	// 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
340 340
 		content = preparedArchive
341 341
 	}
342 342
 
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)
343
+	options := lib.CopyToContainerOptions{
344
+		ContainerID:               dstContainer,
345
+		Path:                      resolvedDstPath,
346
+		Content:                   content,
347
+		AllowOverwriteDirWithFile: false,
358 348
 	}
359 349
 
360
-	return nil
350
+	return cli.client.CopyToContainer(options)
361 351
 }
362 352
new file mode 100644
... ...
@@ -0,0 +1,103 @@
0
+package lib
1
+
2
+import (
3
+	"encoding/base64"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"net/http"
8
+	"net/url"
9
+	"path/filepath"
10
+	"strings"
11
+
12
+	"github.com/docker/docker/api/types"
13
+)
14
+
15
+// CopyToContainerOptions holds information
16
+// about files to copy into a container
17
+type CopyToContainerOptions struct {
18
+	ContainerID               string
19
+	Path                      string
20
+	Content                   io.Reader
21
+	AllowOverwriteDirWithFile bool
22
+}
23
+
24
+// StatContainerPath returns Stat information about a path inside the container filesystem.
25
+func (cli *Client) StatContainerPath(containerID, path string) (types.ContainerPathStat, error) {
26
+	query := make(url.Values, 1)
27
+	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
28
+
29
+	urlStr := fmt.Sprintf("/containers/%s/archive", containerID)
30
+	response, err := cli.HEAD(urlStr, query, nil)
31
+	if err != nil {
32
+		return types.ContainerPathStat{}, err
33
+	}
34
+	defer ensureReaderClosed(response)
35
+	return getContainerPathStatFromHeader(response.header)
36
+}
37
+
38
+// CopyToContainer copies content into the container filesystem.
39
+func (cli *Client) CopyToContainer(options CopyToContainerOptions) error {
40
+	var query url.Values
41
+	query.Set("path", filepath.ToSlash(options.Path)) // Normalize the paths used in the API.
42
+	// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
43
+	if !options.AllowOverwriteDirWithFile {
44
+		query.Set("noOverwriteDirNonDir", "true")
45
+	}
46
+
47
+	path := fmt.Sprintf("/containers/%s/archive", options.ContainerID)
48
+
49
+	response, err := cli.PUT(path, query, options.Content, nil)
50
+	if err != nil {
51
+		return err
52
+	}
53
+
54
+	if response.statusCode != http.StatusOK {
55
+		return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
56
+	}
57
+
58
+	return nil
59
+}
60
+
61
+// CopyFromContainer get the content from the container and return it as a Reader
62
+// to manipulate it in the host. It's up to the caller to close the reader.
63
+func (cli *Client) CopyFromContainer(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
64
+	query := make(url.Values, 1)
65
+	query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
66
+
67
+	apiPath := fmt.Sprintf("/containers/%s/archive", containerID)
68
+	response, err := cli.GET(apiPath, query, nil)
69
+	if err != nil {
70
+		return nil, types.ContainerPathStat{}, err
71
+	}
72
+
73
+	if response.statusCode != http.StatusOK {
74
+		return nil, types.ContainerPathStat{}, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
75
+	}
76
+
77
+	// In order to get the copy behavior right, we need to know information
78
+	// about both the source and the destination. The response headers include
79
+	// stat info about the source that we can use in deciding exactly how to
80
+	// copy it locally. Along with the stat info about the local destination,
81
+	// we have everything we need to handle the multiple possibilities there
82
+	// can be when copying a file/dir from one location to another file/dir.
83
+	stat, err := getContainerPathStatFromHeader(response.header)
84
+	if err != nil {
85
+		return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
86
+	}
87
+	return response.body, stat, err
88
+}
89
+
90
+func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
91
+	var stat types.ContainerPathStat
92
+
93
+	encodedStat := header.Get("X-Docker-Container-Path-Stat")
94
+	statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
95
+
96
+	err := json.NewDecoder(statDecoder).Decode(&stat)
97
+	if err != nil {
98
+		err = fmt.Errorf("unable to decode container path stat header: %s", err)
99
+	}
100
+
101
+	return stat, err
102
+}