Browse code

Add cp command and copy api endpoint

The cp command and copy api endpoint allows users
to copy files and or folders from a containers filesystem.

Closes #382

Michael Crosby authored on 2013/07/17 13:07:41
Showing 10 changed files
... ...
@@ -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,
... ...
@@ -86,3 +86,8 @@ type APIImageConfig struct {
86 86
 	ID string `json:"Id"`
87 87
 	*Config
88 88
 }
89
+
90
+type APICopy struct {
91
+	Resource string
92
+	HostPath string
93
+}
... ...
@@ -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
 
... ...
@@ -30,6 +30,7 @@ Available Commands
30 30
    command/attach
31 31
    command/build
32 32
    command/commit
33
+   command/cp
33 34
    command/diff
34 35
    command/export
35 36
    command/history
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.
... ...
@@ -15,6 +15,7 @@ Contents:
15 15
   attach  <command/attach>
16 16
   build   <command/build>
17 17
   commit  <command/commit>
18
+  cp      <command/cp>
18 19
   diff    <command/diff>
19 20
   export  <command/export>
20 21
   history <command/history>
... ...
@@ -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)