changes.go
ef711962
 package docker
33f6a0aa
 
 import (
 	"fmt"
 	"os"
 	"path/filepath"
 	"strings"
8e8ef7cb
 	"syscall"
33f6a0aa
 )
 
 type ChangeType int
 
 const (
 	ChangeModify = iota
 	ChangeAdd
 	ChangeDelete
 )
 
 type Change struct {
 	Path string
 	Kind ChangeType
 }
 
 func (change *Change) String() string {
 	var kind string
 	switch change.Kind {
 	case ChangeModify:
 		kind = "C"
 	case ChangeAdd:
 		kind = "A"
 	case ChangeDelete:
 		kind = "D"
 	}
 	return fmt.Sprintf("%s %s", kind, change.Path)
 }
 
02b5f136
 type FileInfo struct {
ebfa24ac
 	parent   *FileInfo
 	name     string
 	stat     syscall.Stat_t
02b5f136
 	children map[string]*FileInfo
 }
8e8ef7cb
 
02b5f136
 func (root *FileInfo) LookUp(path string) *FileInfo {
 	parent := root
 	if path == "/" {
 		return root
 	}
8e8ef7cb
 
02b5f136
 	pathElements := strings.Split(path, "/")
 	for _, elem := range pathElements {
 		if elem != "" {
 			child := parent.children[elem]
 			if child == nil {
 				return nil
 			}
 			parent = child
8e8ef7cb
 		}
02b5f136
 	}
 	return parent
 }
8e8ef7cb
 
ebfa24ac
 func (info *FileInfo) path() string {
02b5f136
 	if info.parent == nil {
 		return "/"
 	}
 	return filepath.Join(info.parent.path(), info.name)
 }
 
ebfa24ac
 func (info *FileInfo) unlink() {
02b5f136
 	if info.parent != nil {
 		delete(info.parent.children, info.name)
 	}
 }
 
ebfa24ac
 func (info *FileInfo) Remove(path string) bool {
02b5f136
 	child := info.LookUp(path)
 	if child != nil {
 		child.unlink()
 		return true
 	}
 	return false
 }
 
ebfa24ac
 func (info *FileInfo) isDir() bool {
02b5f136
 	return info.parent == nil || info.stat.Mode&syscall.S_IFDIR == syscall.S_IFDIR
 }
8e8ef7cb
 
ebfa24ac
 func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) {
02b5f136
 	if oldInfo == nil {
 		// add
8e8ef7cb
 		change := Change{
02b5f136
 			Path: info.path(),
 			Kind: ChangeAdd,
8e8ef7cb
 		}
02b5f136
 		*changes = append(*changes, change)
 	}
8e8ef7cb
 
02b5f136
 	// We make a copy so we can modify it to detect additions
 	// also, we only recurse on the old dir if the new info is a directory
 	// otherwise any previous delete/change is considered recursive
 	oldChildren := make(map[string]*FileInfo)
 	if oldInfo != nil && info.isDir() {
 		for k, v := range oldInfo.children {
 			oldChildren[k] = v
8e8ef7cb
 		}
02b5f136
 	}
8e8ef7cb
 
02b5f136
 	for name, newChild := range info.children {
 		oldChild, _ := oldChildren[name]
 		if oldChild != nil {
 			// change?
 			oldStat := &oldChild.stat
 			newStat := &newChild.stat
36603e68
 			// Note: We can't compare inode or ctime or blocksize here, because these change
 			// when copying a file into a container. However, that is not generally a problem
 			// because any content change will change mtime, and any status change should
 			// be visible when actually comparing the stat fields. The only time this
 			// breaks down is if some code intentionally hides a change by setting
 			// back mtime
 			oldMtime := syscall.NsecToTimeval(oldStat.Mtim.Nano())
 			newMtime := syscall.NsecToTimeval(oldStat.Mtim.Nano())
 			if oldStat.Mode != newStat.Mode ||
8e8ef7cb
 				oldStat.Uid != newStat.Uid ||
 				oldStat.Gid != newStat.Gid ||
 				oldStat.Rdev != newStat.Rdev ||
36603e68
 				// Don't look at size for dirs, its not a good measure of change
ebfa24ac
 				(oldStat.Size != newStat.Size && oldStat.Mode&syscall.S_IFDIR != syscall.S_IFDIR) ||
36603e68
 				oldMtime.Sec != newMtime.Sec ||
 				oldMtime.Usec != newMtime.Usec {
02b5f136
 				change := Change{
 					Path: newChild.path(),
 					Kind: ChangeModify,
 				}
 				*changes = append(*changes, change)
8e8ef7cb
 			}
02b5f136
 
 			// Remove from copy so we can detect deletions
 			delete(oldChildren, name)
8e8ef7cb
 		}
 
02b5f136
 		newChild.addChanges(oldChild, changes)
 	}
 	for _, oldChild := range oldChildren {
 		// delete
 		change := Change{
 			Path: oldChild.path(),
 			Kind: ChangeDelete,
 		}
 		*changes = append(*changes, change)
 	}
 
 }
 
ebfa24ac
 func (info *FileInfo) Changes(oldInfo *FileInfo) []Change {
02b5f136
 	var changes []Change
 
 	info.addChanges(oldInfo, &changes)
 
 	return changes
 }
 
ad0a6a03
 func newRootFileInfo() *FileInfo {
ebfa24ac
 	root := &FileInfo{
 		name:     "/",
02b5f136
 		children: make(map[string]*FileInfo),
8e8ef7cb
 	}
ad0a6a03
 	return root
 }
 
 func applyLayer(root *FileInfo, layer string) error {
 	err := filepath.Walk(layer, func(layerPath string, f os.FileInfo, err error) error {
 		if err != nil {
 			return err
 		}
 
 		// Skip root
 		if layerPath == layer {
 			return nil
 		}
 
 		// rebase path
 		relPath, err := filepath.Rel(layer, layerPath)
 		if err != nil {
 			return err
 		}
 		relPath = filepath.Join("/", relPath)
 
 		// Skip AUFS metadata
 		if matched, err := filepath.Match("/.wh..wh.*", relPath); err != nil || matched {
 			if err != nil || !f.IsDir() {
 				return err
 			}
 			return filepath.SkipDir
 		}
 
 		var layerStat syscall.Stat_t
 		err = syscall.Lstat(layerPath, &layerStat)
 		if err != nil {
 			return err
 		}
 
 		file := filepath.Base(relPath)
 		// If there is a whiteout, then the file was removed
 		if strings.HasPrefix(file, ".wh.") {
 			originalFile := file[len(".wh."):]
 			deletePath := filepath.Join(filepath.Dir(relPath), originalFile)
 
 			root.Remove(deletePath)
 		} else {
 			// Added or changed file
 			existing := root.LookUp(relPath)
 			if existing != nil {
 				// Changed file
 				existing.stat = layerStat
 				if !existing.isDir() {
 					// Changed from dir to non-dir, delete all previous files
 					existing.children = make(map[string]*FileInfo)
 				}
 			} else {
 				// Added file
 				parent := root.LookUp(filepath.Dir(relPath))
 				if parent == nil {
 					return fmt.Errorf("collectFileInfo: Unexpectedly no parent for %s", relPath)
 				}
 
ebfa24ac
 				info := &FileInfo{
 					name:     filepath.Base(relPath),
ad0a6a03
 					children: make(map[string]*FileInfo),
ebfa24ac
 					parent:   parent,
 					stat:     layerStat,
ad0a6a03
 				}
 
 				parent.children[info.name] = info
 			}
 		}
 		return nil
 	})
 	return err
 }
 
 func collectFileInfo(sourceDir string) (*FileInfo, error) {
 	root := newRootFileInfo()
02b5f136
 
 	err := filepath.Walk(sourceDir, func(path string, f os.FileInfo, err error) error {
8e8ef7cb
 		if err != nil {
 			return err
 		}
 
 		// Rebase path
02b5f136
 		relPath, err := filepath.Rel(sourceDir, path)
8e8ef7cb
 		if err != nil {
 			return err
 		}
 		relPath = filepath.Join("/", relPath)
 
 		if relPath == "/" {
 			return nil
 		}
 
02b5f136
 		parent := root.LookUp(filepath.Dir(relPath))
 		if parent == nil {
 			return fmt.Errorf("collectFileInfo: Unexpectedly no parent for %s", relPath)
8e8ef7cb
 		}
 
ebfa24ac
 		info := &FileInfo{
 			name:     filepath.Base(relPath),
02b5f136
 			children: make(map[string]*FileInfo),
ebfa24ac
 			parent:   parent,
02b5f136
 		}
8e8ef7cb
 
02b5f136
 		if err := syscall.Lstat(path, &info.stat); err != nil {
 			return err
8e8ef7cb
 		}
 
02b5f136
 		parent.children[info.name] = info
 
8e8ef7cb
 		return nil
 	})
 	if err != nil {
 		return nil, err
 	}
02b5f136
 	return root, nil
 }
 
8e4b3a33
 // Compare a directory with an array of layer directories it was based on and
 // generate an array of Change objects describing the changes
ad0a6a03
 func ChangesLayers(newDir string, layers []string) ([]Change, error) {
 	newRoot, err := collectFileInfo(newDir)
 	if err != nil {
 		return nil, err
 	}
 	oldRoot := newRootFileInfo()
ebfa24ac
 	for i := len(layers) - 1; i >= 0; i-- {
ad0a6a03
 		layer := layers[i]
 		if err = applyLayer(oldRoot, layer); err != nil {
 			return nil, err
 		}
 	}
 
 	return newRoot.Changes(oldRoot), nil
 }
 
8e4b3a33
 // Compare two directories and generate an array of Change objects describing the changes
02b5f136
 func ChangesDirs(newDir, oldDir string) ([]Change, error) {
 	oldRoot, err := collectFileInfo(oldDir)
 	if err != nil {
 		return nil, err
 	}
 	newRoot, err := collectFileInfo(newDir)
 	if err != nil {
 		return nil, err
 	}
 
 	return newRoot.Changes(oldRoot), nil
8e8ef7cb
 }