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