forked from RepoMirrors/btrfs
native Send implementation
This commit is contained in:
parent
f03fa748e7
commit
6a4b966441
17
btrfs.go
17
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
|
||||
|
@ -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...)
|
||||
},
|
||||
}
|
||||
|
||||
|
39
ioctl_h.go
39
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 "<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
136
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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
74
utils.go
74
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
|
||||
|
Loading…
Reference in New Issue
Block a user