diff.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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. "errors"
  17. "os"
  18. "path/filepath"
  19. "strings"
  20. "github.com/containerd/log"
  21. "golang.org/x/sync/errgroup"
  22. )
  23. // ChangeKind is the type of modification that
  24. // a change is making.
  25. type ChangeKind int
  26. const (
  27. // ChangeKindUnmodified represents an unmodified
  28. // file
  29. ChangeKindUnmodified = iota
  30. // ChangeKindAdd represents an addition of
  31. // a file
  32. ChangeKindAdd
  33. // ChangeKindModify represents a change to
  34. // an existing file
  35. ChangeKindModify
  36. // ChangeKindDelete represents a delete of
  37. // a file
  38. ChangeKindDelete
  39. )
  40. func (k ChangeKind) String() string {
  41. switch k {
  42. case ChangeKindUnmodified:
  43. return "unmodified"
  44. case ChangeKindAdd:
  45. return "add"
  46. case ChangeKindModify:
  47. return "modify"
  48. case ChangeKindDelete:
  49. return "delete"
  50. default:
  51. return ""
  52. }
  53. }
  54. // Change represents single change between a diff and its parent.
  55. type Change struct {
  56. Kind ChangeKind
  57. Path string
  58. }
  59. // ChangeFunc is the type of function called for each change
  60. // computed during a directory changes calculation.
  61. type ChangeFunc func(ChangeKind, string, os.FileInfo, error) error
  62. // Changes computes changes between two directories calling the
  63. // given change function for each computed change. The first
  64. // directory is intended to the base directory and second
  65. // directory the changed directory.
  66. //
  67. // The change callback is called by the order of path names and
  68. // should be appliable in that order.
  69. //
  70. // Due to this apply ordering, the following is true
  71. // - Removed directory trees only create a single change for the root
  72. // directory removed. Remaining changes are implied.
  73. // - A directory which is modified to become a file will not have
  74. // delete entries for sub-path items, their removal is implied
  75. // by the removal of the parent directory.
  76. //
  77. // Opaque directories will not be treated specially and each file
  78. // removed from the base directory will show up as a removal.
  79. //
  80. // File content comparisons will be done on files which have timestamps
  81. // which may have been truncated. If either of the files being compared
  82. // has a zero value nanosecond value, each byte will be compared for
  83. // differences. If 2 files have the same seconds value but different
  84. // nanosecond values where one of those values is zero, the files will
  85. // be considered unchanged if the content is the same. This behavior
  86. // is to account for timestamp truncation during archiving.
  87. func Changes(ctx context.Context, a, b string, changeFn ChangeFunc) error {
  88. if a == "" {
  89. log.G(ctx).Debugf("Using single walk diff for %s", b)
  90. return addDirChanges(ctx, changeFn, b)
  91. }
  92. log.G(ctx).Debugf("Using double walk diff for %s from %s", b, a)
  93. return doubleWalkDiff(ctx, changeFn, a, b)
  94. }
  95. func addDirChanges(ctx context.Context, changeFn ChangeFunc, root string) error {
  96. return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
  97. if err != nil {
  98. return err
  99. }
  100. // Rebase path
  101. path, err = filepath.Rel(root, path)
  102. if err != nil {
  103. return err
  104. }
  105. path = filepath.Join(string(os.PathSeparator), path)
  106. // Skip root
  107. if path == string(os.PathSeparator) {
  108. return nil
  109. }
  110. return changeFn(ChangeKindAdd, path, f, nil)
  111. })
  112. }
  113. // DiffChangeSource is the source of diff directory.
  114. type DiffSource int
  115. const (
  116. // DiffSourceOverlayFS indicates that a diff directory is from
  117. // OverlayFS.
  118. DiffSourceOverlayFS DiffSource = iota
  119. )
  120. // diffDirOptions is used when the diff can be directly calculated from
  121. // a diff directory to its base, without walking both trees.
  122. type diffDirOptions struct {
  123. skipChange func(string, os.FileInfo) (bool, error)
  124. deleteChange func(string, string, os.FileInfo, ChangeFunc) (bool, error)
  125. }
  126. // DiffDirChanges walks the diff directory and compares changes against the base.
  127. //
  128. // NOTE: If all the children of a dir are removed, or that dir are recreated
  129. // after remove, we will mark non-existing `.wh..opq` file as deleted. It's
  130. // unlikely to create explicit whiteout files for all the children and all
  131. // descendants. And based on OCI spec, it's not possible to create a file or
  132. // dir with a name beginning with `.wh.`. So, after `.wh..opq` file has been
  133. // deleted, the ChangeFunc, the receiver will add whiteout prefix to create a
  134. // opaque whiteout `.wh..wh..opq`.
  135. //
  136. // REF: https://github.com/opencontainers/image-spec/blob/v1.0/layer.md#whiteouts
  137. func DiffDirChanges(ctx context.Context, baseDir, diffDir string, source DiffSource, changeFn ChangeFunc) error {
  138. var o *diffDirOptions
  139. switch source {
  140. case DiffSourceOverlayFS:
  141. o = &diffDirOptions{
  142. deleteChange: overlayFSWhiteoutConvert,
  143. }
  144. default:
  145. return errors.New("unknown diff change source")
  146. }
  147. changedDirs := make(map[string]struct{})
  148. return filepath.Walk(diffDir, func(path string, f os.FileInfo, err error) error {
  149. if err != nil {
  150. return err
  151. }
  152. // Rebase path
  153. path, err = filepath.Rel(diffDir, path)
  154. if err != nil {
  155. return err
  156. }
  157. path = filepath.Join(string(os.PathSeparator), path)
  158. // Skip root
  159. if path == string(os.PathSeparator) {
  160. return nil
  161. }
  162. if o.skipChange != nil {
  163. if skip, err := o.skipChange(path, f); skip {
  164. return err
  165. }
  166. }
  167. var kind ChangeKind
  168. deletedFile := false
  169. if o.deleteChange != nil {
  170. deletedFile, err = o.deleteChange(diffDir, path, f, changeFn)
  171. if err != nil {
  172. return err
  173. }
  174. _, err = os.Stat(filepath.Join(baseDir, path))
  175. if err != nil {
  176. if !os.IsNotExist(err) {
  177. return err
  178. }
  179. deletedFile = false
  180. }
  181. }
  182. // Find out what kind of modification happened
  183. if deletedFile {
  184. kind = ChangeKindDelete
  185. } else {
  186. // Otherwise, the file was added
  187. kind = ChangeKindAdd
  188. // ...Unless it already existed in a baseDir, in which case, it's a modification
  189. stat, err := os.Stat(filepath.Join(baseDir, path))
  190. if err != nil && !os.IsNotExist(err) {
  191. return err
  192. }
  193. if err == nil {
  194. // The file existed in the baseDir, so that's a modification
  195. // However, if it's a directory, maybe it wasn't actually modified.
  196. // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar
  197. if stat.IsDir() && f.IsDir() {
  198. if f.Size() == stat.Size() && f.Mode() == stat.Mode() && sameFsTime(f.ModTime(), stat.ModTime()) {
  199. // Both directories are the same, don't record the change
  200. return nil
  201. }
  202. }
  203. kind = ChangeKindModify
  204. }
  205. }
  206. // If /foo/bar/file.txt is modified, then /foo/bar must be part of the changed files.
  207. // This block is here to ensure the change is recorded even if the
  208. // modify time, mode and size of the parent directory in the rw and ro layers are all equal.
  209. // Check https://github.com/docker/docker/pull/13590 for details.
  210. if f.IsDir() {
  211. changedDirs[path] = struct{}{}
  212. }
  213. if kind == ChangeKindAdd || kind == ChangeKindDelete {
  214. parent := filepath.Dir(path)
  215. if _, ok := changedDirs[parent]; !ok && parent != "/" {
  216. pi, err := os.Stat(filepath.Join(diffDir, parent))
  217. if err := changeFn(ChangeKindModify, parent, pi, err); err != nil {
  218. return err
  219. }
  220. changedDirs[parent] = struct{}{}
  221. }
  222. }
  223. if kind == ChangeKindDelete {
  224. f = nil
  225. }
  226. return changeFn(kind, path, f, nil)
  227. })
  228. }
  229. // doubleWalkDiff walks both directories to create a diff
  230. func doubleWalkDiff(ctx context.Context, changeFn ChangeFunc, a, b string) (err error) {
  231. g, ctx := errgroup.WithContext(ctx)
  232. var (
  233. c1 = make(chan *currentPath)
  234. c2 = make(chan *currentPath)
  235. f1, f2 *currentPath
  236. rmdir string
  237. )
  238. g.Go(func() error {
  239. defer close(c1)
  240. return pathWalk(ctx, a, c1)
  241. })
  242. g.Go(func() error {
  243. defer close(c2)
  244. return pathWalk(ctx, b, c2)
  245. })
  246. g.Go(func() error {
  247. for c1 != nil || c2 != nil {
  248. if f1 == nil && c1 != nil {
  249. f1, err = nextPath(ctx, c1)
  250. if err != nil {
  251. return err
  252. }
  253. if f1 == nil {
  254. c1 = nil
  255. }
  256. }
  257. if f2 == nil && c2 != nil {
  258. f2, err = nextPath(ctx, c2)
  259. if err != nil {
  260. return err
  261. }
  262. if f2 == nil {
  263. c2 = nil
  264. }
  265. }
  266. if f1 == nil && f2 == nil {
  267. continue
  268. }
  269. var f os.FileInfo
  270. k, p := pathChange(f1, f2)
  271. switch k {
  272. case ChangeKindAdd:
  273. if rmdir != "" {
  274. rmdir = ""
  275. }
  276. f = f2.f
  277. f2 = nil
  278. case ChangeKindDelete:
  279. // Check if this file is already removed by being
  280. // under of a removed directory
  281. if rmdir != "" && strings.HasPrefix(f1.path, rmdir) {
  282. f1 = nil
  283. continue
  284. } else if f1.f.IsDir() {
  285. rmdir = f1.path + string(os.PathSeparator)
  286. } else if rmdir != "" {
  287. rmdir = ""
  288. }
  289. f1 = nil
  290. case ChangeKindModify:
  291. same, err := sameFile(f1, f2)
  292. if err != nil {
  293. return err
  294. }
  295. if f1.f.IsDir() && !f2.f.IsDir() {
  296. rmdir = f1.path + string(os.PathSeparator)
  297. } else if rmdir != "" {
  298. rmdir = ""
  299. }
  300. f = f2.f
  301. f1 = nil
  302. f2 = nil
  303. if same {
  304. if !isLinked(f) {
  305. continue
  306. }
  307. k = ChangeKindUnmodified
  308. }
  309. }
  310. if err := changeFn(k, p, f, nil); err != nil {
  311. return err
  312. }
  313. }
  314. return nil
  315. })
  316. return g.Wait()
  317. }