Browse code

Make container.Copy support volumes Fixes #1992

Right now when you `docker cp` a path which is in a volume, the cp
itself works, however you end up getting files that are in the
container's fs rather than the files in the volume (which is not in the
container's fs).
This makes it so when you `docker cp` a path that is in a volume it
follows the volume to the real path on the host.

archive.go has been modified so that when you do `docker cp mydata:/foo
.`, and /foo is the volume, the outputed folder is called "foo" instead
of the volume ID (because we are telling it to tar up
`/var/lib/docker/vfs/dir/<some id>` and not "foo", but the user would be
expecting "foo", not the ID

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2014/09/24 22:07:11
Showing 5 changed files
... ...
@@ -826,19 +826,25 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
826 826
 		return nil, err
827 827
 	}
828 828
 
829
-	var filter []string
830
-
831 829
 	basePath, err := container.getResourcePath(resource)
832 830
 	if err != nil {
833 831
 		container.Unmount()
834 832
 		return nil, err
835 833
 	}
836 834
 
835
+	// Check if this is actually in a volume
836
+	for _, mnt := range container.VolumeMounts() {
837
+		if len(mnt.MountToPath) > 0 && strings.HasPrefix(resource, mnt.MountToPath[1:]) {
838
+			return mnt.Export(resource)
839
+		}
840
+	}
841
+
837 842
 	stat, err := os.Stat(basePath)
838 843
 	if err != nil {
839 844
 		container.Unmount()
840 845
 		return nil, err
841 846
 	}
847
+	var filter []string
842 848
 	if !stat.IsDir() {
843 849
 		d, f := path.Split(basePath)
844 850
 		basePath = d
... ...
@@ -2,6 +2,7 @@ package daemon
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"io"
5 6
 	"io/ioutil"
6 7
 	"os"
7 8
 	"path/filepath"
... ...
@@ -24,6 +25,18 @@ type Mount struct {
24 24
 	copyData    bool
25 25
 }
26 26
 
27
+func (mnt *Mount) Export(resource string) (io.ReadCloser, error) {
28
+	var name string
29
+	if resource == mnt.MountToPath[1:] {
30
+		name = filepath.Base(resource)
31
+	}
32
+	path, err := filepath.Rel(mnt.MountToPath[1:], resource)
33
+	if err != nil {
34
+		return nil, err
35
+	}
36
+	return mnt.volume.Export(path, name)
37
+}
38
+
27 39
 func (container *Container) prepareVolumes() error {
28 40
 	if container.Volumes == nil || len(container.Volumes) == 0 {
29 41
 		container.Volumes = make(map[string]string)
... ...
@@ -1,6 +1,7 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"bytes"
4 5
 	"fmt"
5 6
 	"io/ioutil"
6 7
 	"os"
... ...
@@ -371,3 +372,109 @@ func TestCpUnprivilegedUser(t *testing.T) {
371 371
 
372 372
 	logDone("cp - unprivileged user")
373 373
 }
374
+
375
+func TestCpVolumePath(t *testing.T) {
376
+	tmpDir, err := ioutil.TempDir("", "cp-test-volumepath")
377
+	if err != nil {
378
+		t.Fatal(err)
379
+	}
380
+	defer os.RemoveAll(tmpDir)
381
+	outDir, err := ioutil.TempDir("", "cp-test-volumepath-out")
382
+	if err != nil {
383
+		t.Fatal(err)
384
+	}
385
+	defer os.RemoveAll(outDir)
386
+	_, err = os.Create(tmpDir + "/test")
387
+	if err != nil {
388
+		t.Fatal(err)
389
+	}
390
+
391
+	out, exitCode, err := cmd(t, "run", "-d", "-v", "/foo", "-v", tmpDir+"/test:/test", "-v", tmpDir+":/baz", "busybox", "/bin/sh", "-c", "touch /foo/bar")
392
+	if err != nil || exitCode != 0 {
393
+		t.Fatal("failed to create a container", out, err)
394
+	}
395
+
396
+	cleanedContainerID := stripTrailingCharacters(out)
397
+	defer deleteContainer(cleanedContainerID)
398
+
399
+	out, _, err = cmd(t, "wait", cleanedContainerID)
400
+	if err != nil || stripTrailingCharacters(out) != "0" {
401
+		t.Fatal("failed to set up container", out, err)
402
+	}
403
+
404
+	// Copy actual volume path
405
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/foo", outDir)
406
+	if err != nil {
407
+		t.Fatalf("couldn't copy from volume path: %s:%s %v", cleanedContainerID, "/foo", err)
408
+	}
409
+	stat, err := os.Stat(outDir + "/foo")
410
+	if err != nil {
411
+		t.Fatal(err)
412
+	}
413
+	if !stat.IsDir() {
414
+		t.Fatal("expected copied content to be dir")
415
+	}
416
+	stat, err = os.Stat(outDir + "/foo/bar")
417
+	if err != nil {
418
+		t.Fatal(err)
419
+	}
420
+	if stat.IsDir() {
421
+		t.Fatal("Expected file `bar` to be a file")
422
+	}
423
+
424
+	// Copy file nested in volume
425
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/foo/bar", outDir)
426
+	if err != nil {
427
+		t.Fatalf("couldn't copy from volume path: %s:%s %v", cleanedContainerID, "/foo", err)
428
+	}
429
+	stat, err = os.Stat(outDir + "/bar")
430
+	if err != nil {
431
+		t.Fatal(err)
432
+	}
433
+	if stat.IsDir() {
434
+		t.Fatal("Expected file `bar` to be a file")
435
+	}
436
+
437
+	// Copy Bind-mounted dir
438
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/baz", outDir)
439
+	if err != nil {
440
+		t.Fatalf("couldn't copy from bind-mounted volume path: %s:%s %v", cleanedContainerID, "/baz", err)
441
+	}
442
+	stat, err = os.Stat(outDir + "/baz")
443
+	if err != nil {
444
+		t.Fatal(err)
445
+	}
446
+	if !stat.IsDir() {
447
+		t.Fatal("Expected `baz` to be a dir")
448
+	}
449
+
450
+	// Copy file nested in bind-mounted dir
451
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/baz/test", outDir)
452
+	fb, err := ioutil.ReadFile(outDir + "/baz/test")
453
+	if err != nil {
454
+		t.Fatal(err)
455
+	}
456
+	fb2, err := ioutil.ReadFile(tmpDir + "/test")
457
+	if err != nil {
458
+		t.Fatal(err)
459
+	}
460
+	if !bytes.Equal(fb, fb2) {
461
+		t.Fatalf("Expected copied file to be duplicate of bind-mounted file")
462
+	}
463
+
464
+	// Copy bind-mounted file
465
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/test", outDir)
466
+	fb, err = ioutil.ReadFile(outDir + "/test")
467
+	if err != nil {
468
+		t.Fatal(err)
469
+	}
470
+	fb2, err = ioutil.ReadFile(tmpDir + "/test")
471
+	if err != nil {
472
+		t.Fatal(err)
473
+	}
474
+	if !bytes.Equal(fb, fb2) {
475
+		t.Fatalf("Expected copied file to be duplicate of bind-mounted file")
476
+	}
477
+
478
+	logDone("cp - volume path")
479
+}
... ...
@@ -34,6 +34,7 @@ type (
34 34
 		Excludes    []string
35 35
 		Compression Compression
36 36
 		NoLchown    bool
37
+		Name        string
37 38
 	}
38 39
 )
39 40
 
... ...
@@ -359,6 +360,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
359 359
 		twBuf := pools.BufioWriter32KPool.Get(nil)
360 360
 		defer pools.BufioWriter32KPool.Put(twBuf)
361 361
 
362
+		var renamedRelFilePath string // For when tar.Options.Name is set
362 363
 		for _, include := range options.Includes {
363 364
 			filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
364 365
 				if err != nil {
... ...
@@ -384,6 +386,15 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
384 384
 					return nil
385 385
 				}
386 386
 
387
+				// Rename the base resource
388
+				if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) {
389
+					renamedRelFilePath = relFilePath
390
+				}
391
+				// Set this to make sure the items underneath also get renamed
392
+				if options.Name != "" {
393
+					relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1)
394
+				}
395
+
387 396
 				if err := addTarFile(filePath, relFilePath, tw, twBuf); err != nil {
388 397
 					log.Debugf("Can't add file %s to tar: %s", srcPath, err)
389 398
 				}
... ...
@@ -2,11 +2,14 @@ package volumes
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
+	"io"
5 6
 	"io/ioutil"
6 7
 	"os"
8
+	"path"
7 9
 	"path/filepath"
8 10
 	"sync"
9 11
 
12
+	"github.com/docker/docker/pkg/archive"
10 13
 	"github.com/docker/docker/pkg/symlink"
11 14
 )
12 15
 
... ...
@@ -21,6 +24,35 @@ type Volume struct {
21 21
 	lock        sync.Mutex
22 22
 }
23 23
 
24
+func (v *Volume) Export(resource, name string) (io.ReadCloser, error) {
25
+	if v.IsBindMount && filepath.Base(resource) == name {
26
+		name = ""
27
+	}
28
+
29
+	basePath, err := v.getResourcePath(resource)
30
+	if err != nil {
31
+		return nil, err
32
+	}
33
+	stat, err := os.Stat(basePath)
34
+	if err != nil {
35
+		return nil, err
36
+	}
37
+	var filter []string
38
+	if !stat.IsDir() {
39
+		d, f := path.Split(basePath)
40
+		basePath = d
41
+		filter = []string{f}
42
+	} else {
43
+		filter = []string{path.Base(basePath)}
44
+		basePath = path.Dir(basePath)
45
+	}
46
+	return archive.TarWithOptions(basePath, &archive.TarOptions{
47
+		Compression: archive.Uncompressed,
48
+		Name:        name,
49
+		Includes:    filter,
50
+	})
51
+}
52
+
24 53
 func (v *Volume) IsDir() (bool, error) {
25 54
 	stat, err := os.Stat(v.Path)
26 55
 	if err != nil {
... ...
@@ -137,3 +169,8 @@ func (v *Volume) getRootResourcePath(path string) (string, error) {
137 137
 	cleanPath := filepath.Join("/", path)
138 138
 	return symlink.FollowSymlinkInScope(filepath.Join(v.configPath, cleanPath), v.configPath)
139 139
 }
140
+
141
+func (v *Volume) getResourcePath(path string) (string, error) {
142
+	cleanPath := filepath.Join("/", path)
143
+	return symlink.FollowSymlinkInScope(filepath.Join(v.Path, cleanPath), v.Path)
144
+}