Add fs package
Add diff comparison with support for double walking two trees for comparison or single walking a diff tree. Single walking requires further implementation for specific mount types. Add directory copy function which is intended to provide fastest possible local copy of file system directories without hardlinking. Add test package to make creating filesystems for test easy and comparisons deep and informative. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
parent
5f08e609c0
commit
574862fd89
16 changed files with 1672 additions and 0 deletions
360
fs/diff.go
Normal file
360
fs/diff.go
Normal file
|
@ -0,0 +1,360 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ChangeKind is the type of modification that
|
||||
// a change is making.
|
||||
type ChangeKind int
|
||||
|
||||
const (
|
||||
// ChangeKindAdd represents an addition of
|
||||
// a file
|
||||
ChangeKindAdd = iota
|
||||
|
||||
// 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 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
|
||||
FileInfo os.FileInfo
|
||||
Source string
|
||||
}
|
||||
|
||||
// Changes returns a stream of changes between the provided upper
|
||||
// directory and lower directory.
|
||||
//
|
||||
// Changes are ordered by name and should be appliable in the
|
||||
// order in which they received.
|
||||
// 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 lower 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, upper, lower string) (context.Context, <-chan Change) {
|
||||
var (
|
||||
changes = make(chan Change)
|
||||
retCtx, cancel = context.WithCancel(ctx)
|
||||
)
|
||||
|
||||
cc := &changeContext{
|
||||
Context: retCtx,
|
||||
}
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
if lower == "" {
|
||||
logrus.Debugf("Using single walk diff for %s", upper)
|
||||
err = addDirChanges(ctx, changes, upper)
|
||||
} else if diffOptions := detectDirDiff(upper, lower); diffOptions != nil {
|
||||
logrus.Debugf("Using single walk diff for %s from %s", diffOptions.diffDir, lower)
|
||||
err = diffDirChanges(ctx, changes, lower, diffOptions)
|
||||
} else {
|
||||
logrus.Debugf("Using double walk diff for %s from %s", upper, lower)
|
||||
err = doubleWalkDiff(ctx, changes, upper, lower)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
cc.errL.Lock()
|
||||
cc.err = err
|
||||
cc.errL.Unlock()
|
||||
cancel()
|
||||
}
|
||||
defer close(changes)
|
||||
}()
|
||||
|
||||
return cc, changes
|
||||
}
|
||||
|
||||
// changeContext wraps a context to allow setting an error
|
||||
// directly from a change streamer to allow streams canceled
|
||||
// due to errors to propagate the error to the caller.
|
||||
type changeContext struct {
|
||||
context.Context
|
||||
|
||||
err error
|
||||
errL sync.Mutex
|
||||
}
|
||||
|
||||
func (cc *changeContext) Err() error {
|
||||
cc.errL.Lock()
|
||||
if cc.err != nil {
|
||||
return cc.err
|
||||
}
|
||||
cc.errL.Unlock()
|
||||
return cc.Context.Err()
|
||||
}
|
||||
|
||||
func sendChange(ctx context.Context, changes chan<- Change, change Change) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case changes <- change:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func addDirChanges(ctx context.Context, changes chan<- Change, 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
|
||||
}
|
||||
|
||||
change := Change{
|
||||
Path: path,
|
||||
Kind: ChangeKindAdd,
|
||||
FileInfo: f,
|
||||
Source: filepath.Join(root, path),
|
||||
}
|
||||
|
||||
return sendChange(ctx, changes, change)
|
||||
})
|
||||
}
|
||||
|
||||
// diffDirOptions is used when the diff can be directly calculated from
|
||||
// a diff directory to its lower, 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 lower.
|
||||
func diffDirChanges(ctx context.Context, changes chan<- Change, lower 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
|
||||
}
|
||||
}
|
||||
|
||||
change := Change{
|
||||
Path: path,
|
||||
}
|
||||
|
||||
deletedFile, err := o.deleteChange(o.diffDir, path, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find out what kind of modification happened
|
||||
if deletedFile != "" {
|
||||
change.Path = deletedFile
|
||||
change.Kind = ChangeKindDelete
|
||||
} else {
|
||||
// Otherwise, the file was added
|
||||
change.Kind = ChangeKindAdd
|
||||
change.FileInfo = f
|
||||
change.Source = filepath.Join(o.diffDir, path)
|
||||
|
||||
// ...Unless it already existed in a lower, in which case, it's a modification
|
||||
stat, err := os.Stat(filepath.Join(lower, path))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
// The file existed in the lower, 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
|
||||
}
|
||||
}
|
||||
change.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 change.Kind == ChangeKindAdd || change.Kind == ChangeKindDelete {
|
||||
parent := filepath.Dir(path)
|
||||
if _, ok := changedDirs[parent]; !ok && parent != "/" {
|
||||
pi, err := os.Stat(filepath.Join(o.diffDir, parent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dirChange := Change{
|
||||
Path: parent,
|
||||
Kind: ChangeKindModify,
|
||||
FileInfo: pi,
|
||||
Source: filepath.Join(o.diffDir, parent),
|
||||
}
|
||||
if err := sendChange(ctx, changes, dirChange); err != nil {
|
||||
return err
|
||||
}
|
||||
changedDirs[parent] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return sendChange(ctx, changes, change)
|
||||
})
|
||||
}
|
||||
|
||||
// doubleWalkDiff walks both directories to create a diff
|
||||
func doubleWalkDiff(ctx context.Context, changes chan<- Change, upper, lower string) (err error) {
|
||||
pathCtx, cancel := context.WithCancel(ctx)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
var (
|
||||
w1 = pathWalker(pathCtx, lower)
|
||||
w2 = pathWalker(pathCtx, upper)
|
||||
f1, f2 *currentPath
|
||||
rmdir string
|
||||
)
|
||||
|
||||
for w1 != nil || w2 != nil {
|
||||
if f1 == nil && w1 != nil {
|
||||
f1, err = nextPath(w1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f1 == nil {
|
||||
w1 = nil
|
||||
}
|
||||
}
|
||||
|
||||
if f2 == nil && w2 != nil {
|
||||
f2, err = nextPath(w2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f2 == nil {
|
||||
w2 = nil
|
||||
}
|
||||
}
|
||||
if f1 == nil && f2 == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
c := pathChange(f1, f2)
|
||||
switch c.Kind {
|
||||
case ChangeKindAdd:
|
||||
if rmdir != "" {
|
||||
rmdir = ""
|
||||
}
|
||||
c.FileInfo = f2.f
|
||||
c.Source = filepath.Join(upper, c.Path)
|
||||
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 = ""
|
||||
}
|
||||
c.FileInfo = f2.f
|
||||
c.Source = filepath.Join(upper, c.Path)
|
||||
f1 = nil
|
||||
f2 = nil
|
||||
if same {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := sendChange(ctx, changes, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue