From 5b8cfbe15c29efd3e72ea97ba87867590aeeba25 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 16 Jul 2013 19:07:41 -0900 Subject: [PATCH] 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 --- api.go | 30 ++++++++++ api_params.go | 5 ++ api_test.go | 64 +++++++++++++++++++++ commands.go | 32 +++++++++++ container.go | 7 +++ docs/sources/api/docker_remote_api_v1.3.rst | 33 ++++++++++- docs/sources/commandline/cli.rst | 1 + docs/sources/commandline/command/cp.rst | 13 +++++ docs/sources/commandline/index.rst | 1 + server.go | 17 ++++++ 10 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 docs/sources/commandline/command/cp.rst diff --git a/api.go b/api.go index f5f0df1b98..f9257b1ccd 100644 --- a/api.go +++ b/api.go @@ -871,6 +871,35 @@ 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{} + if r.Header.Get("Content-Type") == "application/json" { + if err := json.NewDecoder(r.Body).Decode(copyData); err != nil { + return err + } + } else { + return fmt.Errorf("Content-Type not supported: %s", r.Header.Get("Content-Type")) + } + + if copyData.Resource == "" { + return fmt.Errorf("Resource cannot be empty") + } + if copyData.Resource[0] == '/' { + return fmt.Errorf("Resource cannot contain a leading /") + } + + if err := srv.ContainerCopy(name, copyData.Resource, w); err != nil { + utils.Debugf("%s", err) + 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 +947,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, diff --git a/api_params.go b/api_params.go index 1ae26072e7..b75e54c116 100644 --- a/api_params.go +++ b/api_params.go @@ -86,3 +86,8 @@ type APIImageConfig struct { ID string `json:"Id"` *Config } + +type APICopy struct { + Resource string + HostPath string +} diff --git a/api_test.go b/api_test.go index 5316b4fc5e..52baf35f38 100644 --- a/api_test.go +++ b/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 diff --git a/commands.go b/commands.go index c52126519a..eb05202ded 100644 --- a/commands.go +++ b/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 + } + + r := bytes.NewReader(data) + if statusCode == 200 { + 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 == "" { diff --git a/container.go b/container.go index 6c92973920..7791d4b4af 100644 --- a/container.go +++ b/container.go @@ -1089,3 +1089,10 @@ 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 + } + return TarFilter(container.RootfsPath(), Uncompressed, []string{resource}) +} diff --git a/docs/sources/api/docker_remote_api_v1.3.rst b/docs/sources/api/docker_remote_api_v1.3.rst index 8e5c7b2a3b..fe3d204d4d 100644 --- a/docs/sources/api/docker_remote_api_v1.3.rst +++ b/docs/sources/api/docker_remote_api_v1.3.rst @@ -525,6 +525,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 ---------- @@ -1091,7 +1123,6 @@ Monitor Docker's events :statuscode 200: no error :statuscode 500: server error - 3. Going further ================ diff --git a/docs/sources/commandline/cli.rst b/docs/sources/commandline/cli.rst index e499b1f096..bdc4827a60 100644 --- a/docs/sources/commandline/cli.rst +++ b/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 diff --git a/docs/sources/commandline/command/cp.rst b/docs/sources/commandline/command/cp.rst new file mode 100644 index 0000000000..14b5061ef7 --- /dev/null +++ b/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. diff --git a/docs/sources/commandline/index.rst b/docs/sources/commandline/index.rst index a7296b27da..5c2b373205 100644 --- a/docs/sources/commandline/index.rst +++ b/docs/sources/commandline/index.rst @@ -15,6 +15,7 @@ Contents: attach build commit + cp diff export history diff --git a/server.go b/server.go index 7607851f49..c7b2ad2dcb 100644 --- a/server.go +++ b/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)