Skip to content

Commit

Permalink
archive.ChagesDir() - faster and better implementation
Browse files Browse the repository at this point in the history
This replaces the current version with the latest version from the
dm branch. Advantages in this version:

We only scan each directory tree once, which means less i/o and less chance
of container changes inbetween the two scans causing inconsistencies.

We avoid comparing some fields for change detection:

* Inode
* size-in-blocks
  These can change during a copy operation (e.g. in the dummy backend)
  without needing to actually reflect a change in content or metadata.

* Ctime
  Any copy operation will create a new Ctime value, and there is no
  API to change it to the "source" value.

* size for directories
  The size of a directory doesn't have to be the same just because you
  recreated the same content as another director. Internal details in
  the filesystem may make these different with no "real" change.
  • Loading branch information
alexlarsson committed Nov 11, 2013
1 parent 948bb29 commit b6ef4bc
Showing 1 changed file with 130 additions and 58 deletions.
188 changes: 130 additions & 58 deletions archive/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,107 +106,179 @@ func Changes(layers []string, rw string) ([]Change, error) {
return changes, nil
}

func ChangesDirs(newDir, oldDir string) ([]Change, error) {
var changes []Change
err := filepath.Walk(newDir, func(newPath string, f os.FileInfo, err error) error {
if err != nil {
return err
}
type FileInfo struct {
parent *FileInfo
name string
stat syscall.Stat_t
children map[string]*FileInfo
}

var newStat syscall.Stat_t
err = syscall.Lstat(newPath, &newStat)
if err != nil {
return err
}
func (root *FileInfo) LookUp(path string) *FileInfo {
parent := root
if path == "/" {
return root
}

// Rebase path
relPath, err := filepath.Rel(newDir, newPath)
if err != nil {
return err
pathElements := strings.Split(path, "/")
for _, elem := range pathElements {
if elem != "" {
child := parent.children[elem]
if child == nil {
return nil
}
parent = child
}
relPath = filepath.Join("/", relPath)
}
return parent
}

// Skip root
if relPath == "/" || relPath == "/.docker-id" {
return nil
}
func (info *FileInfo) path() string {
if info.parent == nil {
return "/"
}
return filepath.Join(info.parent.path(), info.name)
}

func (info *FileInfo) isDir() bool {
return info.parent == nil || info.stat.Mode&syscall.S_IFDIR == syscall.S_IFDIR
}

func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) {
if oldInfo == nil {
// add
change := Change{
Path: relPath,
Path: info.path(),
Kind: ChangeAdd,
}
*changes = append(*changes, change)
}

oldPath := filepath.Join(oldDir, relPath)

var oldStat = &syscall.Stat_t{}
err = syscall.Lstat(oldPath, oldStat)
if err != nil {
if !os.IsNotExist(err) {
return err
}
oldStat = nil
// 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
}
}

if oldStat == nil {
change.Kind = ChangeAdd
changes = append(changes, change)
} else {
if oldStat.Ino != newStat.Ino ||
oldStat.Mode != newStat.Mode ||
for name, newChild := range info.children {
oldChild, _ := oldChildren[name]
if oldChild != nil {
// change?
oldStat := &oldChild.stat
newStat := &newChild.stat
// 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
if oldStat.Mode != newStat.Mode ||
oldStat.Uid != newStat.Uid ||
oldStat.Gid != newStat.Gid ||
oldStat.Rdev != newStat.Rdev ||
oldStat.Size != newStat.Size ||
oldStat.Blocks != newStat.Blocks ||
oldStat.Mtim != newStat.Mtim ||
oldStat.Ctim != newStat.Ctim {
change.Kind = ChangeModify
changes = append(changes, change)
// Don't look at size for dirs, its not a good measure of change
(oldStat.Size != newStat.Size && oldStat.Mode&syscall.S_IFDIR != syscall.S_IFDIR) ||
oldStat.Mtim != newStat.Mtim {
change := Change{
Path: newChild.path(),
Kind: ChangeModify,
}
*changes = append(*changes, change)
}

// Remove from copy so we can detect deletions
delete(oldChildren, name)
}

return nil
})
if err != nil {
return nil, err
newChild.addChanges(oldChild, changes)
}
err = filepath.Walk(oldDir, func(oldPath string, f os.FileInfo, err error) error {
for _, oldChild := range oldChildren {
// delete
change := Change{
Path: oldChild.path(),
Kind: ChangeDelete,
}
*changes = append(*changes, change)
}

}

func (info *FileInfo) Changes(oldInfo *FileInfo) []Change {
var changes []Change

info.addChanges(oldInfo, &changes)

return changes
}

func newRootFileInfo() *FileInfo {
root := &FileInfo{
name: "/",
children: make(map[string]*FileInfo),
}
return root
}

func collectFileInfo(sourceDir string) (*FileInfo, error) {
root := newRootFileInfo()

err := filepath.Walk(sourceDir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}

// Rebase path
relPath, err := filepath.Rel(oldDir, oldPath)
relPath, err := filepath.Rel(sourceDir, path)
if err != nil {
return err
}
relPath = filepath.Join("/", relPath)

// Skip root
if relPath == "/" {
return nil
}

change := Change{
Path: relPath,
parent := root.LookUp(filepath.Dir(relPath))
if parent == nil {
return fmt.Errorf("collectFileInfo: Unexpectedly no parent for %s", relPath)
}

newPath := filepath.Join(newDir, relPath)
info := &FileInfo{
name: filepath.Base(relPath),
children: make(map[string]*FileInfo),
parent: parent,
}

var newStat = &syscall.Stat_t{}
err = syscall.Lstat(newPath, newStat)
if err != nil && os.IsNotExist(err) {
change.Kind = ChangeDelete
changes = append(changes, change)
if err := syscall.Lstat(path, &info.stat); err != nil {
return err
}

parent.children[info.name] = info

return nil
})
if err != nil {
return nil, err
}
return changes, nil
return root, nil
}

// Compare two directories and generate an array of Change objects describing the changes
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
}

func ExportChanges(root, rw string) (Archive, error) {
changes, err := ChangesDirs(root, rw)
Expand Down

0 comments on commit b6ef4bc

Please sign in to comment.