| package docker |
| |
| import ( |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "syscall" |
| ) |
| |
| 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) |
| } |
| |
| type FileInfo struct { |
| parent *FileInfo |
| name string |
| stat syscall.Stat_t |
| children map[string]*FileInfo |
| } |
| |
| func (root *FileInfo) LookUp(path string) *FileInfo { |
| parent := root |
| if path == "/" { |
| return root |
| } |
| |
| pathElements := strings.Split(path, "/") |
| for _, elem := range pathElements { |
| if elem != "" { |
| child := parent.children[elem] |
| if child == nil { |
| return nil |
| } |
| parent = child |
| } |
| } |
| return parent |
| } |
| |
| func (info *FileInfo) path() string { |
| if info.parent == nil { |
| return "/" |
| } |
| return filepath.Join(info.parent.path(), info.name) |
| } |
| |
| func (info *FileInfo) unlink() { |
| if info.parent != nil { |
| delete(info.parent.children, info.name) |
| } |
| } |
| |
| func (info *FileInfo) Remove(path string) bool { |
| child := info.LookUp(path) |
| if child != nil { |
| child.unlink() |
| return true |
| } |
| return false |
| } |
| |
| 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: info.path(), |
| Kind: ChangeAdd, |
| } |
| *changes = append(*changes, change) |
| } |
| |
| // 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 |
| } |
| } |
| |
| 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 |
| oldMtime := syscall.NsecToTimeval(oldStat.Mtim.Nano()) |
| newMtime := syscall.NsecToTimeval(oldStat.Mtim.Nano()) |
| if oldStat.Mode != newStat.Mode || |
| oldStat.Uid != newStat.Uid || |
| oldStat.Gid != newStat.Gid || |
| oldStat.Rdev != newStat.Rdev || |
| // 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) || |
| oldMtime.Sec != newMtime.Sec || |
| oldMtime.Usec != newMtime.Usec { |
| change := Change{ |
| Path: newChild.path(), |
| Kind: ChangeModify, |
| } |
| *changes = append(*changes, change) |
| } |
| |
| // Remove from copy so we can detect deletions |
| delete(oldChildren, name) |
| } |
| |
| newChild.addChanges(oldChild, changes) |
| } |
| 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 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) |
| } |
| |
| info := &FileInfo{ |
| name: filepath.Base(relPath), |
| children: make(map[string]*FileInfo), |
| parent: parent, |
| stat: layerStat, |
| } |
| |
| parent.children[info.name] = info |
| } |
| } |
| return nil |
| }) |
| return err |
| } |
| |
| 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(sourceDir, path) |
| if err != nil { |
| return err |
| } |
| relPath = filepath.Join("/", relPath) |
| |
| if relPath == "/" { |
| return nil |
| } |
| |
| parent := root.LookUp(filepath.Dir(relPath)) |
| if parent == nil { |
| return fmt.Errorf("collectFileInfo: Unexpectedly no parent for %s", relPath) |
| } |
| |
| info := &FileInfo{ |
| name: filepath.Base(relPath), |
| children: make(map[string]*FileInfo), |
| parent: parent, |
| } |
| |
| 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 root, nil |
| } |
| |
| func ChangesLayers(newDir string, layers []string) ([]Change, error) { |
| newRoot, err := collectFileInfo(newDir) |
| if err != nil { |
| return nil, err |
| } |
| oldRoot := newRootFileInfo() |
| for i := len(layers) - 1; i >= 0; i-- { |
| layer := layers[i] |
| if err = applyLayer(oldRoot, layer); err != nil { |
| return nil, err |
| } |
| } |
| |
| return newRoot.Changes(oldRoot), nil |
| } |
| |
| 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 |
| } |
| |
| // Ignore changes in .docker-id |
| _ = newRoot.Remove("/.docker-id") |
| _ = oldRoot.Remove("/.docker-id") |
| |
| return newRoot.Changes(oldRoot), nil |
| } |