123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- /*
- Copyright The containerd Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- 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 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()
- }
|