Signed-off-by: David Calavera <david.calavera@gmail.com>
| ... | ... |
@@ -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 |
+} |