diff --git a/filesystem.go b/filesystem.go index 7a37807790..41dacd5fdd 100644 --- a/filesystem.go +++ b/filesystem.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "os" + "path/filepath" + "strings" "syscall" ) @@ -65,6 +67,91 @@ func (fs *Filesystem) IsMounted() bool { return false } +type ChangeType int + +const ( + ChangeModify = iota + ChangeAdd + ChangeDelete +) + +type Change struct { + Path string + Kind ChangeType +} + +func (fs *Filesystem) Changes() ([]Change, error) { + var changes []Change + err := filepath.Walk(fs.RWPath, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + // Rebase path + path, err = filepath.Rel(fs.RWPath, path) + if err != nil { + return err + } + path = filepath.Join("/", path) + + // Skip root + if path == "/" { + return nil + } + + // Skip AUFS metadata + if matched, err := filepath.Match("/.wh..wh.*", path); err != nil || matched { + return err + } + + change := Change{ + Path: path, + } + + // Find out what kind of modification happened + file := filepath.Base(path) + // If there is a whiteout, then the file was removed + if strings.HasPrefix(file, ".wh.") { + originalFile := strings.TrimLeft(file, ".wh.") + change.Path = filepath.Join(filepath.Dir(path), originalFile) + change.Kind = ChangeDelete + } else { + // Otherwise, the file was added + change.Kind = ChangeAdd + + // ...Unless it already existed in a top layer, in which case, it's a modification + for _, layer := range fs.Layers { + stat, err := os.Stat(filepath.Join(layer, path)) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + // The file existed in the top layer, so that's a modification + + // However, if it's a directory, maybe it wasn't actually modified. + // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar + if stat.IsDir() && f.IsDir() { + if f.Size() == stat.Size() && f.Mode() == stat.Mode() && f.ModTime() == stat.ModTime() { + // Both directories are the same, don't record the change + return nil + } + } + change.Kind = ChangeModify + break + } + } + } + + // Record change + changes = append(changes, change) + return nil + }) + if err != nil { + return nil, err + } + return changes, nil +} + func newFilesystem(rootfs string, rwpath string, layers []string) *Filesystem { return &Filesystem{ RootFS: rootfs, diff --git a/filesystem_test.go b/filesystem_test.go index 7bf9514dc3..b4deb02b36 100644 --- a/filesystem_test.go +++ b/filesystem_test.go @@ -8,12 +8,12 @@ import ( "testing" ) -func newTestFilesystem(t *testing.T, layers []string) (rootfs string, rwpath string, fs *Filesystem) { +func newTestFilesystem(t *testing.T, layers []string) (rootfs string, fs *Filesystem) { rootfs, err := ioutil.TempDir("", "docker-test-root") if err != nil { t.Fatal(err) } - rwpath, err = ioutil.TempDir("", "docker-test-rw") + rwpath, err := ioutil.TempDir("", "docker-test-rw") if err != nil { t.Fatal(err) } @@ -22,7 +22,7 @@ func newTestFilesystem(t *testing.T, layers []string) (rootfs string, rwpath str } func TestFilesystem(t *testing.T) { - _, _, filesystem := newTestFilesystem(t, []string{"/var/lib/docker/images/ubuntu"}) + _, filesystem := newTestFilesystem(t, []string{"/var/lib/docker/images/ubuntu"}) if err := filesystem.Umount(); err == nil { t.Errorf("Umount succeeded even though the filesystem was not mounted") } @@ -56,7 +56,7 @@ func TestFilesystemMultiLayer(t *testing.T) { } // Create the layered filesystem and add our fake layer on top - rootfs, _, filesystem := newTestFilesystem(t, []string{"/var/lib/docker/images/ubuntu", fakeLayer}) + rootfs, filesystem := newTestFilesystem(t, []string{"/var/lib/docker/images/ubuntu", fakeLayer}) // Mount it if err := filesystem.Mount(); err != nil { @@ -80,3 +80,109 @@ func TestFilesystemMultiLayer(t *testing.T) { t.Error(string(fsdata)) } } + +func TestChanges(t *testing.T) { + rootfs, filesystem := newTestFilesystem(t, []string{"/var/lib/docker/images/ubuntu"}) + // Mount it + if err := filesystem.Mount(); err != nil { + t.Fatal(err) + } + defer filesystem.Umount() + + var changes []Change + var err error + + // Test without changes + changes, err = filesystem.Changes() + if err != nil { + t.Fatal(err) + } + if len(changes) != 0 { + t.Errorf("Unexpected changes :%v", changes) + } + + // Test simple change + file, err := os.Create(path.Join(rootfs, "test_change")) + if err != nil { + t.Fatal(err) + } + file.Close() + + changes, err = filesystem.Changes() + if err != nil { + t.Fatal(err) + } + if len(changes) != 1 { + t.Errorf("Unexpected changes :%v", changes) + } + if changes[0].Path != "/test_change" || changes[0].Kind != ChangeAdd { + t.Errorf("Unexpected changes :%v", changes) + } + + // Test subdirectory change + if err := os.Mkdir(path.Join(rootfs, "sub_change"), 0700); err != nil { + t.Fatal(err) + } + + file, err = os.Create(path.Join(rootfs, "sub_change", "test")) + if err != nil { + t.Fatal(err) + } + file.Close() + + changes, err = filesystem.Changes() + if err != nil { + t.Fatal(err) + } + if len(changes) != 3 { + t.Errorf("Unexpected changes: %v", changes) + } + if changes[0].Path != "/sub_change" || changes[0].Kind != ChangeAdd || changes[1].Path != "/sub_change/test" || changes[1].Kind != ChangeAdd { + t.Errorf("Unexpected changes: %v", changes) + } + + // Test permission change + if err := os.Chmod(path.Join(rootfs, "root"), 0000); err != nil { + t.Fatal(err) + } + changes, err = filesystem.Changes() + if err != nil { + t.Fatal(err) + } + if len(changes) != 4 { + t.Errorf("Unexpected changes: %v", changes) + } + if changes[0].Path != "/root" || changes[0].Kind != ChangeModify { + t.Errorf("Unexpected changes: %v", changes) + } + + // Test removal + if err := os.Remove(path.Join(rootfs, "etc", "passwd")); err != nil { + t.Fatal(err) + } + changes, err = filesystem.Changes() + if err != nil { + t.Fatal(err) + } + if len(changes) != 6 { + t.Errorf("Unexpected changes: %v", changes) + } + if changes[0].Path != "/etc" || changes[0].Kind != ChangeModify || changes[1].Path != "/etc/passwd" || changes[1].Kind != ChangeDelete { + t.Errorf("Unexpected changes: %v", changes) + } + + // Test sub-directory removal + if err := os.Remove(path.Join(rootfs, "usr", "bin", "sudo")); err != nil { + t.Fatal(err) + } + changes, err = filesystem.Changes() + if err != nil { + t.Fatal(err) + } + if len(changes) != 8 { + t.Errorf("Unexpected changes: %v", changes) + } + if changes[6].Path != "/usr/bin" || changes[6].Kind != ChangeModify || changes[7].Path != "/usr/bin/sudo" || changes[7].Kind != ChangeDelete { + t.Errorf("Unexpected changes: %v", changes) + } +}