Browse code

Fix copying hardlinks in graphdriver/copy

Previously, graphdriver/copy would improperly copy hardlinks as just regular
files. This patch changes that behaviour, and instead the code now keeps
track of inode numbers, and if it sees the same inode number again
during the copy loop, it hardlinks it, instead of copying it.

Signed-off-by: Sargun Dhillon <sargun@sargun.me>

Sargun Dhillon authored on 2017/11/22 03:11:43
Showing 2 changed files
... ...
@@ -106,11 +106,19 @@ func copyXattr(srcPath, dstPath, attr string) error {
106 106
 	return nil
107 107
 }
108 108
 
109
+type fileID struct {
110
+	dev uint64
111
+	ino uint64
112
+}
113
+
109 114
 // DirCopy copies or hardlinks the contents of one directory to another,
110 115
 // properly handling xattrs, and soft links
111 116
 func DirCopy(srcDir, dstDir string, copyMode Mode) error {
112 117
 	copyWithFileRange := true
113 118
 	copyWithFileClone := true
119
+	// This is a map of source file inodes to dst file paths
120
+	copiedFiles := make(map[fileID]string)
121
+
114 122
 	err := filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error {
115 123
 		if err != nil {
116 124
 			return err
... ...
@@ -136,15 +144,21 @@ func DirCopy(srcDir, dstDir string, copyMode Mode) error {
136 136
 
137 137
 		switch f.Mode() & os.ModeType {
138 138
 		case 0: // Regular file
139
+			id := fileID{dev: stat.Dev, ino: stat.Ino}
139 140
 			if copyMode == Hardlink {
140 141
 				isHardlink = true
141 142
 				if err2 := os.Link(srcPath, dstPath); err2 != nil {
142 143
 					return err2
143 144
 				}
145
+			} else if hardLinkDstPath, ok := copiedFiles[id]; ok {
146
+				if err2 := os.Link(hardLinkDstPath, dstPath); err2 != nil {
147
+					return err2
148
+				}
144 149
 			} else {
145 150
 				if err2 := copyRegular(srcPath, dstPath, f, &copyWithFileRange, &copyWithFileClone); err2 != nil {
146 151
 					return err2
147 152
 				}
153
+				copiedFiles[id] = dstPath
148 154
 			}
149 155
 
150 156
 		case os.ModeDir:
... ...
@@ -9,6 +9,8 @@ import (
9 9
 	"path/filepath"
10 10
 	"testing"
11 11
 
12
+	"golang.org/x/sys/unix"
13
+
12 14
 	"github.com/docker/docker/pkg/parsers/kernel"
13 15
 	"github.com/stretchr/testify/assert"
14 16
 	"github.com/stretchr/testify/require"
... ...
@@ -65,3 +67,32 @@ func doCopyTest(t *testing.T, copyWithFileRange, copyWithFileClone *bool) {
65 65
 	require.NoError(t, err)
66 66
 	assert.Equal(t, buf, readBuf)
67 67
 }
68
+
69
+func TestCopyHardlink(t *testing.T) {
70
+	var srcFile1FileInfo, srcFile2FileInfo, dstFile1FileInfo, dstFile2FileInfo unix.Stat_t
71
+
72
+	srcDir, err := ioutil.TempDir("", "srcDir")
73
+	require.NoError(t, err)
74
+	defer os.RemoveAll(srcDir)
75
+
76
+	dstDir, err := ioutil.TempDir("", "dstDir")
77
+	require.NoError(t, err)
78
+	defer os.RemoveAll(dstDir)
79
+
80
+	srcFile1 := filepath.Join(srcDir, "file1")
81
+	srcFile2 := filepath.Join(srcDir, "file2")
82
+	dstFile1 := filepath.Join(dstDir, "file1")
83
+	dstFile2 := filepath.Join(dstDir, "file2")
84
+	require.NoError(t, ioutil.WriteFile(srcFile1, []byte{}, 0777))
85
+	require.NoError(t, os.Link(srcFile1, srcFile2))
86
+
87
+	assert.NoError(t, DirCopy(srcDir, dstDir, Content))
88
+
89
+	require.NoError(t, unix.Stat(srcFile1, &srcFile1FileInfo))
90
+	require.NoError(t, unix.Stat(srcFile2, &srcFile2FileInfo))
91
+	require.Equal(t, srcFile1FileInfo.Ino, srcFile2FileInfo.Ino)
92
+
93
+	require.NoError(t, unix.Stat(dstFile1, &dstFile1FileInfo))
94
+	require.NoError(t, unix.Stat(dstFile2, &dstFile2FileInfo))
95
+	assert.Equal(t, dstFile1FileInfo.Ino, dstFile2FileInfo.Ino)
96
+}