The cp command and copy api endpoint allows users
to copy files and or folders from a containers filesystem.
Closes #382
| ... | ... |
@@ -871,6 +871,35 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ |
| 871 | 871 |
return nil |
| 872 | 872 |
} |
| 873 | 873 |
|
| 874 |
+func postContainersCopy(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
| 875 |
+ if vars == nil {
|
|
| 876 |
+ return fmt.Errorf("Missing parameter")
|
|
| 877 |
+ } |
|
| 878 |
+ name := vars["name"] |
|
| 879 |
+ |
|
| 880 |
+ copyData := &APICopy{}
|
|
| 881 |
+ if r.Header.Get("Content-Type") == "application/json" {
|
|
| 882 |
+ if err := json.NewDecoder(r.Body).Decode(copyData); err != nil {
|
|
| 883 |
+ return err |
|
| 884 |
+ } |
|
| 885 |
+ } else {
|
|
| 886 |
+ return fmt.Errorf("Content-Type not supported: %s", r.Header.Get("Content-Type"))
|
|
| 887 |
+ } |
|
| 888 |
+ |
|
| 889 |
+ if copyData.Resource == "" {
|
|
| 890 |
+ return fmt.Errorf("Resource cannot be empty")
|
|
| 891 |
+ } |
|
| 892 |
+ if copyData.Resource[0] == '/' {
|
|
| 893 |
+ return fmt.Errorf("Resource cannot contain a leading /")
|
|
| 894 |
+ } |
|
| 895 |
+ |
|
| 896 |
+ if err := srv.ContainerCopy(name, copyData.Resource, w); err != nil {
|
|
| 897 |
+ utils.Debugf("%s", err)
|
|
| 898 |
+ return err |
|
| 899 |
+ } |
|
| 900 |
+ return nil |
|
| 901 |
+} |
|
| 902 |
+ |
|
| 874 | 903 |
func optionsHandler(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 875 | 904 |
w.WriteHeader(http.StatusOK) |
| 876 | 905 |
return nil |
| ... | ... |
@@ -918,6 +947,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) {
|
| 918 | 918 |
"/containers/{name:.*}/wait": postContainersWait,
|
| 919 | 919 |
"/containers/{name:.*}/resize": postContainersResize,
|
| 920 | 920 |
"/containers/{name:.*}/attach": postContainersAttach,
|
| 921 |
+ "/containers/{name:.*}/copy": postContainersCopy,
|
|
| 921 | 922 |
}, |
| 922 | 923 |
"DELETE": {
|
| 923 | 924 |
"/containers/{name:.*}": deleteContainers,
|
| ... | ... |
@@ -1156,6 +1156,70 @@ func TestJsonContentType(t *testing.T) {
|
| 1156 | 1156 |
} |
| 1157 | 1157 |
} |
| 1158 | 1158 |
|
| 1159 |
+func TestPostContainersCopy(t *testing.T) {
|
|
| 1160 |
+ runtime := mkRuntime(t) |
|
| 1161 |
+ defer nuke(runtime) |
|
| 1162 |
+ |
|
| 1163 |
+ srv := &Server{runtime: runtime}
|
|
| 1164 |
+ |
|
| 1165 |
+ builder := NewBuilder(runtime) |
|
| 1166 |
+ |
|
| 1167 |
+ // Create a container and remove a file |
|
| 1168 |
+ container, err := builder.Create( |
|
| 1169 |
+ &Config{
|
|
| 1170 |
+ Image: GetTestImage(runtime).ID, |
|
| 1171 |
+ Cmd: []string{"touch", "/test.txt"},
|
|
| 1172 |
+ }, |
|
| 1173 |
+ ) |
|
| 1174 |
+ if err != nil {
|
|
| 1175 |
+ t.Fatal(err) |
|
| 1176 |
+ } |
|
| 1177 |
+ defer runtime.Destroy(container) |
|
| 1178 |
+ |
|
| 1179 |
+ if err := container.Run(); err != nil {
|
|
| 1180 |
+ t.Fatal(err) |
|
| 1181 |
+ } |
|
| 1182 |
+ |
|
| 1183 |
+ r := httptest.NewRecorder() |
|
| 1184 |
+ copyData := APICopy{HostPath: ".", Resource: "test.txt"}
|
|
| 1185 |
+ |
|
| 1186 |
+ jsonData, err := json.Marshal(copyData) |
|
| 1187 |
+ if err != nil {
|
|
| 1188 |
+ t.Fatal(err) |
|
| 1189 |
+ } |
|
| 1190 |
+ |
|
| 1191 |
+ req, err := http.NewRequest("POST", "/containers/"+container.ID+"/copy", bytes.NewReader(jsonData))
|
|
| 1192 |
+ if err != nil {
|
|
| 1193 |
+ t.Fatal(err) |
|
| 1194 |
+ } |
|
| 1195 |
+ req.Header.Add("Content-Type", "application/json")
|
|
| 1196 |
+ if err = postContainersCopy(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil {
|
|
| 1197 |
+ t.Fatal(err) |
|
| 1198 |
+ } |
|
| 1199 |
+ |
|
| 1200 |
+ if r.Code != http.StatusOK {
|
|
| 1201 |
+ t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code)
|
|
| 1202 |
+ } |
|
| 1203 |
+ |
|
| 1204 |
+ found := false |
|
| 1205 |
+ for tarReader := tar.NewReader(r.Body); ; {
|
|
| 1206 |
+ h, err := tarReader.Next() |
|
| 1207 |
+ if err != nil {
|
|
| 1208 |
+ if err == io.EOF {
|
|
| 1209 |
+ break |
|
| 1210 |
+ } |
|
| 1211 |
+ t.Fatal(err) |
|
| 1212 |
+ } |
|
| 1213 |
+ if h.Name == "test.txt" {
|
|
| 1214 |
+ found = true |
|
| 1215 |
+ break |
|
| 1216 |
+ } |
|
| 1217 |
+ } |
|
| 1218 |
+ if !found {
|
|
| 1219 |
+ t.Fatalf("The created test file has not been found in the copied output")
|
|
| 1220 |
+ } |
|
| 1221 |
+} |
|
| 1222 |
+ |
|
| 1159 | 1223 |
// Mocked types for tests |
| 1160 | 1224 |
type NopConn struct {
|
| 1161 | 1225 |
io.ReadCloser |
| ... | ... |
@@ -77,6 +77,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
|
| 77 | 77 |
{"attach", "Attach to a running container"},
|
| 78 | 78 |
{"build", "Build a container from a Dockerfile"},
|
| 79 | 79 |
{"commit", "Create a new image from a container's changes"},
|
| 80 |
+ {"cp", "Copy files/folders from the containers filesystem to the host path"},
|
|
| 80 | 81 |
{"diff", "Inspect changes on a container's filesystem"},
|
| 81 | 82 |
{"events", "Get real time events from the server"},
|
| 82 | 83 |
{"export", "Stream the contents of a container as a tar archive"},
|
| ... | ... |
@@ -1469,6 +1470,37 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
| 1469 | 1469 |
return nil |
| 1470 | 1470 |
} |
| 1471 | 1471 |
|
| 1472 |
+func (cli *DockerCli) CmdCp(args ...string) error {
|
|
| 1473 |
+ cmd := Subcmd("cp", "CONTAINER:RESOURCE HOSTPATH", "Copy files/folders from the RESOURCE to the HOSTPATH")
|
|
| 1474 |
+ if err := cmd.Parse(args); err != nil {
|
|
| 1475 |
+ return nil |
|
| 1476 |
+ } |
|
| 1477 |
+ |
|
| 1478 |
+ if cmd.NArg() != 2 {
|
|
| 1479 |
+ cmd.Usage() |
|
| 1480 |
+ return nil |
|
| 1481 |
+ } |
|
| 1482 |
+ |
|
| 1483 |
+ var copyData APICopy |
|
| 1484 |
+ info := strings.Split(cmd.Arg(0), ":") |
|
| 1485 |
+ |
|
| 1486 |
+ copyData.Resource = info[1] |
|
| 1487 |
+ copyData.HostPath = cmd.Arg(1) |
|
| 1488 |
+ |
|
| 1489 |
+ data, statusCode, err := cli.call("POST", "/containers/"+info[0]+"/copy", copyData)
|
|
| 1490 |
+ if err != nil {
|
|
| 1491 |
+ return err |
|
| 1492 |
+ } |
|
| 1493 |
+ |
|
| 1494 |
+ r := bytes.NewReader(data) |
|
| 1495 |
+ if statusCode == 200 {
|
|
| 1496 |
+ if err := Untar(r, copyData.HostPath); err != nil {
|
|
| 1497 |
+ return err |
|
| 1498 |
+ } |
|
| 1499 |
+ } |
|
| 1500 |
+ return nil |
|
| 1501 |
+} |
|
| 1502 |
+ |
|
| 1472 | 1503 |
func (cli *DockerCli) checkIfLogged(action string) error {
|
| 1473 | 1504 |
// If condition AND the login failed |
| 1474 | 1505 |
if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
|
| ... | ... |
@@ -1089,3 +1089,10 @@ func (container *Container) GetSize() (int64, int64) {
|
| 1089 | 1089 |
} |
| 1090 | 1090 |
return sizeRw, sizeRootfs |
| 1091 | 1091 |
} |
| 1092 |
+ |
|
| 1093 |
+func (container *Container) Copy(resource string) (Archive, error) {
|
|
| 1094 |
+ if err := container.EnsureMounted(); err != nil {
|
|
| 1095 |
+ return nil, err |
|
| 1096 |
+ } |
|
| 1097 |
+ return TarFilter(container.RootfsPath(), Uncompressed, []string{resource})
|
|
| 1098 |
+} |
| ... | ... |
@@ -525,6 +525,38 @@ Remove a container |
| 525 | 525 |
:statuscode 500: server error |
| 526 | 526 |
|
| 527 | 527 |
|
| 528 |
+Copy files or folders from a container |
|
| 529 |
+************************************** |
|
| 530 |
+ |
|
| 531 |
+.. http:post:: /containers/(id)/copy |
|
| 532 |
+ |
|
| 533 |
+ Copy files or folders of container ``id`` |
|
| 534 |
+ |
|
| 535 |
+ **Example request**: |
|
| 536 |
+ |
|
| 537 |
+ .. sourcecode:: http |
|
| 538 |
+ |
|
| 539 |
+ POST /containers/4fa6e0f0c678/copy HTTP/1.1 |
|
| 540 |
+ Content-Type: application/json |
|
| 541 |
+ |
|
| 542 |
+ {
|
|
| 543 |
+ "Resource":"test.txt" |
|
| 544 |
+ } |
|
| 545 |
+ |
|
| 546 |
+ **Example response**: |
|
| 547 |
+ |
|
| 548 |
+ .. sourcecode:: http |
|
| 549 |
+ |
|
| 550 |
+ HTTP/1.1 200 OK |
|
| 551 |
+ Content-Type: application/octet-stream |
|
| 552 |
+ |
|
| 553 |
+ {{ STREAM }}
|
|
| 554 |
+ |
|
| 555 |
+ :statuscode 200: no error |
|
| 556 |
+ :statuscode 404: no such container |
|
| 557 |
+ :statuscode 500: server error |
|
| 558 |
+ |
|
| 559 |
+ |
|
| 528 | 560 |
2.2 Images |
| 529 | 561 |
---------- |
| 530 | 562 |
|
| ... | ... |
@@ -1091,7 +1123,6 @@ Monitor Docker's events |
| 1091 | 1091 |
:statuscode 200: no error |
| 1092 | 1092 |
:statuscode 500: server error |
| 1093 | 1093 |
|
| 1094 |
- |
|
| 1095 | 1094 |
3. Going further |
| 1096 | 1095 |
================ |
| 1097 | 1096 |
|
| 36 | 37 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,13 @@ |
| 0 |
+:title: Cp Command |
|
| 1 |
+:description: Copy files/folders from the containers filesystem to the host path |
|
| 2 |
+:keywords: cp, docker, container, documentation, copy |
|
| 3 |
+ |
|
| 4 |
+=========================================================== |
|
| 5 |
+``cp`` -- Copy files/folders from the containers filesystem to the host path |
|
| 6 |
+=========================================================== |
|
| 7 |
+ |
|
| 8 |
+:: |
|
| 9 |
+ |
|
| 10 |
+ Usage: docker cp CONTAINER:RESOURCE HOSTPATH |
|
| 11 |
+ |
|
| 12 |
+ Copy files/folders from the containers filesystem to the host path. Paths are relative to the root of the filesystem. |
| ... | ... |
@@ -1169,6 +1169,23 @@ func (srv *Server) ImageInspect(name string) (*Image, error) {
|
| 1169 | 1169 |
return nil, fmt.Errorf("No such image: %s", name)
|
| 1170 | 1170 |
} |
| 1171 | 1171 |
|
| 1172 |
+func (srv *Server) ContainerCopy(name string, resource string, out io.Writer) error {
|
|
| 1173 |
+ if container := srv.runtime.Get(name); container != nil {
|
|
| 1174 |
+ |
|
| 1175 |
+ data, err := container.Copy(resource) |
|
| 1176 |
+ if err != nil {
|
|
| 1177 |
+ return err |
|
| 1178 |
+ } |
|
| 1179 |
+ |
|
| 1180 |
+ if _, err := io.Copy(out, data); err != nil {
|
|
| 1181 |
+ return err |
|
| 1182 |
+ } |
|
| 1183 |
+ return nil |
|
| 1184 |
+ } |
|
| 1185 |
+ return fmt.Errorf("No such container: %s", name)
|
|
| 1186 |
+ |
|
| 1187 |
+} |
|
| 1188 |
+ |
|
| 1172 | 1189 |
func NewServer(flGraphPath string, autoRestart, enableCors bool, dns ListOpts) (*Server, error) {
|
| 1173 | 1190 |
if runtime.GOARCH != "amd64" {
|
| 1174 | 1191 |
log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH)
|