native Send implementation

This commit is contained in:
Denys Smirnov 2016-09-19 20:01:22 +03:00
parent f03fa748e7
commit 6a4b966441
6 changed files with 237 additions and 46 deletions

View File

@ -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

View File

@ -19,6 +19,8 @@ func init() {
SubvolumeDeleteCmd,
SubvolumeListCmd,
)
SendCmd.Flags().StringP("parent", "p", "", "Send an incremental stream from <parent> to <subvol>.")
}
var RootCmd = &cobra.Command{
@ -27,7 +29,8 @@ var RootCmd = &cobra.Command{
}
var SubvolumeCmd = &cobra.Command{
Use: "subvolume <command> <args>",
Use: "subvolume <command> <args>",
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 <mount>",
Short: "List subvolumes",
Use: "list <mount>",
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 <parent>] [-c <clone-src>] [-f <outfile>] <subvol> [<subvol>...]",
Use: "send [-v] [-p <parent>] [-c <clone-src>] [-f <outfile>] <subvol> [<subvol>...]",
Short: "Send the subvolume(s) to stdout.",
Long: `Sends the subvolume(s) specified by <subvol> to stdout.
<subvol> 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...)
},
}

View File

@ -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 "<nil>"
}
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 {

136
send.go
View File

@ -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()
}

View File

@ -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 {

View File

@ -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