diff --git a/btrfs.go b/btrfs.go index aef7e10..a99204b 100644 --- a/btrfs.go +++ b/btrfs.go @@ -1,6 +1,7 @@ package btrfs import ( + "bytes" "fmt" "github.com/dennwc/btrfs/ioctl" "io" @@ -228,6 +229,8 @@ const ( ZLIB = Compression("zlib") ) +const xattrCompression = xattrPrefix + "compression" + func SetCompression(path string, v Compression) error { var value []byte if v != CompressionNone { @@ -237,9 +240,39 @@ func SetCompression(path string, v Compression) error { return err } } - err := syscall.Setxattr(path, xattrPrefix+"compression", value, 0) + err := syscall.Setxattr(path, xattrCompression, value, 0) if err != nil { return &os.PathError{Op: "setxattr", Path: path, Err: err} } return nil } + +func GetCompression(path string) (Compression, error) { + var buf []byte + for { + sz, err := syscall.Getxattr(path, xattrCompression, nil) + if err == syscall.ENODATA || sz == 0 { + return CompressionNone, nil + } else if err != nil { + return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err} + } + if cap(buf) < sz { + buf = make([]byte, sz) + } else { + buf = buf[:sz] + } + sz, err = syscall.Getxattr(path, xattrCompression, buf) + if err == syscall.ENODATA { + return CompressionNone, nil + } else if err == syscall.ERANGE { + // xattr changed by someone else, and is larger than our current buffer + continue + } else if err != nil { + return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err} + } + buf = buf[:sz] + break + } + buf = bytes.TrimSuffix(buf, []byte{0}) + return Compression(buf), nil +} diff --git a/btrfs_test.go b/btrfs_test.go new file mode 100644 index 0000000..e09abe1 --- /dev/null +++ b/btrfs_test.go @@ -0,0 +1,97 @@ +package btrfs + +import ( + "github.com/dennwc/btrfs/test" + "os" + "path/filepath" + "testing" +) + +const sizeDef = 256 * 1024 * 1024 + +func TestOpen(t *testing.T) { + dir, closer := btrfstest.New(t, sizeDef) + defer closer() + fs, err := Open(dir, true) + if err != nil { + t.Fatal(err) + } + if err = fs.Close(); err != nil { + t.Fatal(err) + } +} + +func TestIsSubvolume(t *testing.T) { + dir, closer := btrfstest.New(t, sizeDef) + defer closer() + + isSubvol := func(path string, expect bool) { + ok, err := IsSubVolume(path) + if err != nil { + t.Errorf("failed to check subvolume %v: %v", path, err) + return + } else if ok != expect { + t.Errorf("unexpected result for %v", path) + } + } + mkdir := func(path string) { + path = filepath.Join(dir, path) + if err := os.MkdirAll(path, 0755); err != nil { + t.Fatalf("cannot create dir %v: %v", path, err) + } + isSubvol(path, false) + } + + mksub := func(path string) { + path = filepath.Join(dir, path) + if err := CreateSubVolume(path); err != nil { + t.Fatalf("cannot create subvolume %v: %v", path, err) + } + isSubvol(path, true) + } + + mksub("v1") + + mkdir("v1/d2") + mksub("v1/v2") + + mkdir("v1/d2/d3") + mksub("v1/d2/v3") + + mkdir("v1/v2/d3") + mksub("v1/v2/v3") + + mkdir("d1") + + mkdir("d1/d2") + mksub("d1/v2") + + mkdir("d1/d2/d3") + mksub("d1/d2/v3") + + mkdir("d1/v2/d3") + mksub("d1/v2/v3") +} + +func TestCompression(t *testing.T) { + dir, closer := btrfstest.New(t, sizeDef) + defer closer() + fs, err := Open(dir, true) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + if err := fs.CreateSubVolume("sub"); err != nil { + t.Fatal(err) + } + path := filepath.Join(dir, "sub") + + if err := SetCompression(path, LZO); err != nil { + t.Fatal(err) + } + if c, err := GetCompression(path); err != nil { + t.Fatal(err) + } else if c != LZO { + t.Fatalf("unexpected compression returned: %q", string(c)) + } +} diff --git a/test/btrfstest.go b/test/btrfstest.go new file mode 100644 index 0000000..bb1cced --- /dev/null +++ b/test/btrfstest.go @@ -0,0 +1,106 @@ +package btrfstest + +import ( + "bytes" + "errors" + "io/ioutil" + "log" + "os" + "os/exec" + "strings" + "testing" + "time" +) + +func run(name string, args ...string) error { + buf := bytes.NewBuffer(nil) + cmd := exec.Command(name, args...) + cmd.Stdout = buf + cmd.Stderr = buf + err := cmd.Run() + if err == nil { + return nil + } else if buf.Len() == 0 { + return err + } + return errors.New("error: " + strings.TrimSpace(string(buf.Bytes()))) +} + +func Mkfs(file string, size int64) error { + f, err := os.Create(file) + if err != nil { + return err + } + if err = f.Truncate(size); err != nil { + f.Close() + return err + } + if err = f.Close(); err != nil { + return err + } + if err = run("mkfs.btrfs", file); err != nil { + os.Remove(file) + return err + } + return err +} + +func Mount(mount string, file string) error { + if err := run("mount", file, mount); err != nil { + return err + } + return nil +} + +func New(t testing.TB, size int64) (string, func()) { + f, err := ioutil.TempFile("", "btrfs_vol") + if err != nil { + t.Fatal(err) + } + name := f.Name() + f.Close() + rm := func() { + os.Remove(name) + } + if err = Mkfs(name, size); err != nil { + rm() + } + mount, err := ioutil.TempDir("", "btrfs_mount") + if err != nil { + rm() + t.Fatal(err) + } + if err = Mount(mount, name); err != nil { + rm() + os.RemoveAll(mount) + if txt := err.Error(); strings.Contains(txt, "permission denied") || + strings.Contains(txt, "only root") { + t.Skip(err) + } else { + t.Fatal(err) + } + } + done := false + return mount, func() { + if done { + return + } + for i := 0; i < 5; i++ { + if err := run("umount", mount); err == nil { + break + } else { + log.Println("umount failed:", err) + if strings.Contains(err.Error(), "busy") { + time.Sleep(time.Second) + } else { + break + } + } + } + if err := os.Remove(mount); err != nil { + log.Println("cleanup failed:", err) + } + rm() + done = true + } +}