forked from RepoMirrors/btrfs
380 lines
8.5 KiB
Go
380 lines
8.5 KiB
Go
package btrfs
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
func checkSubVolumeName(name string) bool {
|
|
return name != "" && name[0] != 0 && !strings.ContainsRune(name, '/') &&
|
|
name != "." && name != ".."
|
|
}
|
|
|
|
func IsSubVolume(path string) (bool, error) {
|
|
var st syscall.Stat_t
|
|
if err := syscall.Stat(path, &st); err != nil {
|
|
return false, &os.PathError{Op: "stat", Path: path, Err: err}
|
|
}
|
|
if objectID(st.Ino) != firstFreeObjectid ||
|
|
st.Mode&syscall.S_IFMT != syscall.S_IFDIR {
|
|
return false, nil
|
|
}
|
|
return isBtrfs(path)
|
|
}
|
|
|
|
func CreateSubVolume(path string) error {
|
|
var inherit *btrfs_qgroup_inherit // TODO
|
|
|
|
cpath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newName := filepath.Base(cpath)
|
|
dstDir := filepath.Dir(cpath)
|
|
if !checkSubVolumeName(newName) {
|
|
return fmt.Errorf("invalid subvolume name: %s", newName)
|
|
} else if len(newName) >= volNameMax {
|
|
return fmt.Errorf("subvolume name too long: %s", newName)
|
|
}
|
|
dst, err := openDir(dstDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dst.Close()
|
|
if inherit != nil {
|
|
panic("not implemented") // TODO
|
|
args := btrfs_ioctl_vol_args_v2{
|
|
flags: subvolQGroupInherit,
|
|
btrfs_ioctl_vol_args_v2_u1: btrfs_ioctl_vol_args_v2_u1{
|
|
//size: qgroup_inherit_size(inherit),
|
|
qgroup_inherit: inherit,
|
|
},
|
|
}
|
|
copy(args.name[:], newName)
|
|
return iocSubvolCreateV2(dst, &args)
|
|
}
|
|
var args btrfs_ioctl_vol_args
|
|
copy(args.name[:], newName)
|
|
return iocSubvolCreate(dst, &args)
|
|
}
|
|
|
|
func DeleteSubVolume(path string) error {
|
|
if ok, err := IsSubVolume(path); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return fmt.Errorf("not a subvolume: %s", path)
|
|
}
|
|
cpath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dname := filepath.Dir(cpath)
|
|
vname := filepath.Base(cpath)
|
|
|
|
dir, err := openDir(dname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dir.Close()
|
|
var args btrfs_ioctl_vol_args
|
|
copy(args.name[:], vname)
|
|
return iocSnapDestroy(dir, &args)
|
|
}
|
|
|
|
func SnapshotSubVolume(subvol, dst string, ro bool) error {
|
|
if ok, err := IsSubVolume(subvol); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return fmt.Errorf("not a subvolume: %s", subvol)
|
|
}
|
|
exists := false
|
|
if st, err := os.Stat(dst); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
} else if err == nil {
|
|
if !st.IsDir() {
|
|
return fmt.Errorf("'%s' exists and it is not a directory", dst)
|
|
}
|
|
exists = true
|
|
}
|
|
var (
|
|
newName string
|
|
dstDir string
|
|
)
|
|
if exists {
|
|
newName = filepath.Base(subvol)
|
|
dstDir = dst
|
|
} else {
|
|
newName = filepath.Base(dst)
|
|
dstDir = filepath.Dir(dst)
|
|
}
|
|
if !checkSubVolumeName(newName) {
|
|
return fmt.Errorf("invalid snapshot name '%s'", newName)
|
|
} else if len(newName) >= volNameMax {
|
|
return fmt.Errorf("snapshot name too long '%s'", newName)
|
|
}
|
|
fdst, err := openDir(dstDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fdst.Close()
|
|
// TODO: make SnapshotSubVolume a method on FS to use existing fd
|
|
f, err := openDir(subvol)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot open dest dir: %v", err)
|
|
}
|
|
defer f.Close()
|
|
args := btrfs_ioctl_vol_args_v2{
|
|
fd: int64(f.Fd()),
|
|
}
|
|
if ro {
|
|
args.flags |= SubvolReadOnly
|
|
}
|
|
// TODO
|
|
//if inherit != nil {
|
|
// args.flags |= subvolQGroupInherit
|
|
// args.size = qgroup_inherit_size(inherit)
|
|
// args.qgroup_inherit = inherit
|
|
//}
|
|
copy(args.name[:], newName)
|
|
if err := iocSnapCreateV2(fdst, &args); err != nil {
|
|
return fmt.Errorf("snapshot create failed: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsReadOnly(path string) (bool, error) {
|
|
f, err := GetFlags(path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return f.ReadOnly(), nil
|
|
}
|
|
|
|
func GetFlags(path string) (SubvolFlags, error) {
|
|
fs, err := Open(path, true)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer fs.Close()
|
|
return fs.GetFlags()
|
|
}
|
|
|
|
func listSubVolumes(f *os.File, filter func(SubvolInfo) bool) (map[objectID]SubvolInfo, error) {
|
|
sk := btrfs_ioctl_search_key{
|
|
// search in the tree of tree roots
|
|
tree_id: rootTreeObjectid,
|
|
|
|
// Set the min and max to backref keys. The search will
|
|
// only send back this type of key now.
|
|
min_type: rootItemKey,
|
|
max_type: rootBackrefKey,
|
|
|
|
min_objectid: firstFreeObjectid,
|
|
|
|
// Set all the other params to the max, we'll take any objectid
|
|
// and any trans.
|
|
max_objectid: lastFreeObjectid,
|
|
max_offset: maxUint64,
|
|
max_transid: maxUint64,
|
|
|
|
nr_items: 4096, // just a big number, doesn't matter much
|
|
}
|
|
m := make(map[objectID]SubvolInfo)
|
|
for {
|
|
out, err := treeSearchRaw(f, sk)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if len(out) == 0 {
|
|
break
|
|
}
|
|
for _, obj := range out {
|
|
switch obj.Type {
|
|
//case rootBackrefKey:
|
|
// ref := asRootRef(obj.Data)
|
|
// o := m[obj.ObjectID]
|
|
// o.TransID = obj.TransID
|
|
// o.ObjectID = obj.ObjectID
|
|
// o.RefTree = obj.Offset
|
|
// o.DirID = ref.DirID
|
|
// o.Name = ref.Name
|
|
// m[obj.ObjectID] = o
|
|
case rootItemKey:
|
|
o := m[obj.ObjectID]
|
|
o.RootID = obj.ObjectID
|
|
robj := asRootItem(obj.Data).Decode()
|
|
o.fillFromItem(&robj)
|
|
m[obj.ObjectID] = o
|
|
}
|
|
}
|
|
// record the mins in key so we can make sure the
|
|
// next search doesn't repeat this root
|
|
last := out[len(out)-1]
|
|
sk.min_objectid = last.ObjectID
|
|
sk.min_type = last.Type
|
|
sk.min_offset = last.Offset + 1
|
|
if sk.min_offset == 0 { // overflow
|
|
sk.min_type++
|
|
} else {
|
|
continue
|
|
}
|
|
if sk.min_type > rootBackrefKey {
|
|
sk.min_type = rootItemKey
|
|
sk.min_objectid++
|
|
} else {
|
|
continue
|
|
}
|
|
if sk.min_objectid > sk.max_objectid {
|
|
break
|
|
}
|
|
}
|
|
// resolve paths
|
|
for id, v := range m {
|
|
if path, err := subvolidResolve(f, id); err == ErrNotFound {
|
|
delete(m, id)
|
|
continue
|
|
} else if err != nil {
|
|
return m, fmt.Errorf("cannot resolve path for %v: %v", id, err)
|
|
} else {
|
|
v.Path = path
|
|
m[id] = v
|
|
}
|
|
if filter != nil && !filter(v) {
|
|
delete(m, id)
|
|
}
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
type SubvolInfo struct {
|
|
RootID objectID
|
|
|
|
UUID UUID
|
|
ParentUUID UUID
|
|
ReceivedUUID UUID
|
|
|
|
CTime time.Time
|
|
OTime time.Time
|
|
STime time.Time
|
|
RTime time.Time
|
|
|
|
CTransID uint64
|
|
OTransID uint64
|
|
STransID uint64
|
|
RTransID uint64
|
|
|
|
Path string
|
|
}
|
|
|
|
func (s *SubvolInfo) fillFromItem(it *rootItem) {
|
|
s.UUID = it.UUID
|
|
s.ReceivedUUID = it.ReceivedUUID
|
|
s.ParentUUID = it.ParentUUID
|
|
|
|
s.CTime = it.CTime
|
|
s.OTime = it.OTime
|
|
s.STime = it.STime
|
|
s.RTime = it.RTime
|
|
|
|
s.CTransID = it.CTransID
|
|
s.OTransID = it.OTransID
|
|
s.STransID = it.STransID
|
|
s.RTransID = it.RTransID
|
|
}
|
|
|
|
func subvolSearchByUUID(mnt *os.File, uuid UUID) (*SubvolInfo, error) {
|
|
id, err := lookupUUIDSubvolItem(mnt, uuid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return subvolSearchByRootID(mnt, id, "")
|
|
}
|
|
|
|
func subvolSearchByReceivedUUID(mnt *os.File, uuid UUID) (*SubvolInfo, error) {
|
|
id, err := lookupUUIDReceivedSubvolItem(mnt, uuid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return subvolSearchByRootID(mnt, id, "")
|
|
}
|
|
|
|
func subvolSearchByPath(mnt *os.File, path string) (*SubvolInfo, error) {
|
|
if !filepath.IsAbs(path) {
|
|
path = filepath.Join(mnt.Name(), path)
|
|
}
|
|
id, err := getPathRootID(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return subvolSearchByRootID(mnt, id, path)
|
|
}
|
|
|
|
func subvolidResolve(mnt *os.File, subvolID objectID) (string, error) {
|
|
return subvolidResolveSub(mnt, "", subvolID)
|
|
}
|
|
|
|
func subvolidResolveSub(mnt *os.File, path string, subvolID objectID) (string, error) {
|
|
if subvolID == fsTreeObjectid {
|
|
return "", nil
|
|
}
|
|
sk := btrfs_ioctl_search_key{
|
|
tree_id: rootTreeObjectid,
|
|
min_objectid: subvolID,
|
|
max_objectid: subvolID,
|
|
min_type: rootBackrefKey,
|
|
max_type: rootBackrefKey,
|
|
max_offset: maxUint64,
|
|
max_transid: maxUint64,
|
|
nr_items: 1,
|
|
}
|
|
results, err := treeSearchRaw(mnt, sk)
|
|
if err != nil {
|
|
return "", err
|
|
} else if len(results) < 1 {
|
|
return "", ErrNotFound
|
|
}
|
|
res := results[0]
|
|
if objectID(res.Offset) != fsTreeObjectid {
|
|
spath, err := subvolidResolveSub(mnt, path, objectID(res.Offset))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
path = spath + "/"
|
|
}
|
|
backRef := asRootRef(res.Data)
|
|
if backRef.DirID != firstFreeObjectid {
|
|
arg := btrfs_ioctl_ino_lookup_args{
|
|
treeid: objectID(res.Offset),
|
|
objectid: backRef.DirID,
|
|
}
|
|
if err := iocInoLookup(mnt, &arg); err != nil {
|
|
return "", err
|
|
}
|
|
path += arg.Name()
|
|
}
|
|
return path + backRef.Name, nil
|
|
}
|
|
|
|
// subvolSearchByRootID
|
|
//
|
|
// Path is optional, and will be resolved automatically if not set.
|
|
func subvolSearchByRootID(mnt *os.File, rootID objectID, path string) (*SubvolInfo, error) {
|
|
robj, err := readRootItem(mnt, rootID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info := &SubvolInfo{
|
|
RootID: rootID,
|
|
Path: path,
|
|
}
|
|
info.fillFromItem(robj)
|
|
if path == "" {
|
|
info.Path, err = subvolidResolve(mnt, info.RootID)
|
|
}
|
|
return info, err
|
|
}
|