Skip to content

Commit b467f8b

Browse files
author
Sargun Dhillon
committed
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>
1 parent c307e0c commit b467f8b

File tree

2 files changed

+45
-0
lines changed

2 files changed

+45
-0
lines changed

daemon/graphdriver/copy/copy.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,19 @@ func copyXattr(srcPath, dstPath, attr string) error {
106106
return nil
107107
}
108108

109+
type fileID struct {
110+
dev uint64
111+
ino uint64
112+
}
113+
109114
// DirCopy copies or hardlinks the contents of one directory to another,
110115
// properly handling xattrs, and soft links
111116
func DirCopy(srcDir, dstDir string, copyMode Mode) error {
112117
copyWithFileRange := true
113118
copyWithFileClone := true
119+
// This is a map of source file inodes to dst file paths
120+
copiedFiles := make(map[fileID]string)
121+
114122
err := filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error {
115123
if err != nil {
116124
return err
@@ -136,15 +144,21 @@ func DirCopy(srcDir, dstDir string, copyMode Mode) error {
136144

137145
switch f.Mode() & os.ModeType {
138146
case 0: // Regular file
147+
id := fileID{dev: stat.Dev, ino: stat.Ino}
139148
if copyMode == Hardlink {
140149
isHardlink = true
141150
if err2 := os.Link(srcPath, dstPath); err2 != nil {
142151
return err2
143152
}
153+
} else if hardLinkDstPath, ok := copiedFiles[id]; ok {
154+
if err2 := os.Link(hardLinkDstPath, dstPath); err2 != nil {
155+
return err2
156+
}
144157
} else {
145158
if err2 := copyRegular(srcPath, dstPath, f, &copyWithFileRange, &copyWithFileClone); err2 != nil {
146159
return err2
147160
}
161+
copiedFiles[id] = dstPath
148162
}
149163

150164
case os.ModeDir:

daemon/graphdriver/copy/copy_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"path/filepath"
1010
"testing"
1111

12+
"golang.org/x/sys/unix"
13+
1214
"github.com/docker/docker/pkg/parsers/kernel"
1315
"github.com/stretchr/testify/assert"
1416
"github.com/stretchr/testify/require"
@@ -65,3 +67,32 @@ func doCopyTest(t *testing.T, copyWithFileRange, copyWithFileClone *bool) {
6567
require.NoError(t, err)
6668
assert.Equal(t, buf, readBuf)
6769
}
70+
71+
func TestCopyHardlink(t *testing.T) {
72+
var srcFile1FileInfo, srcFile2FileInfo, dstFile1FileInfo, dstFile2FileInfo unix.Stat_t
73+
74+
srcDir, err := ioutil.TempDir("", "srcDir")
75+
require.NoError(t, err)
76+
defer os.RemoveAll(srcDir)
77+
78+
dstDir, err := ioutil.TempDir("", "dstDir")
79+
require.NoError(t, err)
80+
defer os.RemoveAll(dstDir)
81+
82+
srcFile1 := filepath.Join(srcDir, "file1")
83+
srcFile2 := filepath.Join(srcDir, "file2")
84+
dstFile1 := filepath.Join(dstDir, "file1")
85+
dstFile2 := filepath.Join(dstDir, "file2")
86+
require.NoError(t, ioutil.WriteFile(srcFile1, []byte{}, 0777))
87+
require.NoError(t, os.Link(srcFile1, srcFile2))
88+
89+
assert.NoError(t, DirCopy(srcDir, dstDir, Content))
90+
91+
require.NoError(t, unix.Stat(srcFile1, &srcFile1FileInfo))
92+
require.NoError(t, unix.Stat(srcFile2, &srcFile2FileInfo))
93+
require.Equal(t, srcFile1FileInfo.Ino, srcFile2FileInfo.Ino)
94+
95+
require.NoError(t, unix.Stat(dstFile1, &dstFile1FileInfo))
96+
require.NoError(t, unix.Stat(dstFile2, &dstFile2FileInfo))
97+
assert.Equal(t, dstFile1FileInfo.Ino, dstFile2FileInfo.Ino)
98+
}

0 commit comments

Comments
 (0)