github.com/goern/docker@v1.9.0-rc1/pkg/archive/changes.go (about) 1 package archive 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "sort" 12 "strings" 13 "syscall" 14 "time" 15 16 "github.com/Sirupsen/logrus" 17 "github.com/docker/docker/pkg/idtools" 18 "github.com/docker/docker/pkg/pools" 19 "github.com/docker/docker/pkg/system" 20 ) 21 22 // ChangeType represents the change type. 23 type ChangeType int 24 25 const ( 26 // ChangeModify represents the modify operation. 27 ChangeModify = iota 28 // ChangeAdd represents the add operation. 29 ChangeAdd 30 // ChangeDelete represents the delete operation. 31 ChangeDelete 32 ) 33 34 // Change represents a change, it wraps the change type and path. 35 // It describes changes of the files in the path respect to the 36 // parent layers. The change could be modify, add, delete. 37 // This is used for layer diff. 38 type Change struct { 39 Path string 40 Kind ChangeType 41 } 42 43 func (change *Change) String() string { 44 var kind string 45 switch change.Kind { 46 case ChangeModify: 47 kind = "C" 48 case ChangeAdd: 49 kind = "A" 50 case ChangeDelete: 51 kind = "D" 52 } 53 return fmt.Sprintf("%s %s", kind, change.Path) 54 } 55 56 // for sort.Sort 57 type changesByPath []Change 58 59 func (c changesByPath) Less(i, j int) bool { return c[i].Path < c[j].Path } 60 func (c changesByPath) Len() int { return len(c) } 61 func (c changesByPath) Swap(i, j int) { c[j], c[i] = c[i], c[j] } 62 63 // Gnu tar and the go tar writer don't have sub-second mtime 64 // precision, which is problematic when we apply changes via tar 65 // files, we handle this by comparing for exact times, *or* same 66 // second count and either a or b having exactly 0 nanoseconds 67 func sameFsTime(a, b time.Time) bool { 68 return a == b || 69 (a.Unix() == b.Unix() && 70 (a.Nanosecond() == 0 || b.Nanosecond() == 0)) 71 } 72 73 func sameFsTimeSpec(a, b syscall.Timespec) bool { 74 return a.Sec == b.Sec && 75 (a.Nsec == b.Nsec || a.Nsec == 0 || b.Nsec == 0) 76 } 77 78 // Changes walks the path rw and determines changes for the files in the path, 79 // with respect to the parent layers 80 func Changes(layers []string, rw string) ([]Change, error) { 81 var ( 82 changes []Change 83 changedDirs = make(map[string]struct{}) 84 ) 85 86 err := filepath.Walk(rw, func(path string, f os.FileInfo, err error) error { 87 if err != nil { 88 return err 89 } 90 91 // Rebase path 92 path, err = filepath.Rel(rw, path) 93 if err != nil { 94 return err 95 } 96 97 // As this runs on the daemon side, file paths are OS specific. 98 path = filepath.Join(string(os.PathSeparator), path) 99 100 // Skip root 101 if path == string(os.PathSeparator) { 102 return nil 103 } 104 105 // Skip AUFS metadata 106 if matched, err := filepath.Match(string(os.PathSeparator)+WhiteoutMetaPrefix+"*", path); err != nil || matched { 107 return err 108 } 109 110 change := Change{ 111 Path: path, 112 } 113 114 // Find out what kind of modification happened 115 file := filepath.Base(path) 116 // If there is a whiteout, then the file was removed 117 if strings.HasPrefix(file, WhiteoutPrefix) { 118 originalFile := file[len(WhiteoutPrefix):] 119 change.Path = filepath.Join(filepath.Dir(path), originalFile) 120 change.Kind = ChangeDelete 121 } else { 122 // Otherwise, the file was added 123 change.Kind = ChangeAdd 124 125 // ...Unless it already existed in a top layer, in which case, it's a modification 126 for _, layer := range layers { 127 stat, err := os.Stat(filepath.Join(layer, path)) 128 if err != nil && !os.IsNotExist(err) { 129 return err 130 } 131 if err == nil { 132 // The file existed in the top layer, so that's a modification 133 134 // However, if it's a directory, maybe it wasn't actually modified. 135 // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar 136 if stat.IsDir() && f.IsDir() { 137 if f.Size() == stat.Size() && f.Mode() == stat.Mode() && sameFsTime(f.ModTime(), stat.ModTime()) { 138 // Both directories are the same, don't record the change 139 return nil 140 } 141 } 142 change.Kind = ChangeModify 143 break 144 } 145 } 146 } 147 148 // If /foo/bar/file.txt is modified, then /foo/bar must be part of the changed files. 149 // This block is here to ensure the change is recorded even if the 150 // modify time, mode and size of the parent directoriy in the rw and ro layers are all equal. 151 // Check https://github.com/docker/docker/pull/13590 for details. 152 if f.IsDir() { 153 changedDirs[path] = struct{}{} 154 } 155 if change.Kind == ChangeAdd || change.Kind == ChangeDelete { 156 parent := filepath.Dir(path) 157 if _, ok := changedDirs[parent]; !ok && parent != "/" { 158 changes = append(changes, Change{Path: parent, Kind: ChangeModify}) 159 changedDirs[parent] = struct{}{} 160 } 161 } 162 163 // Record change 164 changes = append(changes, change) 165 return nil 166 }) 167 if err != nil && !os.IsNotExist(err) { 168 return nil, err 169 } 170 return changes, nil 171 } 172 173 // FileInfo describes the information of a file. 174 type FileInfo struct { 175 parent *FileInfo 176 name string 177 stat *system.StatT 178 children map[string]*FileInfo 179 capability []byte 180 added bool 181 } 182 183 // LookUp looks up the file information of a file. 184 func (info *FileInfo) LookUp(path string) *FileInfo { 185 // As this runs on the daemon side, file paths are OS specific. 186 parent := info 187 if path == string(os.PathSeparator) { 188 return info 189 } 190 191 pathElements := strings.Split(path, string(os.PathSeparator)) 192 for _, elem := range pathElements { 193 if elem != "" { 194 child := parent.children[elem] 195 if child == nil { 196 return nil 197 } 198 parent = child 199 } 200 } 201 return parent 202 } 203 204 func (info *FileInfo) path() string { 205 if info.parent == nil { 206 // As this runs on the daemon side, file paths are OS specific. 207 return string(os.PathSeparator) 208 } 209 return filepath.Join(info.parent.path(), info.name) 210 } 211 212 func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) { 213 214 sizeAtEntry := len(*changes) 215 216 if oldInfo == nil { 217 // add 218 change := Change{ 219 Path: info.path(), 220 Kind: ChangeAdd, 221 } 222 *changes = append(*changes, change) 223 info.added = true 224 } 225 226 // We make a copy so we can modify it to detect additions 227 // also, we only recurse on the old dir if the new info is a directory 228 // otherwise any previous delete/change is considered recursive 229 oldChildren := make(map[string]*FileInfo) 230 if oldInfo != nil && info.isDir() { 231 for k, v := range oldInfo.children { 232 oldChildren[k] = v 233 } 234 } 235 236 for name, newChild := range info.children { 237 oldChild, _ := oldChildren[name] 238 if oldChild != nil { 239 // change? 240 oldStat := oldChild.stat 241 newStat := newChild.stat 242 // Note: We can't compare inode or ctime or blocksize here, because these change 243 // when copying a file into a container. However, that is not generally a problem 244 // because any content change will change mtime, and any status change should 245 // be visible when actually comparing the stat fields. The only time this 246 // breaks down is if some code intentionally hides a change by setting 247 // back mtime 248 if statDifferent(oldStat, newStat) || 249 bytes.Compare(oldChild.capability, newChild.capability) != 0 { 250 change := Change{ 251 Path: newChild.path(), 252 Kind: ChangeModify, 253 } 254 *changes = append(*changes, change) 255 newChild.added = true 256 } 257 258 // Remove from copy so we can detect deletions 259 delete(oldChildren, name) 260 } 261 262 newChild.addChanges(oldChild, changes) 263 } 264 for _, oldChild := range oldChildren { 265 // delete 266 change := Change{ 267 Path: oldChild.path(), 268 Kind: ChangeDelete, 269 } 270 *changes = append(*changes, change) 271 } 272 273 // If there were changes inside this directory, we need to add it, even if the directory 274 // itself wasn't changed. This is needed to properly save and restore filesystem permissions. 275 // As this runs on the daemon side, file paths are OS specific. 276 if len(*changes) > sizeAtEntry && info.isDir() && !info.added && info.path() != string(os.PathSeparator) { 277 change := Change{ 278 Path: info.path(), 279 Kind: ChangeModify, 280 } 281 // Let's insert the directory entry before the recently added entries located inside this dir 282 *changes = append(*changes, change) // just to resize the slice, will be overwritten 283 copy((*changes)[sizeAtEntry+1:], (*changes)[sizeAtEntry:]) 284 (*changes)[sizeAtEntry] = change 285 } 286 287 } 288 289 // Changes add changes to file information. 290 func (info *FileInfo) Changes(oldInfo *FileInfo) []Change { 291 var changes []Change 292 293 info.addChanges(oldInfo, &changes) 294 295 return changes 296 } 297 298 func newRootFileInfo() *FileInfo { 299 // As this runs on the daemon side, file paths are OS specific. 300 root := &FileInfo{ 301 name: string(os.PathSeparator), 302 children: make(map[string]*FileInfo), 303 } 304 return root 305 } 306 307 // ChangesDirs compares two directories and generates an array of Change objects describing the changes. 308 // If oldDir is "", then all files in newDir will be Add-Changes. 309 func ChangesDirs(newDir, oldDir string) ([]Change, error) { 310 var ( 311 oldRoot, newRoot *FileInfo 312 ) 313 if oldDir == "" { 314 emptyDir, err := ioutil.TempDir("", "empty") 315 if err != nil { 316 return nil, err 317 } 318 defer os.Remove(emptyDir) 319 oldDir = emptyDir 320 } 321 oldRoot, newRoot, err := collectFileInfoForChanges(oldDir, newDir) 322 if err != nil { 323 return nil, err 324 } 325 326 return newRoot.Changes(oldRoot), nil 327 } 328 329 // ChangesSize calculates the size in bytes of the provided changes, based on newDir. 330 func ChangesSize(newDir string, changes []Change) int64 { 331 var size int64 332 for _, change := range changes { 333 if change.Kind == ChangeModify || change.Kind == ChangeAdd { 334 file := filepath.Join(newDir, change.Path) 335 fileInfo, _ := os.Lstat(file) 336 if fileInfo != nil && !fileInfo.IsDir() { 337 size += fileInfo.Size() 338 } 339 } 340 } 341 return size 342 } 343 344 // ExportChanges produces an Archive from the provided changes, relative to dir. 345 func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMap) (Archive, error) { 346 reader, writer := io.Pipe() 347 go func() { 348 ta := &tarAppender{ 349 TarWriter: tar.NewWriter(writer), 350 Buffer: pools.BufioWriter32KPool.Get(nil), 351 SeenFiles: make(map[uint64]string), 352 UIDMaps: uidMaps, 353 GIDMaps: gidMaps, 354 } 355 // this buffer is needed for the duration of this piped stream 356 defer pools.BufioWriter32KPool.Put(ta.Buffer) 357 358 sort.Sort(changesByPath(changes)) 359 360 // In general we log errors here but ignore them because 361 // during e.g. a diff operation the container can continue 362 // mutating the filesystem and we can see transient errors 363 // from this 364 for _, change := range changes { 365 if change.Kind == ChangeDelete { 366 whiteOutDir := filepath.Dir(change.Path) 367 whiteOutBase := filepath.Base(change.Path) 368 whiteOut := filepath.Join(whiteOutDir, WhiteoutPrefix+whiteOutBase) 369 timestamp := time.Now() 370 hdr := &tar.Header{ 371 Name: whiteOut[1:], 372 Size: 0, 373 ModTime: timestamp, 374 AccessTime: timestamp, 375 ChangeTime: timestamp, 376 } 377 if err := ta.TarWriter.WriteHeader(hdr); err != nil { 378 logrus.Debugf("Can't write whiteout header: %s", err) 379 } 380 } else { 381 path := filepath.Join(dir, change.Path) 382 if err := ta.addTarFile(path, change.Path[1:]); err != nil { 383 logrus.Debugf("Can't add file %s to tar: %s", path, err) 384 } 385 } 386 } 387 388 // Make sure to check the error on Close. 389 if err := ta.TarWriter.Close(); err != nil { 390 logrus.Debugf("Can't close layer: %s", err) 391 } 392 if err := writer.Close(); err != nil { 393 logrus.Debugf("failed close Changes writer: %s", err) 394 } 395 }() 396 return reader, nil 397 }