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"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
const SuperMagic = 0x9123683E
|
const SuperMagic = 0x9123683E
|
||||||
|
|
||||||
func Open(path string) (*FS, error) {
|
func Open(path string, ro bool) (*FS, error) {
|
||||||
if ok, err := IsSubVolume(path); err != nil {
|
if ok, err := IsSubVolume(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
return nil, ErrNotBtrfs{Path: path}
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if st, err := dir.Stat(); err != nil {
|
} else if st, err := dir.Stat(); err != nil {
|
||||||
@ -139,6 +148,10 @@ func (f *FS) GetSupportedFeatures() (out FSFeatureFlags, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FS) GetFlags() (SubvolFlags, error) {
|
||||||
|
return iocSubvolGetflags(f.f)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FS) Sync() (err error) {
|
func (f *FS) Sync() (err error) {
|
||||||
if err = ioctl.Do(f.f, _BTRFS_IOC_START_SYNC, nil); err != nil {
|
if err = ioctl.Do(f.f, _BTRFS_IOC_START_SYNC, nil); err != nil {
|
||||||
return
|
return
|
||||||
|
@ -19,6 +19,8 @@ func init() {
|
|||||||
SubvolumeDeleteCmd,
|
SubvolumeDeleteCmd,
|
||||||
SubvolumeListCmd,
|
SubvolumeListCmd,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SendCmd.Flags().StringP("parent", "p", "", "Send an incremental stream from <parent> to <subvol>.")
|
||||||
}
|
}
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
var RootCmd = &cobra.Command{
|
||||||
@ -27,7 +29,8 @@ var RootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var SubvolumeCmd = &cobra.Command{
|
var SubvolumeCmd = &cobra.Command{
|
||||||
Use: "subvolume <command> <args>",
|
Use: "subvolume <command> <args>",
|
||||||
|
Aliases: []string{"subvol", "sub", "sv"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var SubvolumeCreateCmd = &cobra.Command{
|
var SubvolumeCreateCmd = &cobra.Command{
|
||||||
@ -64,8 +67,9 @@ operation is safely stored on the media.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var SubvolumeListCmd = &cobra.Command{
|
var SubvolumeListCmd = &cobra.Command{
|
||||||
Use: "list <mount>",
|
Use: "list <mount>",
|
||||||
Short: "List subvolumes",
|
Short: "List subvolumes",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return fmt.Errorf("expected one destination argument")
|
return fmt.Errorf("expected one destination argument")
|
||||||
@ -81,12 +85,13 @@ var SubvolumeListCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var SendCmd = &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.",
|
Short: "Send the subvolume(s) to stdout.",
|
||||||
Long: `Sends the subvolume(s) specified by <subvol> to stdout.
|
Long: `Sends the subvolume(s) specified by <subvol> to stdout.
|
||||||
<subvol> should be read-only here.`,
|
<subvol> should be read-only here.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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"
|
"encoding/hex"
|
||||||
"github.com/dennwc/btrfs/ioctl"
|
"github.com/dennwc/btrfs/ioctl"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,7 +88,25 @@ type btrfs_ioctl_vol_args_v2_u1 struct {
|
|||||||
|
|
||||||
const subvolNameMax = 4039
|
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
|
// flags for subvolumes
|
||||||
//
|
//
|
||||||
@ -97,15 +117,15 @@ type subvolFlags uint64
|
|||||||
// - BTRFS_IOC_SUBVOL_GETFLAGS
|
// - BTRFS_IOC_SUBVOL_GETFLAGS
|
||||||
// - BTRFS_IOC_SUBVOL_SETFLAGS
|
// - BTRFS_IOC_SUBVOL_SETFLAGS
|
||||||
const (
|
const (
|
||||||
subvolCreateAsync = subvolFlags(1 << 0)
|
subvolCreateAsync = SubvolFlags(1 << 0)
|
||||||
subvolReadOnly = subvolFlags(1 << 1)
|
SubvolReadOnly = SubvolFlags(1 << 1)
|
||||||
subvolQGroupInherit = subvolFlags(1 << 2)
|
subvolQGroupInherit = SubvolFlags(1 << 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
type btrfs_ioctl_vol_args_v2 struct {
|
type btrfs_ioctl_vol_args_v2 struct {
|
||||||
fd int64
|
fd int64
|
||||||
transid uint64
|
transid uint64
|
||||||
flags subvolFlags
|
flags SubvolFlags
|
||||||
btrfs_ioctl_vol_args_v2_u1
|
btrfs_ioctl_vol_args_v2_u1
|
||||||
unused [2]uint64
|
unused [2]uint64
|
||||||
name [subvolNameMax + 1]byte
|
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)
|
return ioctl.Do(f, _BTRFS_IOC_WAIT_SYNC, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func iocSubvolGetflags(f *os.File, out *uint64) error {
|
func iocSubvolGetflags(f *os.File) (out SubvolFlags, err error) {
|
||||||
return ioctl.Do(f, _BTRFS_IOC_SUBVOL_GETFLAGS, out)
|
err = ioctl.Do(f, _BTRFS_IOC_SUBVOL_GETFLAGS, &out)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func iocSubvolSetflags(f *os.File, out *uint64) error {
|
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)
|
return ioctl.Do(f, _BTRFS_IOC_SET_RECEIVED_SUBVOL, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func iocSend(f *os.File, out *btrfs_ioctl_send_args) error {
|
func iocSend(f *os.File, in *btrfs_ioctl_send_args) error {
|
||||||
return ioctl.Do(f, _BTRFS_IOC_SEND, out)
|
return ioctl.Do(f, _BTRFS_IOC_SEND, in)
|
||||||
}
|
}
|
||||||
|
|
||||||
func iocDevicesReady(f *os.File, out *btrfs_ioctl_vol_args) error {
|
func iocDevicesReady(f *os.File, out *btrfs_ioctl_vol_args) error {
|
||||||
|
136
send.go
136
send.go
@ -1,46 +1,124 @@
|
|||||||
package btrfs
|
package btrfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"fmt"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Send(w io.Writer, parent string, subvols ...string) error {
|
func Send(w io.Writer, parent string, subvols ...string) error {
|
||||||
if len(subvols) == 0 {
|
if len(subvols) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// TODO: write a native implementation?
|
// use first send subvol to determine mount_root
|
||||||
args := []string{
|
subvol, err := filepath.Abs(subvols[0])
|
||||||
"send",
|
|
||||||
}
|
|
||||||
if parent != "" {
|
|
||||||
args = append(args, "-p", parent)
|
|
||||||
}
|
|
||||||
tf, err := ioutil.TempFile("", "btrfs_snap")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
mountRoot, err := findMountRoot(subvol)
|
||||||
name := tf.Name()
|
if err == os.ErrNotExist {
|
||||||
tf.Close()
|
return fmt.Errorf("cannot find a mountpoint for %s", subvol)
|
||||||
os.Remove(name)
|
} else if err != nil {
|
||||||
}()
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tf.Seek(0, 0)
|
var (
|
||||||
_, err = io.Copy(w, tf)
|
cloneSrc []uint64
|
||||||
return err
|
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()),
|
fd: int64(f.Fd()),
|
||||||
}
|
}
|
||||||
if ro {
|
if ro {
|
||||||
args.flags |= subvolReadOnly
|
args.flags |= SubvolReadOnly
|
||||||
}
|
}
|
||||||
// TODO
|
// TODO
|
||||||
//if inherit != nil {
|
//if inherit != nil {
|
||||||
|
74
utils.go
74
utils.go
@ -1,8 +1,12 @@
|
|||||||
package btrfs
|
package btrfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@ -15,6 +19,76 @@ func isBtrfs(path string) (bool, error) {
|
|||||||
return stfs.Type == SuperMagic, nil
|
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:
|
// openDir does the following checks before calling Open:
|
||||||
// 1: path is in a btrfs filesystem
|
// 1: path is in a btrfs filesystem
|
||||||
// 2: path is a directory
|
// 2: path is a directory
|
||||||
|
Loading…
Reference in New Issue
Block a user