diff.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /*
  2. Copyright The containerd Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package fs
  14. import (
  15. "context"
  16. "os"
  17. "path/filepath"
  18. "strings"
  19. "golang.org/x/sync/errgroup"
  20. "github.com/sirupsen/logrus"
  21. )
  22. // ChangeKind is the type of modification that
  23. // a change is making.
  24. type ChangeKind int
  25. const (
  26. // ChangeKindUnmodified represents an unmodified
  27. // file
  28. ChangeKindUnmodified = iota
  29. // ChangeKindAdd represents an addition of
  30. // a file
  31. ChangeKindAdd
  32. // ChangeKindModify represents a change to
  33. // an existing file
  34. ChangeKindModify
  35. // ChangeKindDelete represents a delete of
  36. // a file
  37. ChangeKindDelete
  38. )
  39. func (k ChangeKind) String() string {
  40. switch k {
  41. case ChangeKindUnmodified:
  42. return "unmodified"
  43. case ChangeKindAdd:
  44. return "add"
  45. case ChangeKindModify:
  46. return "modify"
  47. case ChangeKindDelete:
  48. return "delete"
  49. default:
  50. return ""
  51. }
  52. }
  53. // Change represents single change between a diff and its parent.
  54. type Change struct {
  55. Kind ChangeKind
  56. Path string
  57. }
  58. // ChangeFunc is the type of function called for each change
  59. // computed during a directory changes calculation.
  60. type ChangeFunc func(ChangeKind, string, os.FileInfo, error) error
  61. // Changes computes changes between two directories calling the
  62. // given change function for each computed change. The first
  63. // directory is intended to the base directory and second
  64. // directory the changed directory.
  65. //
  66. // The change callback is called by the order of path names and
  67. // should be appliable in that order.
  68. // Due to this apply ordering, the following is true
  69. // - Removed directory trees only create a single change for the root
  70. // directory removed. Remaining changes are implied.
  71. // - A directory which is modified to become a file will not have
  72. // delete entries for sub-path items, their removal is implied
  73. // by the removal of the parent directory.
  74. //
  75. // Opaque directories will not be treated specially and each file
  76. // removed from the base directory will show up as a removal.
  77. //
  78. // File content comparisons will be done on files which have timestamps
  79. // which may have been truncated. If either of the files being compared
  80. // has a zero value nanosecond value, each byte will be compared for
  81. // differences. If 2 files have the same seconds value but different
  82. // nanosecond values where one of those values is zero, the files will
  83. // be considered unchanged if the content is the same. This behavior
  84. // is to account for timestamp truncation during archiving.
  85. func Changes(ctx context.Context, a, b string, changeFn ChangeFunc) error {
  86. if a == "" {
  87. logrus.Debugf("Using single walk diff for %s", b)
  88. return addDirChanges(ctx, changeFn, b)
  89. } else if diffOptions := detectDirDiff(b, a); diffOptions != nil {
  90. logrus.Debugf("Using single walk diff for %s from %s", diffOptions.diffDir, a)
  91. return diffDirChanges(ctx, changeFn, a, diffOptions)
  92. }
  93. logrus.Debugf("Using double walk diff for %s from %s", b, a)
  94. return doubleWalkDiff(ctx, changeFn, a, b)
  95. }
  96. func addDirChanges(ctx context.Context, changeFn ChangeFunc, root string) error {
  97. return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
  98. if err != nil {
  99. return err
  100. }
  101. // Rebase path
  102. path, err = filepath.Rel(root, path)
  103. if err != nil {
  104. return err
  105. }
  106. path = filepath.Join(string(os.PathSeparator), path)
  107. // Skip root
  108. if path == string(os.PathSeparator) {
  109. return nil
  110. }
  111. return changeFn(ChangeKindAdd, path, f, nil)
  112. })
  113. }
  114. // diffDirOptions is used when the diff can be directly calculated from
  115. // a diff directory to its base, without walking both trees.
  116. type diffDirOptions struct {
  117. diffDir string
  118. skipChange func(string) (bool, error)
  119. deleteChange func(string, string, os.FileInfo) (string, error)
  120. }
  121. // diffDirChanges walks the diff directory and compares changes against the base.
  122. func diffDirChanges(ctx context.Context, changeFn ChangeFunc, base string, o *diffDirOptions) error {
  123. changedDirs := make(map[string]struct{})
  124. return filepath.Walk(o.diffDir, func(path string, f os.FileInfo, err error) error {
  125. if err != nil {
  126. return err
  127. }
  128. // Rebase path
  129. path, err = filepath.Rel(o.diffDir, path)
  130. if err != nil {
  131. return err
  132. }
  133. path = filepath.Join(string(os.PathSeparator), path)
  134. // Skip root
  135. if path == string(os.PathSeparator) {
  136. return nil
  137. }
  138. // TODO: handle opaqueness, start new double walker at this
  139. // location to get deletes, and skip tree in single walker
  140. if o.skipChange != nil {
  141. if skip, err := o.skipChange(path); skip {
  142. return err
  143. }
  144. }
  145. var kind ChangeKind
  146. deletedFile, err := o.deleteChange(o.diffDir, path, f)
  147. if err != nil {
  148. return err
  149. }
  150. // Find out what kind of modification happened
  151. if deletedFile != "" {
  152. path = deletedFile
  153. kind = ChangeKindDelete
  154. f = nil
  155. } else {
  156. // Otherwise, the file was added
  157. kind = ChangeKindAdd
  158. // ...Unless it already existed in a base, in which case, it's a modification
  159. stat, err := os.Stat(filepath.Join(base, path))
  160. if err != nil && !os.IsNotExist(err) {
  161. return err
  162. }
  163. if err == nil {
  164. // The file existed in the base, so that's a modification
  165. // However, if it's a directory, maybe it wasn't actually modified.
  166. // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar
  167. if stat.IsDir() && f.IsDir() {
  168. if f.Size() == stat.Size() && f.Mode() == stat.Mode() && sameFsTime(f.ModTime(), stat.ModTime()) {
  169. // Both directories are the same, don't record the change
  170. return nil
  171. }
  172. }
  173. kind = ChangeKindModify
  174. }
  175. }
  176. // If /foo/bar/file.txt is modified, then /foo/bar must be part of the changed files.
  177. // This block is here to ensure the change is recorded even if the
  178. // modify time, mode and size of the parent directory in the rw and ro layers are all equal.
  179. // Check https://github.com/docker/docker/pull/13590 for details.
  180. if f.IsDir() {
  181. changedDirs[path] = struct{}{}
  182. }
  183. if kind == ChangeKindAdd || kind == ChangeKindDelete {
  184. parent := filepath.Dir(path)
  185. if _, ok := changedDirs[parent]; !ok && parent != "/" {
  186. pi, err := os.Stat(filepath.Join(o.diffDir, parent))
  187. if err := changeFn(ChangeKindModify, parent, pi, err); err != nil {
  188. return err
  189. }
  190. changedDirs[parent] = struct{}{}
  191. }
  192. }
  193. return changeFn(kind, path, f, nil)
  194. })
  195. }
  196. // doubleWalkDiff walks both directories to create a diff
  197. func doubleWalkDiff(ctx context.Context, changeFn ChangeFunc, a, b string) (err error) {
  198. g, ctx := errgroup.WithContext(ctx)
  199. var (
  200. c1 = make(chan *currentPath)
  201. c2 = make(chan *currentPath)
  202. f1, f2 *currentPath
  203. rmdir string
  204. )
  205. g.Go(func() error {
  206. defer close(c1)
  207. return pathWalk(ctx, a, c1)
  208. })
  209. g.Go(func() error {
  210. defer close(c2)
  211. return pathWalk(ctx, b, c2)
  212. })
  213. g.Go(func() error {
  214. for c1 != nil || c2 != nil {
  215. if f1 == nil && c1 != nil {
  216. f1, err = nextPath(ctx, c1)
  217. if err != nil {
  218. return err
  219. }
  220. if f1 == nil {
  221. c1 = nil
  222. }
  223. }
  224. if f2 == nil && c2 != nil {
  225. f2, err = nextPath(ctx, c2)
  226. if err != nil {
  227. return err
  228. }
  229. if f2 == nil {
  230. c2 = nil
  231. }
  232. }
  233. if f1 == nil && f2 == nil {
  234. continue
  235. }
  236. var f os.FileInfo
  237. k, p := pathChange(f1, f2)
  238. switch k {
  239. case ChangeKindAdd:
  240. if rmdir != "" {
  241. rmdir = ""
  242. }
  243. f = f2.f
  244. f2 = nil
  245. case ChangeKindDelete:
  246. // Check if this file is already removed by being
  247. // under of a removed directory
  248. if rmdir != "" && strings.HasPrefix(f1.path, rmdir) {
  249. f1 = nil
  250. continue
  251. } else if f1.f.IsDir() {
  252. rmdir = f1.path + string(os.PathSeparator)
  253. } else if rmdir != "" {
  254. rmdir = ""
  255. }
  256. f1 = nil
  257. case ChangeKindModify:
  258. same, err := sameFile(f1, f2)
  259. if err != nil {
  260. return err
  261. }
  262. if f1.f.IsDir() && !f2.f.IsDir() {
  263. rmdir = f1.path + string(os.PathSeparator)
  264. } else if rmdir != "" {
  265. rmdir = ""
  266. }
  267. f = f2.f
  268. f1 = nil
  269. f2 = nil
  270. if same {
  271. if !isLinked(f) {
  272. continue
  273. }
  274. k = ChangeKindUnmodified
  275. }
  276. }
  277. if err := changeFn(k, p, f, nil); err != nil {
  278. return err
  279. }
  280. }
  281. return nil
  282. })
  283. return g.Wait()
  284. }