From 6a4b966441d9dd44c811e68b30a503db54f8c03a Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Mon, 19 Sep 2016 20:01:22 +0300 Subject: [PATCH] native Send implementation --- btrfs.go | 17 ++++++- cmd/gbtrfs.go | 15 ++++-- ioctl_h.go | 39 +++++++++++---- send.go | 136 +++++++++++++++++++++++++++++++++++++++----------- subvolume.go | 2 +- utils.go | 74 +++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 46 deletions(-) diff --git a/btrfs.go b/btrfs.go index 478b43a..0100677 100644 --- a/btrfs.go +++ b/btrfs.go @@ -6,17 +6,26 @@ import ( "io" "os" "path/filepath" + "syscall" ) const SuperMagic = 0x9123683E -func Open(path string) (*FS, error) { +func Open(path string, ro bool) (*FS, error) { if ok, err := IsSubVolume(path); err != nil { return nil, err } else if !ok { return nil, ErrNotBtrfs{Path: path} } - dir, err := os.Open(path) + var ( + dir *os.File + err error + ) + if ro { + dir, err = os.OpenFile(path, os.O_RDONLY|syscall.O_NOATIME, 0644) + } else { + dir, err = os.Open(path) + } if err != nil { return nil, err } else if st, err := dir.Stat(); err != nil { @@ -139,6 +148,10 @@ func (f *FS) GetSupportedFeatures() (out FSFeatureFlags, err error) { return } +func (f *FS) GetFlags() (SubvolFlags, error) { + return iocSubvolGetflags(f.f) +} + func (f *FS) Sync() (err error) { if err = ioctl.Do(f.f, _BTRFS_IOC_START_SYNC, nil); err != nil { return diff --git a/cmd/gbtrfs.go b/cmd/gbtrfs.go index 3c42a77..dee48f0 100644 --- a/cmd/gbtrfs.go +++ b/cmd/gbtrfs.go @@ -19,6 +19,8 @@ func init() { SubvolumeDeleteCmd, SubvolumeListCmd, ) + + SendCmd.Flags().StringP("parent", "p", "", "Send an incremental stream from to .") } var RootCmd = &cobra.Command{ @@ -27,7 +29,8 @@ var RootCmd = &cobra.Command{ } var SubvolumeCmd = &cobra.Command{ - Use: "subvolume ", + Use: "subvolume ", + Aliases: []string{"subvol", "sub", "sv"}, } var SubvolumeCreateCmd = &cobra.Command{ @@ -64,8 +67,9 @@ operation is safely stored on the media.`, } var SubvolumeListCmd = &cobra.Command{ - Use: "list ", - Short: "List subvolumes", + Use: "list ", + Short: "List subvolumes", + Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return fmt.Errorf("expected one destination argument") @@ -81,12 +85,13 @@ var SubvolumeListCmd = &cobra.Command{ } var SendCmd = &cobra.Command{ - Use: "send [-ve] [-p ] [-c ] [-f ] [...]", + Use: "send [-v] [-p ] [-c ] [-f ] [...]", Short: "Send the subvolume(s) to stdout.", Long: `Sends the subvolume(s) specified by to stdout. should be read-only here.`, RunE: func(cmd *cobra.Command, args []string) error { - return btrfs.Send(os.Stdout, "", args...) + parent, _ := cmd.Flags().GetString("parent") + return btrfs.Send(os.Stdout, parent, args...) }, } diff --git a/ioctl_h.go b/ioctl_h.go index 1c73d5f..a0fcb98 100644 --- a/ioctl_h.go +++ b/ioctl_h.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "github.com/dennwc/btrfs/ioctl" "os" + "strconv" + "strings" "unsafe" ) @@ -86,7 +88,25 @@ type btrfs_ioctl_vol_args_v2_u1 struct { const subvolNameMax = 4039 -type subvolFlags uint64 +type SubvolFlags uint64 + +func (f SubvolFlags) ReadOnly() bool { + return f&SubvolReadOnly != 0 +} +func (f SubvolFlags) String() string { + if f == 0 { + return "" + } + var out []string + if f&SubvolReadOnly != 0 { + out = append(out, "RO") + f = f & (^SubvolReadOnly) + } + if f != 0 { + out = append(out, "0x"+strconv.FormatInt(int64(f), 16)) + } + return strings.Join(out, "|") +} // flags for subvolumes // @@ -97,15 +117,15 @@ type subvolFlags uint64 // - BTRFS_IOC_SUBVOL_GETFLAGS // - BTRFS_IOC_SUBVOL_SETFLAGS const ( - subvolCreateAsync = subvolFlags(1 << 0) - subvolReadOnly = subvolFlags(1 << 1) - subvolQGroupInherit = subvolFlags(1 << 2) + subvolCreateAsync = SubvolFlags(1 << 0) + SubvolReadOnly = SubvolFlags(1 << 1) + subvolQGroupInherit = SubvolFlags(1 << 2) ) type btrfs_ioctl_vol_args_v2 struct { fd int64 transid uint64 - flags subvolFlags + flags SubvolFlags btrfs_ioctl_vol_args_v2_u1 unused [2]uint64 name [subvolNameMax + 1]byte @@ -670,8 +690,9 @@ func iocWaitSync(f *os.File, out *uint64) error { return ioctl.Do(f, _BTRFS_IOC_WAIT_SYNC, out) } -func iocSubvolGetflags(f *os.File, out *uint64) error { - return ioctl.Do(f, _BTRFS_IOC_SUBVOL_GETFLAGS, out) +func iocSubvolGetflags(f *os.File) (out SubvolFlags, err error) { + err = ioctl.Do(f, _BTRFS_IOC_SUBVOL_GETFLAGS, &out) + return } func iocSubvolSetflags(f *os.File, out *uint64) error { @@ -714,8 +735,8 @@ func iocSetReceivedSubvol(f *os.File, out *btrfs_ioctl_received_subvol_args) err return ioctl.Do(f, _BTRFS_IOC_SET_RECEIVED_SUBVOL, out) } -func iocSend(f *os.File, out *btrfs_ioctl_send_args) error { - return ioctl.Do(f, _BTRFS_IOC_SEND, out) +func iocSend(f *os.File, in *btrfs_ioctl_send_args) error { + return ioctl.Do(f, _BTRFS_IOC_SEND, in) } func iocDevicesReady(f *os.File, out *btrfs_ioctl_vol_args) error { diff --git a/send.go b/send.go index 4c4b584..bfd6046 100644 --- a/send.go +++ b/send.go @@ -1,46 +1,124 @@ package btrfs import ( - "bytes" - "errors" + "fmt" "io" - "io/ioutil" "os" - "os/exec" + "path/filepath" ) func Send(w io.Writer, parent string, subvols ...string) error { if len(subvols) == 0 { return nil } - // TODO: write a native implementation? - args := []string{ - "send", - } - if parent != "" { - args = append(args, "-p", parent) - } - tf, err := ioutil.TempFile("", "btrfs_snap") + // use first send subvol to determine mount_root + subvol, err := filepath.Abs(subvols[0]) if err != nil { return err } - defer func() { - name := tf.Name() - tf.Close() - os.Remove(name) - }() - args = append(args, "-f", tf.Name()) - buf := bytes.NewBuffer(nil) - args = append(args, subvols...) - cmd := exec.Command("btrfs", args...) - cmd.Stderr = buf - if err = cmd.Run(); err != nil { - if buf.Len() != 0 { - return errors.New(buf.String()) - } + mountRoot, err := findMountRoot(subvol) + if err == os.ErrNotExist { + return fmt.Errorf("cannot find a mountpoint for %s", subvol) + } else if err != nil { return err } - tf.Seek(0, 0) - _, err = io.Copy(w, tf) - return err + var ( + cloneSrc []uint64 + parentID uint64 + ) + if parent != "" { + parent, err = filepath.Abs(parent) + if err != nil { + return err + } + f, err := os.Open(parent) + if err != nil { + return fmt.Errorf("cannot open parent: %v", err) + } + id, err := getPathRootID(f) + f.Close() + if err != nil { + return fmt.Errorf("cannot get parent root id: %v", err) + } + parentID = id + cloneSrc = append(cloneSrc, id) + } + // check all subvolumes + paths := make([]string, 0, len(subvols)) + for _, sub := range subvols { + sub, err = filepath.Abs(sub) + if err != nil { + return err + } + paths = append(paths, sub) + mount, err := findMountRoot(sub) + if err != nil { + return err + } else if mount != mountRoot { + return fmt.Errorf("all subvolumes must be from the same filesystem (%s is not)", sub) + } + ok, err := IsReadOnly(sub) + if err != nil { + return err + } else if !ok { + return fmt.Errorf("subvolume %s is not read-only", sub) + } + } + //full := len(cloneSrc) == 0 + for i, sub := range paths { + //if len(cloneSrc) > 1 { + // // TODO: find_good_parent + //} + //if !full { // TODO + // cloneSrc = append(cloneSrc, ) + //} + fs, err := Open(sub, true) + if err != nil { + return err + } + var flags uint64 + if i != 0 { // not first + flags |= _BTRFS_SEND_FLAG_OMIT_STREAM_HEADER + } + if i < len(paths)-1 { // not last + flags |= _BTRFS_SEND_FLAG_OMIT_END_CMD + } + err = send(w, fs.f, parentID, cloneSrc, flags) + if err != nil { + return fmt.Errorf("error sending %s: %v", sub, err) + } + } + return nil +} + +func send(w io.Writer, subvol *os.File, parent uint64, sources []uint64, flags uint64) error { + pr, pw, err := os.Pipe() + if err != nil { + return err + } + errc := make(chan error, 1) + go func() { + defer pr.Close() + _, err := io.Copy(w, pr) + errc <- err + }() + fd := pw.Fd() + wait := func() error { + pw.Close() + return <-errc + } + args := &btrfs_ioctl_send_args{ + send_fd: int64(fd), + parent_root: parent, + flags: flags, + } + if len(sources) != 0 { + args.clone_sources = &sources[0] + args.clone_sources_count = uint64(len(sources)) + } + if err := iocSend(subvol, args); err != nil { + wait() + return err + } + return wait() } diff --git a/subvolume.go b/subvolume.go index e860830..74d8207 100644 --- a/subvolume.go +++ b/subvolume.go @@ -131,7 +131,7 @@ func SnapshotSubVolume(subvol, dst string, ro bool) error { fd: int64(f.Fd()), } if ro { - args.flags |= subvolReadOnly + args.flags |= SubvolReadOnly } // TODO //if inherit != nil { diff --git a/utils.go b/utils.go index 6071f85..9151aa9 100644 --- a/utils.go +++ b/utils.go @@ -1,8 +1,12 @@ package btrfs import ( + "bufio" "fmt" + "io" "os" + "path/filepath" + "strings" "syscall" "unsafe" ) @@ -15,6 +19,76 @@ func isBtrfs(path string) (bool, error) { return stfs.Type == SuperMagic, nil } +func IsReadOnly(path string) (bool, error) { + fs, err := Open(path, true) + if err != nil { + return false, err + } + defer fs.Close() + f, err := fs.GetFlags() + if err != nil { + return false, err + } + return f.ReadOnly(), nil +} + +type mountPoint struct { + Dev string + Mount string + Type string + Opts string +} + +func getMounts() ([]mountPoint, error) { + file, err := os.Open("/etc/mtab") + if err != nil { + return nil, err + } + defer file.Close() + r := bufio.NewReader(file) + var out []mountPoint + for { + line, err := r.ReadString('\n') + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + fields := strings.Fields(line) + out = append(out, mountPoint{ + Dev: fields[0], + Mount: fields[1], + Type: fields[2], + Opts: fields[3], + }) + } + return out, nil +} + +func findMountRoot(path string) (string, error) { + mounts, err := getMounts() + if err != nil { + return "", err + } + longest := "" + isBtrfs := false + for _, m := range mounts { + if !strings.HasPrefix(path, m.Mount) { + continue + } + if len(longest) < len(m.Mount) { + longest = m.Mount + isBtrfs = m.Type == "btrfs" + } + } + if longest == "" { + return "", os.ErrNotExist + } else if !isBtrfs { + return "", ErrNotBtrfs{Path: longest} + } + return filepath.Abs(longest) +} + // openDir does the following checks before calling Open: // 1: path is in a btrfs filesystem // 2: path is a directory