mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
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
This commit is contained in:
commit
4af24e11a4
10 changed files with 217 additions and 0 deletions
31
api.go
31
api.go
|
@ -871,6 +871,36 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
|
||||||
return nil
|
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 {
|
func optionsHandler(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return nil
|
return nil
|
||||||
|
@ -918,6 +948,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) {
|
||||||
"/containers/{name:.*}/wait": postContainersWait,
|
"/containers/{name:.*}/wait": postContainersWait,
|
||||||
"/containers/{name:.*}/resize": postContainersResize,
|
"/containers/{name:.*}/resize": postContainersResize,
|
||||||
"/containers/{name:.*}/attach": postContainersAttach,
|
"/containers/{name:.*}/attach": postContainersAttach,
|
||||||
|
"/containers/{name:.*}/copy": postContainersCopy,
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"/containers/{name:.*}": deleteContainers,
|
"/containers/{name:.*}": deleteContainers,
|
||||||
|
|
|
@ -86,3 +86,8 @@ type APIImageConfig struct {
|
||||||
ID string `json:"Id"`
|
ID string `json:"Id"`
|
||||||
*Config
|
*Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type APICopy struct {
|
||||||
|
Resource string
|
||||||
|
HostPath string
|
||||||
|
}
|
||||||
|
|
64
api_test.go
64
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
|
// Mocked types for tests
|
||||||
type NopConn struct {
|
type NopConn struct {
|
||||||
io.ReadCloser
|
io.ReadCloser
|
||||||
|
|
32
commands.go
32
commands.go
|
@ -77,6 +77,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
|
||||||
{"attach", "Attach to a running container"},
|
{"attach", "Attach to a running container"},
|
||||||
{"build", "Build a container from a Dockerfile"},
|
{"build", "Build a container from a Dockerfile"},
|
||||||
{"commit", "Create a new image from a container's changes"},
|
{"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"},
|
{"diff", "Inspect changes on a container's filesystem"},
|
||||||
{"events", "Get real time events from the server"},
|
{"events", "Get real time events from the server"},
|
||||||
{"export", "Stream the contents of a container as a tar archive"},
|
{"export", "Stream the contents of a container as a tar archive"},
|
||||||
|
@ -1469,6 +1470,37 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
||||||
return nil
|
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 {
|
func (cli *DockerCli) checkIfLogged(action string) error {
|
||||||
// If condition AND the login failed
|
// If condition AND the login failed
|
||||||
if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
|
if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
|
||||||
|
|
21
container.go
21
container.go
|
@ -1089,3 +1089,24 @@ func (container *Container) GetSize() (int64, int64) {
|
||||||
}
|
}
|
||||||
return sizeRw, sizeRootfs
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -528,6 +528,38 @@ Remove a container
|
||||||
:statuscode 500: server error
|
: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
|
2.2 Images
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ Available Commands
|
||||||
command/attach
|
command/attach
|
||||||
command/build
|
command/build
|
||||||
command/commit
|
command/commit
|
||||||
|
command/cp
|
||||||
command/diff
|
command/diff
|
||||||
command/export
|
command/export
|
||||||
command/history
|
command/history
|
||||||
|
|
13
docs/sources/commandline/command/cp.rst
Normal file
13
docs/sources/commandline/command/cp.rst
Normal file
|
@ -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.
|
|
@ -15,6 +15,7 @@ Contents:
|
||||||
attach <command/attach>
|
attach <command/attach>
|
||||||
build <command/build>
|
build <command/build>
|
||||||
commit <command/commit>
|
commit <command/commit>
|
||||||
|
cp <command/cp>
|
||||||
diff <command/diff>
|
diff <command/diff>
|
||||||
export <command/export>
|
export <command/export>
|
||||||
history <command/history>
|
history <command/history>
|
||||||
|
|
17
server.go
17
server.go
|
@ -1169,6 +1169,23 @@ func (srv *Server) ImageInspect(name string) (*Image, error) {
|
||||||
return nil, fmt.Errorf("No such image: %s", name)
|
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) {
|
func NewServer(flGraphPath string, autoRestart, enableCors bool, dns ListOpts) (*Server, error) {
|
||||||
if runtime.GOARCH != "amd64" {
|
if runtime.GOARCH != "amd64" {
|
||||||
log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH)
|
log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH)
|
||||||
|
|
Loading…
Reference in a new issue