Add fs package

Add diff comparison with support for double walking
two trees for comparison or single walking a diff
tree. Single walking requires further implementation
for specific mount types.

Add directory copy function which is intended to provide
fastest possible local copy of file system directories
without hardlinking.

Add test package to make creating filesystems for
test easy and comparisons deep and informative.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
Derek McGowan 2017-01-31 15:17:33 -08:00
parent 5f08e609c0
commit 574862fd89
16 changed files with 1672 additions and 0 deletions

37
fs/fstest/compare.go Normal file
View file

@ -0,0 +1,37 @@
package fstest
import (
"github.com/pkg/errors"
"github.com/stevvooe/continuity"
)
// CheckDirectoryEqual compares two directory paths to make sure that
// the content of the directories is the same.
func CheckDirectoryEqual(d1, d2 string) error {
c1, err := continuity.NewContext(d1)
if err != nil {
return errors.Wrap(err, "failed to build context")
}
c2, err := continuity.NewContext(d2)
if err != nil {
return errors.Wrap(err, "failed to build context")
}
m1, err := continuity.BuildManifest(c1)
if err != nil {
return errors.Wrap(err, "failed to build manifest")
}
m2, err := continuity.BuildManifest(c2)
if err != nil {
return errors.Wrap(err, "failed to build manifest")
}
diff := diffResourceList(m1.Resources, m2.Resources)
if diff.HasDiff() {
return errors.Errorf("directory diff between %s and %s\n%s", d1, d2, diff.String())
}
return nil
}

View file

@ -0,0 +1,189 @@
package fstest
import (
"bytes"
"fmt"
"github.com/stevvooe/continuity"
)
type resourceUpdate struct {
Original continuity.Resource
Updated continuity.Resource
}
func (u resourceUpdate) String() string {
return fmt.Sprintf("%s(mode: %o, uid: %s, gid: %s) -> %s(mode: %o, uid: %s, gid: %s)",
u.Original.Path(), u.Original.Mode(), u.Original.UID(), u.Original.GID(),
u.Updated.Path(), u.Updated.Mode(), u.Updated.UID(), u.Updated.GID(),
)
}
type resourceListDifference struct {
Additions []continuity.Resource
Deletions []continuity.Resource
Updates []resourceUpdate
}
func (l resourceListDifference) HasDiff() bool {
return len(l.Additions) > 0 || len(l.Deletions) > 0 || len(l.Updates) > 0
}
func (l resourceListDifference) String() string {
buf := bytes.NewBuffer(nil)
for _, add := range l.Additions {
fmt.Fprintf(buf, "+ %s\n", add.Path())
}
for _, del := range l.Deletions {
fmt.Fprintf(buf, "- %s\n", del.Path())
}
for _, upt := range l.Updates {
fmt.Fprintf(buf, "~ %s\n", upt.String())
}
return string(buf.Bytes())
}
// diffManifest compares two resource lists and returns the list
// of adds updates and deletes, resource lists are not reordered
// before doing difference.
func diffResourceList(r1, r2 []continuity.Resource) resourceListDifference {
i1 := 0
i2 := 0
var d resourceListDifference
for i1 < len(r1) && i2 < len(r2) {
p1 := r1[i1].Path()
p2 := r2[i2].Path()
switch {
case p1 < p2:
d.Deletions = append(d.Deletions, r1[i1])
i1++
case p1 == p2:
if !compareResource(r1[i1], r2[i2]) {
d.Updates = append(d.Updates, resourceUpdate{
Original: r1[i1],
Updated: r2[i2],
})
}
i1++
i2++
case p1 > p2:
d.Additions = append(d.Additions, r2[i2])
i2++
}
}
for i1 < len(r1) {
d.Deletions = append(d.Deletions, r1[i1])
i1++
}
for i2 < len(r2) {
d.Additions = append(d.Additions, r2[i2])
i2++
}
return d
}
func compareResource(r1, r2 continuity.Resource) bool {
if r1.Path() != r2.Path() {
return false
}
if r1.Mode() != r2.Mode() {
return false
}
if r1.UID() != r2.UID() {
return false
}
if r1.GID() != r2.GID() {
return false
}
// TODO(dmcgowan): Check if is XAttrer
return compareResourceTypes(r1, r2)
}
func compareResourceTypes(r1, r2 continuity.Resource) bool {
switch t1 := r1.(type) {
case continuity.RegularFile:
t2, ok := r2.(continuity.RegularFile)
if !ok {
return false
}
return compareRegularFile(t1, t2)
case continuity.Directory:
t2, ok := r2.(continuity.Directory)
if !ok {
return false
}
return compareDirectory(t1, t2)
case continuity.SymLink:
t2, ok := r2.(continuity.SymLink)
if !ok {
return false
}
return compareSymLink(t1, t2)
case continuity.NamedPipe:
t2, ok := r2.(continuity.NamedPipe)
if !ok {
return false
}
return compareNamedPipe(t1, t2)
case continuity.Device:
t2, ok := r2.(continuity.Device)
if !ok {
return false
}
return compareDevice(t1, t2)
default:
// TODO(dmcgowan): Should this panic?
return r1 == r2
}
}
func compareRegularFile(r1, r2 continuity.RegularFile) bool {
if r1.Size() != r2.Size() {
return false
}
p1 := r1.Paths()
p2 := r2.Paths()
if len(p1) != len(p2) {
return false
}
for i := range p1 {
if p1[i] != p2[i] {
return false
}
}
d1 := r1.Digests()
d2 := r2.Digests()
if len(d1) != len(d2) {
return false
}
for i := range d1 {
if d1[i] != d2[i] {
return false
}
}
return true
}
func compareSymLink(r1, r2 continuity.SymLink) bool {
return r1.Target() == r2.Target()
}
func compareDirectory(r1, r2 continuity.Directory) bool {
return true
}
func compareNamedPipe(r1, r2 continuity.NamedPipe) bool {
return true
}
func compareDevice(r1, r2 continuity.Device) bool {
return r1.Major() == r2.Major() && r1.Minor() == r2.Minor()
}

109
fs/fstest/file.go Normal file
View file

@ -0,0 +1,109 @@
package fstest
import (
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/stevvooe/continuity/sysx"
)
// Applier applies single file changes
type Applier func(root string) error
// NewTestFile returns a file applier which creates a file as the
// provided name with the given content and permission.
func NewTestFile(name string, content []byte, perm os.FileMode) Applier {
return func(root string) error {
fullPath := filepath.Join(root, name)
if err := ioutil.WriteFile(fullPath, content, perm); err != nil {
return err
}
if err := os.Chmod(fullPath, perm); err != nil {
return err
}
return nil
}
}
// RemoveFile returns a file applier which removes the provided file name
func RemoveFile(name string) Applier {
return func(root string) error {
return os.RemoveAll(filepath.Join(root, name))
}
}
// CreateDirectory returns a file applier to create the directory with
// the provided name and permission
func CreateDirectory(name string, perm os.FileMode) Applier {
return func(root string) error {
fullPath := filepath.Join(root, name)
if err := os.MkdirAll(fullPath, perm); err != nil {
return err
}
return nil
}
}
// Rename returns a file applier which renames a file
func Rename(old, new string) Applier {
return func(root string) error {
return os.Rename(filepath.Join(root, old), filepath.Join(root, new))
}
}
// Chown returns a file applier which changes the ownership of a file
func Chown(name string, uid, gid int) Applier {
return func(root string) error {
return os.Chown(filepath.Join(root, name), uid, gid)
}
}
// Chtime changes access and mod time of file
func Chtime(name string, t time.Time) Applier {
return func(root string) error {
return os.Chtimes(filepath.Join(root, name), t, t)
}
}
// Symlink returns a file applier which creates a symbolic link
func Symlink(oldname, newname string) Applier {
return func(root string) error {
return os.Symlink(oldname, filepath.Join(root, newname))
}
}
// Link returns a file applier which creates a hard link
func Link(oldname, newname string) Applier {
return func(root string) error {
return os.Link(filepath.Join(root, oldname), filepath.Join(root, newname))
}
}
func SetXAttr(name, key, value string) Applier {
return func(root string) error {
return sysx.LSetxattr(name, key, []byte(value), 0)
}
}
// TODO: Make platform specific, windows applier is always no-op
//func Mknod(name string, mode int32, dev int) Applier {
// return func(root string) error {
// return return syscall.Mknod(path, mode, dev)
// }
//}
// MultiApply returns a new applier from the given appliers
func MultiApply(appliers ...Applier) Applier {
return func(root string) error {
for _, a := range appliers {
if err := a(root); err != nil {
return err
}
}
return nil
}
}