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