mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
320 lines
7 KiB
Go
320 lines
7 KiB
Go
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
|
|
}
|
|
|
|
// Compare a directory with an array of layer directories it was based on and
|
|
// generate an array of Change objects describing the changes
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Ignore changes in .docker-id
|
|
_ = newRoot.Remove("/.docker-id")
|
|
_ = oldRoot.Remove("/.docker-id")
|
|
|
|
return newRoot.Changes(oldRoot), nil
|
|
}
|