浏览代码

Merge pull request #1221 from crosbymichael/cmd-cp

*Client: Add docker cp command and copy api endpoint to copy container files/folders to the host
Victor Vieux 12 年之前
父节点
当前提交
4af24e11a4

+ 31 - 0
api.go

@@ -871,6 +871,36 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
 	return nil
 }
 
+func postContainersCopy(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if vars == nil {
+		return fmt.Errorf("Missing parameter")
+	}
+	name := vars["name"]
+
+	copyData := &APICopy{}
+	contentType := r.Header.Get("Content-Type")
+	if contentType == "application/json" {
+		if err := json.NewDecoder(r.Body).Decode(copyData); err != nil {
+			return err
+		}
+	} else {
+		return fmt.Errorf("Content-Type not supported: %s", contentType)
+	}
+
+	if copyData.Resource == "" {
+		return fmt.Errorf("Resource cannot be empty")
+	}
+	if copyData.Resource[0] == '/' {
+		copyData.Resource = copyData.Resource[1:]
+	}
+
+	if err := srv.ContainerCopy(name, copyData.Resource, w); err != nil {
+		utils.Debugf("%s", err.Error())
+		return err
+	}
+	return nil
+}
+
 func optionsHandler(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	w.WriteHeader(http.StatusOK)
 	return nil
@@ -918,6 +948,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) {
 			"/containers/{name:.*}/wait":    postContainersWait,
 			"/containers/{name:.*}/resize":  postContainersResize,
 			"/containers/{name:.*}/attach":  postContainersAttach,
+			"/containers/{name:.*}/copy":    postContainersCopy,
 		},
 		"DELETE": {
 			"/containers/{name:.*}": deleteContainers,

+ 5 - 0
api_params.go

@@ -86,3 +86,8 @@ type APIImageConfig struct {
 	ID string `json:"Id"`
 	*Config
 }
+
+type APICopy struct {
+	Resource string
+	HostPath string
+}

+ 64 - 0
api_test.go

@@ -1156,6 +1156,70 @@ func TestJsonContentType(t *testing.T) {
 	}
 }
 
+func TestPostContainersCopy(t *testing.T) {
+	runtime := mkRuntime(t)
+	defer nuke(runtime)
+
+	srv := &Server{runtime: runtime}
+
+	builder := NewBuilder(runtime)
+
+	// Create a container and remove a file
+	container, err := builder.Create(
+		&Config{
+			Image: GetTestImage(runtime).ID,
+			Cmd:   []string{"touch", "/test.txt"},
+		},
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer runtime.Destroy(container)
+
+	if err := container.Run(); err != nil {
+		t.Fatal(err)
+	}
+
+	r := httptest.NewRecorder()
+	copyData := APICopy{HostPath: ".", Resource: "/test.txt"}
+
+	jsonData, err := json.Marshal(copyData)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req, err := http.NewRequest("POST", "/containers/"+container.ID+"/copy", bytes.NewReader(jsonData))
+	if err != nil {
+		t.Fatal(err)
+	}
+	req.Header.Add("Content-Type", "application/json")
+	if err = postContainersCopy(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil {
+		t.Fatal(err)
+	}
+
+	if r.Code != http.StatusOK {
+		t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code)
+	}
+
+	found := false
+	for tarReader := tar.NewReader(r.Body); ; {
+		h, err := tarReader.Next()
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			t.Fatal(err)
+		}
+		if h.Name == "test.txt" {
+			found = true
+			break
+		}
+	}
+	if !found {
+		t.Fatalf("The created test file has not been found in the copied output")
+	}
+}
+
 // Mocked types for tests
 type NopConn struct {
 	io.ReadCloser

+ 32 - 0
commands.go

@@ -77,6 +77,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
 		{"attach", "Attach to a running container"},
 		{"build", "Build a container from a Dockerfile"},
 		{"commit", "Create a new image from a container's changes"},
+		{"cp", "Copy files/folders from the containers filesystem to the host path"},
 		{"diff", "Inspect changes on a container's filesystem"},
 		{"events", "Get real time events from the server"},
 		{"export", "Stream the contents of a container as a tar archive"},
@@ -1469,6 +1470,37 @@ func (cli *DockerCli) CmdRun(args ...string) error {
 	return nil
 }
 
+func (cli *DockerCli) CmdCp(args ...string) error {
+	cmd := Subcmd("cp", "CONTAINER:RESOURCE HOSTPATH", "Copy files/folders from the RESOURCE to the HOSTPATH")
+	if err := cmd.Parse(args); err != nil {
+		return nil
+	}
+
+	if cmd.NArg() != 2 {
+		cmd.Usage()
+		return nil
+	}
+
+	var copyData APICopy
+	info := strings.Split(cmd.Arg(0), ":")
+
+	copyData.Resource = info[1]
+	copyData.HostPath = cmd.Arg(1)
+
+	data, statusCode, err := cli.call("POST", "/containers/"+info[0]+"/copy", copyData)
+	if err != nil {
+		return err
+	}
+
+	if statusCode == 200 {
+		r := bytes.NewReader(data)
+		if err := Untar(r, copyData.HostPath); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (cli *DockerCli) checkIfLogged(action string) error {
 	// If condition AND the login failed
 	if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {

+ 21 - 0
container.go

@@ -1089,3 +1089,24 @@ func (container *Container) GetSize() (int64, int64) {
 	}
 	return sizeRw, sizeRootfs
 }
+
+func (container *Container) Copy(resource string) (Archive, error) {
+	if err := container.EnsureMounted(); err != nil {
+		return nil, err
+	}
+	var filter []string
+	basePath := path.Join(container.RootfsPath(), resource)
+	stat, err := os.Stat(basePath)
+	if err != nil {
+		return nil, err
+	}
+	if !stat.IsDir() {
+		d, f := path.Split(basePath)
+		basePath = d
+		filter = []string{f}
+	} else {
+		filter = []string{path.Base(basePath)}
+		basePath = path.Dir(basePath)
+	}
+	return TarFilter(basePath, Uncompressed, filter)
+}

+ 32 - 0
docs/sources/api/docker_remote_api_v1.4.rst

@@ -528,6 +528,38 @@ Remove a container
         :statuscode 500: server error
 
 
+Copy files or folders from a container
+**************************************
+
+.. http:post:: /containers/(id)/copy
+
+	Copy files or folders of container ``id``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   POST /containers/4fa6e0f0c678/copy HTTP/1.1
+	   Content-Type: application/json
+
+	   {
+		"Resource":"test.txt"
+	   }
+
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/octet-stream
+	   
+	   {{ STREAM }}
+
+	:statuscode 200: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
 2.2 Images
 ----------
 

+ 1 - 0
docs/sources/commandline/cli.rst

@@ -30,6 +30,7 @@ Available Commands
    command/attach
    command/build
    command/commit
+   command/cp
    command/diff
    command/export
    command/history

+ 13 - 0
docs/sources/commandline/command/cp.rst

@@ -0,0 +1,13 @@
+:title: Cp Command
+:description: Copy files/folders from the containers filesystem to the host path
+:keywords: cp, docker, container, documentation, copy
+
+===========================================================
+``cp`` -- Copy files/folders from the containers filesystem to the host path
+===========================================================
+
+::
+
+    Usage: docker cp CONTAINER:RESOURCE HOSTPATH
+
+    Copy files/folders from the containers filesystem to the host path.  Paths are relative to the root of the filesystem.

+ 1 - 0
docs/sources/commandline/index.rst

@@ -15,6 +15,7 @@ Contents:
   attach  <command/attach>
   build   <command/build>
   commit  <command/commit>
+  cp      <command/cp>
   diff    <command/diff>
   export  <command/export>
   history <command/history>

+ 17 - 0
server.go

@@ -1169,6 +1169,23 @@ func (srv *Server) ImageInspect(name string) (*Image, error) {
 	return nil, fmt.Errorf("No such image: %s", name)
 }
 
+func (srv *Server) ContainerCopy(name string, resource string, out io.Writer) error {
+	if container := srv.runtime.Get(name); container != nil {
+
+		data, err := container.Copy(resource)
+		if err != nil {
+			return err
+		}
+
+		if _, err := io.Copy(out, data); err != nil {
+			return err
+		}
+		return nil
+	}
+	return fmt.Errorf("No such container: %s", name)
+
+}
+
 func NewServer(flGraphPath string, autoRestart, enableCors bool, dns ListOpts) (*Server, error) {
 	if runtime.GOARCH != "amd64" {
 		log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH)