311 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			311 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package fs
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"golang.org/x/sync/errgroup"
 | 
						|
 | 
						|
	"github.com/sirupsen/logrus"
 | 
						|
)
 | 
						|
 | 
						|
// ChangeKind is the type of modification that
 | 
						|
// a change is making.
 | 
						|
type ChangeKind int
 | 
						|
 | 
						|
const (
 | 
						|
	// ChangeKindUnmodified represents an unmodified
 | 
						|
	// file
 | 
						|
	ChangeKindUnmodified = iota
 | 
						|
 | 
						|
	// ChangeKindAdd represents an addition of
 | 
						|
	// a file
 | 
						|
	ChangeKindAdd
 | 
						|
 | 
						|
	// ChangeKindModify represents a change to
 | 
						|
	// an existing file
 | 
						|
	ChangeKindModify
 | 
						|
 | 
						|
	// ChangeKindDelete represents a delete of
 | 
						|
	// a file
 | 
						|
	ChangeKindDelete
 | 
						|
)
 | 
						|
 | 
						|
func (k ChangeKind) String() string {
 | 
						|
	switch k {
 | 
						|
	case ChangeKindUnmodified:
 | 
						|
		return "unmodified"
 | 
						|
	case ChangeKindAdd:
 | 
						|
		return "add"
 | 
						|
	case ChangeKindModify:
 | 
						|
		return "modify"
 | 
						|
	case ChangeKindDelete:
 | 
						|
		return "delete"
 | 
						|
	default:
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Change represents single change between a diff and its parent.
 | 
						|
type Change struct {
 | 
						|
	Kind ChangeKind
 | 
						|
	Path string
 | 
						|
}
 | 
						|
 | 
						|
// ChangeFunc is the type of function called for each change
 | 
						|
// computed during a directory changes calculation.
 | 
						|
type ChangeFunc func(ChangeKind, string, os.FileInfo, error) error
 | 
						|
 | 
						|
// Changes computes changes between two directories calling the
 | 
						|
// given change function for each computed change. The first
 | 
						|
// directory is intended to the base directory and second
 | 
						|
// directory the changed directory.
 | 
						|
//
 | 
						|
// The change callback is called by the order of path names and
 | 
						|
// should be appliable in that order.
 | 
						|
//  Due to this apply ordering, the following is true
 | 
						|
//  - Removed directory trees only create a single change for the root
 | 
						|
//    directory removed. Remaining changes are implied.
 | 
						|
//  - A directory which is modified to become a file will not have
 | 
						|
//    delete entries for sub-path items, their removal is implied
 | 
						|
//    by the removal of the parent directory.
 | 
						|
//
 | 
						|
// Opaque directories will not be treated specially and each file
 | 
						|
// removed from the base directory will show up as a removal.
 | 
						|
//
 | 
						|
// File content comparisons will be done on files which have timestamps
 | 
						|
// which may have been truncated. If either of the files being compared
 | 
						|
// has a zero value nanosecond value, each byte will be compared for
 | 
						|
// differences. If 2 files have the same seconds value but different
 | 
						|
// nanosecond values where one of those values is zero, the files will
 | 
						|
// be considered unchanged if the content is the same. This behavior
 | 
						|
// is to account for timestamp truncation during archiving.
 | 
						|
func Changes(ctx context.Context, a, b string, changeFn ChangeFunc) error {
 | 
						|
	if a == "" {
 | 
						|
		logrus.Debugf("Using single walk diff for %s", b)
 | 
						|
		return addDirChanges(ctx, changeFn, b)
 | 
						|
	} else if diffOptions := detectDirDiff(b, a); diffOptions != nil {
 | 
						|
		logrus.Debugf("Using single walk diff for %s from %s", diffOptions.diffDir, a)
 | 
						|
		return diffDirChanges(ctx, changeFn, a, diffOptions)
 | 
						|
	}
 | 
						|
 | 
						|
	logrus.Debugf("Using double walk diff for %s from %s", b, a)
 | 
						|
	return doubleWalkDiff(ctx, changeFn, a, b)
 | 
						|
}
 | 
						|
 | 
						|
func addDirChanges(ctx context.Context, changeFn ChangeFunc, root string) error {
 | 
						|
	return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		// Rebase path
 | 
						|
		path, err = filepath.Rel(root, path)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		path = filepath.Join(string(os.PathSeparator), path)
 | 
						|
 | 
						|
		// Skip root
 | 
						|
		if path == string(os.PathSeparator) {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
 | 
						|
		return changeFn(ChangeKindAdd, path, f, nil)
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// diffDirOptions is used when the diff can be directly calculated from
 | 
						|
// a diff directory to its base, without walking both trees.
 | 
						|
type diffDirOptions struct {
 | 
						|
	diffDir      string
 | 
						|
	skipChange   func(string) (bool, error)
 | 
						|
	deleteChange func(string, string, os.FileInfo) (string, error)
 | 
						|
}
 | 
						|
 | 
						|
// diffDirChanges walks the diff directory and compares changes against the base.
 | 
						|
func diffDirChanges(ctx context.Context, changeFn ChangeFunc, base string, o *diffDirOptions) error {
 | 
						|
	changedDirs := make(map[string]struct{})
 | 
						|
	return filepath.Walk(o.diffDir, func(path string, f os.FileInfo, err error) error {
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		// Rebase path
 | 
						|
		path, err = filepath.Rel(o.diffDir, path)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		path = filepath.Join(string(os.PathSeparator), path)
 | 
						|
 | 
						|
		// Skip root
 | 
						|
		if path == string(os.PathSeparator) {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
 | 
						|
		// TODO: handle opaqueness, start new double walker at this
 | 
						|
		// location to get deletes, and skip tree in single walker
 | 
						|
 | 
						|
		if o.skipChange != nil {
 | 
						|
			if skip, err := o.skipChange(path); skip {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		var kind ChangeKind
 | 
						|
 | 
						|
		deletedFile, err := o.deleteChange(o.diffDir, path, f)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		// Find out what kind of modification happened
 | 
						|
		if deletedFile != "" {
 | 
						|
			path = deletedFile
 | 
						|
			kind = ChangeKindDelete
 | 
						|
			f = nil
 | 
						|
		} else {
 | 
						|
			// Otherwise, the file was added
 | 
						|
			kind = ChangeKindAdd
 | 
						|
 | 
						|
			// ...Unless it already existed in a base, in which case, it's a modification
 | 
						|
			stat, err := os.Stat(filepath.Join(base, path))
 | 
						|
			if err != nil && !os.IsNotExist(err) {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
			if err == nil {
 | 
						|
				// The file existed in the base, so that's a modification
 | 
						|
 | 
						|
				// However, if it's a directory, maybe it wasn't actually modified.
 | 
						|
				// If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar
 | 
						|
				if stat.IsDir() && f.IsDir() {
 | 
						|
					if f.Size() == stat.Size() && f.Mode() == stat.Mode() && sameFsTime(f.ModTime(), stat.ModTime()) {
 | 
						|
						// Both directories are the same, don't record the change
 | 
						|
						return nil
 | 
						|
					}
 | 
						|
				}
 | 
						|
				kind = ChangeKindModify
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// If /foo/bar/file.txt is modified, then /foo/bar must be part of the changed files.
 | 
						|
		// This block is here to ensure the change is recorded even if the
 | 
						|
		// modify time, mode and size of the parent directory in the rw and ro layers are all equal.
 | 
						|
		// Check https://github.com/docker/docker/pull/13590 for details.
 | 
						|
		if f.IsDir() {
 | 
						|
			changedDirs[path] = struct{}{}
 | 
						|
		}
 | 
						|
		if kind == ChangeKindAdd || kind == ChangeKindDelete {
 | 
						|
			parent := filepath.Dir(path)
 | 
						|
			if _, ok := changedDirs[parent]; !ok && parent != "/" {
 | 
						|
				pi, err := os.Stat(filepath.Join(o.diffDir, parent))
 | 
						|
				if err := changeFn(ChangeKindModify, parent, pi, err); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
				changedDirs[parent] = struct{}{}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return changeFn(kind, path, f, nil)
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// doubleWalkDiff walks both directories to create a diff
 | 
						|
func doubleWalkDiff(ctx context.Context, changeFn ChangeFunc, a, b string) (err error) {
 | 
						|
	g, ctx := errgroup.WithContext(ctx)
 | 
						|
 | 
						|
	var (
 | 
						|
		c1 = make(chan *currentPath)
 | 
						|
		c2 = make(chan *currentPath)
 | 
						|
 | 
						|
		f1, f2 *currentPath
 | 
						|
		rmdir  string
 | 
						|
	)
 | 
						|
	g.Go(func() error {
 | 
						|
		defer close(c1)
 | 
						|
		return pathWalk(ctx, a, c1)
 | 
						|
	})
 | 
						|
	g.Go(func() error {
 | 
						|
		defer close(c2)
 | 
						|
		return pathWalk(ctx, b, c2)
 | 
						|
	})
 | 
						|
	g.Go(func() error {
 | 
						|
		for c1 != nil || c2 != nil {
 | 
						|
			if f1 == nil && c1 != nil {
 | 
						|
				f1, err = nextPath(ctx, c1)
 | 
						|
				if err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
				if f1 == nil {
 | 
						|
					c1 = nil
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			if f2 == nil && c2 != nil {
 | 
						|
				f2, err = nextPath(ctx, c2)
 | 
						|
				if err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
				if f2 == nil {
 | 
						|
					c2 = nil
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if f1 == nil && f2 == nil {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			var f os.FileInfo
 | 
						|
			k, p := pathChange(f1, f2)
 | 
						|
			switch k {
 | 
						|
			case ChangeKindAdd:
 | 
						|
				if rmdir != "" {
 | 
						|
					rmdir = ""
 | 
						|
				}
 | 
						|
				f = f2.f
 | 
						|
				f2 = nil
 | 
						|
			case ChangeKindDelete:
 | 
						|
				// Check if this file is already removed by being
 | 
						|
				// under of a removed directory
 | 
						|
				if rmdir != "" && strings.HasPrefix(f1.path, rmdir) {
 | 
						|
					f1 = nil
 | 
						|
					continue
 | 
						|
				} else if rmdir == "" && f1.f.IsDir() {
 | 
						|
					rmdir = f1.path + string(os.PathSeparator)
 | 
						|
				} else if rmdir != "" {
 | 
						|
					rmdir = ""
 | 
						|
				}
 | 
						|
				f1 = nil
 | 
						|
			case ChangeKindModify:
 | 
						|
				same, err := sameFile(f1, f2)
 | 
						|
				if err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
				if f1.f.IsDir() && !f2.f.IsDir() {
 | 
						|
					rmdir = f1.path + string(os.PathSeparator)
 | 
						|
				} else if rmdir != "" {
 | 
						|
					rmdir = ""
 | 
						|
				}
 | 
						|
				f = f2.f
 | 
						|
				f1 = nil
 | 
						|
				f2 = nil
 | 
						|
				if same {
 | 
						|
					if !isLinked(f) {
 | 
						|
						continue
 | 
						|
					}
 | 
						|
					k = ChangeKindUnmodified
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if err := changeFn(k, p, f, nil); err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
 | 
						|
	return g.Wait()
 | 
						|
}
 |