Browse code

Fixes #9283. Consider hardlinks in image size.

Based on #8984. This patch fixes behavior when image size calculation
didn't consider hardlinks.

Signed-off-by: Dmitry Vorobev <dimahabr@gmail.com>

Dmitry Vorobev authored on 2015/10/13 04:11:22
Showing 7 changed files
... ...
@@ -249,14 +249,14 @@ func (ta *tarAppender) addTarFile(path, name string) error {
249 249
 	}
250 250
 	hdr.Name = name
251 251
 
252
-	nlink, inode, err := setHeaderForSpecialDevice(hdr, ta, name, fi.Sys())
252
+	inode, err := setHeaderForSpecialDevice(hdr, ta, name, fi.Sys())
253 253
 	if err != nil {
254 254
 		return err
255 255
 	}
256 256
 
257
-	// if it's a regular file and has more than 1 link,
257
+	// if it's not a directory and has more than 1 link,
258 258
 	// it's hardlinked, so set the type flag accordingly
259
-	if fi.Mode().IsRegular() && nlink > 1 {
259
+	if !fi.IsDir() && hasHardlinks(fi) {
260 260
 		// a link should have a name that it links too
261 261
 		// and that linked name should be first in the tar archive
262 262
 		if oldpath, ok := ta.SeenFiles[inode]; ok {
... ...
@@ -40,7 +40,7 @@ func chmodTarEntry(perm os.FileMode) os.FileMode {
40 40
 	return perm // noop for unix as golang APIs provide perm bits correctly
41 41
 }
42 42
 
43
-func setHeaderForSpecialDevice(hdr *tar.Header, ta *tarAppender, name string, stat interface{}) (nlink uint32, inode uint64, err error) {
43
+func setHeaderForSpecialDevice(hdr *tar.Header, ta *tarAppender, name string, stat interface{}) (inode uint64, err error) {
44 44
 	s, ok := stat.(*syscall.Stat_t)
45 45
 
46 46
 	if !ok {
... ...
@@ -48,7 +48,6 @@ func setHeaderForSpecialDevice(hdr *tar.Header, ta *tarAppender, name string, st
48 48
 		return
49 49
 	}
50 50
 
51
-	nlink = uint32(s.Nlink)
52 51
 	inode = uint64(s.Ino)
53 52
 
54 53
 	// Currently go does not fill in the major/minors
... ...
@@ -49,7 +49,7 @@ func chmodTarEntry(perm os.FileMode) os.FileMode {
49 49
 	return perm
50 50
 }
51 51
 
52
-func setHeaderForSpecialDevice(hdr *tar.Header, ta *tarAppender, name string, stat interface{}) (nlink uint32, inode uint64, err error) {
52
+func setHeaderForSpecialDevice(hdr *tar.Header, ta *tarAppender, name string, stat interface{}) (inode uint64, err error) {
53 53
 	// do nothing. no notion of Rdev, Inode, Nlink in stat on Windows
54 54
 	return
55 55
 }
... ...
@@ -328,13 +328,29 @@ func ChangesDirs(newDir, oldDir string) ([]Change, error) {
328 328
 
329 329
 // ChangesSize calculates the size in bytes of the provided changes, based on newDir.
330 330
 func ChangesSize(newDir string, changes []Change) int64 {
331
-	var size int64
331
+	var (
332
+		size int64
333
+		sf   = make(map[uint64]struct{})
334
+	)
332 335
 	for _, change := range changes {
333 336
 		if change.Kind == ChangeModify || change.Kind == ChangeAdd {
334 337
 			file := filepath.Join(newDir, change.Path)
335
-			fileInfo, _ := os.Lstat(file)
338
+			fileInfo, err := os.Lstat(file)
339
+			if err != nil {
340
+				logrus.Errorf("Can not stat %q: %s", file, err)
341
+				continue
342
+			}
343
+
336 344
 			if fileInfo != nil && !fileInfo.IsDir() {
337
-				size += fileInfo.Size()
345
+				if hasHardlinks(fileInfo) {
346
+					inode := getIno(fileInfo)
347
+					if _, ok := sf[inode]; !ok {
348
+						size += fileInfo.Size()
349
+						sf[inode] = struct{}{}
350
+					}
351
+				} else {
352
+					size += fileInfo.Size()
353
+				}
338 354
 			}
339 355
 		}
340 356
 	}
... ...
@@ -434,6 +434,35 @@ func TestApplyLayer(t *testing.T) {
434 434
 	}
435 435
 }
436 436
 
437
+func TestChangesSizeWithHardlinks(t *testing.T) {
438
+	srcDir, err := ioutil.TempDir("", "docker-test-srcDir")
439
+	if err != nil {
440
+		t.Fatal(err)
441
+	}
442
+	defer os.RemoveAll(srcDir)
443
+
444
+	destDir, err := ioutil.TempDir("", "docker-test-destDir")
445
+	if err != nil {
446
+		t.Fatal(err)
447
+	}
448
+	defer os.RemoveAll(destDir)
449
+
450
+	creationSize, err := prepareUntarSourceDirectory(100, destDir, true)
451
+	if err != nil {
452
+		t.Fatal(err)
453
+	}
454
+
455
+	changes, err := ChangesDirs(destDir, srcDir)
456
+	if err != nil {
457
+		t.Fatal(err)
458
+	}
459
+
460
+	got := ChangesSize(destDir, changes)
461
+	if got != int64(creationSize) {
462
+		t.Errorf("Expected %d bytes of changes, got %d", creationSize, got)
463
+	}
464
+}
465
+
437 466
 func TestChangesSizeWithNoChanges(t *testing.T) {
438 467
 	size := ChangesSize("/tmp", nil)
439 468
 	if size != 0 {
... ...
@@ -468,7 +497,7 @@ func TestChangesSize(t *testing.T) {
468 468
 	}
469 469
 	size := ChangesSize(parentPath, changes)
470 470
 	if size != 6 {
471
-		t.Fatalf("ChangesSizes with only delete changes should be 0, was %d", size)
471
+		t.Fatalf("Expected 6 bytes of changes, got %d", size)
472 472
 	}
473 473
 }
474 474
 
... ...
@@ -3,6 +3,7 @@
3 3
 package archive
4 4
 
5 5
 import (
6
+	"os"
6 7
 	"syscall"
7 8
 
8 9
 	"github.com/docker/docker/pkg/system"
... ...
@@ -25,3 +26,11 @@ func statDifferent(oldStat *system.StatT, newStat *system.StatT) bool {
25 25
 func (info *FileInfo) isDir() bool {
26 26
 	return info.parent == nil || info.stat.Mode()&syscall.S_IFDIR != 0
27 27
 }
28
+
29
+func getIno(fi os.FileInfo) uint64 {
30
+	return uint64(fi.Sys().(*syscall.Stat_t).Ino)
31
+}
32
+
33
+func hasHardlinks(fi os.FileInfo) bool {
34
+	return fi.Sys().(*syscall.Stat_t).Nlink > 1
35
+}
... ...
@@ -1,6 +1,8 @@
1 1
 package archive
2 2
 
3 3
 import (
4
+	"os"
5
+
4 6
 	"github.com/docker/docker/pkg/system"
5 7
 )
6 8
 
... ...
@@ -18,3 +20,11 @@ func statDifferent(oldStat *system.StatT, newStat *system.StatT) bool {
18 18
 func (info *FileInfo) isDir() bool {
19 19
 	return info.parent == nil || info.stat.IsDir()
20 20
 }
21
+
22
+func getIno(fi os.FileInfo) (inode uint64) {
23
+	return
24
+}
25
+
26
+func hasHardlinks(fi os.FileInfo) bool {
27
+	return false
28
+}