From b300237e77a7cfcac0e6caff3b4857cfb30ce4a6 Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Thu, 15 Sep 2016 20:39:46 +0300 Subject: [PATCH] Implement basic subvolume and snapshot commands. Exec-based send and receive. * better code generator * regenerate btrfs_tree.h constants * implement subvolume create, delete and snapshot create commands * exec-based snapshot send and receive --- btrfs.go | 48 ++++- btrfs_list.go | 13 ++ btrfs_tree_h.go | 23 --- btrfs_tree_hc.go | 482 +++++++++++++++++++++++++++++++++++++++++++++++ cmd/gbtrfs.go | 94 +++++++++ cmd/hgen.go | 43 ++++- errors.go | 2 +- headers.go | 4 + ioctl/ioctl.go | 4 - ioctl_h.go | 42 ++++- receive.go | 32 ++++ send.go | 46 +++++ subvolume.go | 132 ++++++++++++- utils.go | 37 ++++ 14 files changed, 954 insertions(+), 48 deletions(-) create mode 100644 btrfs_list.go delete mode 100644 btrfs_tree_h.go create mode 100644 btrfs_tree_hc.go create mode 100644 cmd/gbtrfs.go create mode 100644 headers.go create mode 100644 receive.go create mode 100644 send.go create mode 100644 utils.go diff --git a/btrfs.go b/btrfs.go index 5d8e054..478b43a 100644 --- a/btrfs.go +++ b/btrfs.go @@ -1,13 +1,13 @@ package btrfs import ( + "fmt" "github.com/dennwc/btrfs/ioctl" + "io" "os" + "path/filepath" ) -//go:generate go run cmd/hgen.go -p btrfs -o btrfs_tree_hc.go btrfs_tree.h -//go:generate go fmt -w btrfs_tree_hc.go - const SuperMagic = 0x9123683E func Open(path string) (*FS, error) { @@ -19,6 +19,12 @@ func Open(path string) (*FS, error) { dir, err := os.Open(path) if err != nil { return nil, err + } else if st, err := dir.Stat(); err != nil { + dir.Close() + return nil, err + } else if !st.IsDir() { + dir.Close() + return nil, fmt.Errorf("not a directory: %s", path) } return &FS{f: dir}, nil } @@ -139,3 +145,39 @@ func (f *FS) Sync() (err error) { } return ioctl.Do(f.f, _BTRFS_IOC_WAIT_SYNC, nil) } + +func (f *FS) CreateSubVolume(name string) error { + return CreateSubVolume(filepath.Join(f.f.Name(), name)) +} + +func (f *FS) DeleteSubVolume(name string) error { + return DeleteSubVolume(filepath.Join(f.f.Name(), name)) +} + +func (f *FS) Snapshot(dst string, ro bool) error { + return SnapshotSubVolume(f.f.Name(), filepath.Join(f.f.Name(), dst), ro) +} + +func (f *FS) SnapshotSubVolume(name string, dst string, ro bool) error { + return SnapshotSubVolume(filepath.Join(f.f.Name(), name), + filepath.Join(f.f.Name(), dst), ro) +} + +func (f *FS) Send(w io.Writer, parent string, subvols ...string) error { + if parent != "" { + parent = filepath.Join(f.f.Name(), parent) + } + sub := make([]string, 0, len(subvols)) + for _, s := range subvols { + sub = append(sub, filepath.Join(f.f.Name(), s)) + } + return Send(w, parent, sub...) +} + +func (f *FS) Receive(r io.Reader) error { + return Receive(r, f.f.Name()) +} + +func (f *FS) ReceiveTo(r io.Reader, mount string) error { + return Receive(r, filepath.Join(f.f.Name(), mount)) +} diff --git a/btrfs_list.go b/btrfs_list.go new file mode 100644 index 0000000..4b4a941 --- /dev/null +++ b/btrfs_list.go @@ -0,0 +1,13 @@ +package btrfs + +import "os" + +func getPathRootID(file *os.File) (uint64, error) { + args := btrfs_ioctl_ino_lookup_args{ + objectid: firstFreeObjectid, + } + if err := iocInoLookup(file, &args); err != nil { + return 0, err + } + return args.treeid, nil +} diff --git a/btrfs_tree_h.go b/btrfs_tree_h.go deleted file mode 100644 index e35273d..0000000 --- a/btrfs_tree_h.go +++ /dev/null @@ -1,23 +0,0 @@ -package btrfs - -// This header contains the structure definitions and constants used -// by file system objects that can be retrieved using -// the BTRFS_IOC_SEARCH_TREE ioctl. That means basically anything that -// is needed to describe a leaf node's key or item contents. - -const ( - // Holds pointers to all of the tree roots - BTRFS_ROOT_TREE_OBJECTID = 1 - - // Stores information about which extents are in use, and reference counts - BTRFS_EXTENT_TREE_OBJECTID = 2 - - // Chunk tree stores translations from logical -> physical block numbering - // the super block points to the chunk tree. - BTRFS_CHUNK_TREE_OBJECTID = 3 - - // All files have objectids in this range. - BTRFS_FIRST_FREE_OBJECTID = 256 - BTRFS_LAST_FREE_OBJECTID = 0xffffff00 // -256 - BTRFS_FIRST_CHUNK_TREE_OBJECTID = 256 -) diff --git a/btrfs_tree_hc.go b/btrfs_tree_hc.go new file mode 100644 index 0000000..5b74214 --- /dev/null +++ b/btrfs_tree_hc.go @@ -0,0 +1,482 @@ +package btrfs + +// This code was auto-generated; DO NOT EDIT! + +// This header contains the structure definitions and constants used +// by file system objects that can be retrieved using +// the BTRFS_IOC_SEARCH_TREE ioctl. That means basically anything that +// is needed to describe a leaf node's key or item contents. + +const ( + // Holds pointers to all of the tree roots + rootTreeObjectid = 1 + + // Stores information about which extents are in use, and reference counts + extentTreeObjectid = 2 + + // Chunk tree stores translations from logical -> physical block numbering + // the super block points to the chunk tree + chunkTreeObjectid = 3 + + // Stores information about which areas of a given device are in use. + // one per device. The tree of tree roots points to the device tree + devTreeObjectid = 4 + + // One per subvolume, storing files and directories + fsTreeObjectid = 5 + + // Directory objectid inside the root tree + rootTreeDirObjectid = 6 + + // Holds checksums of all the data extents + csumTreeObjectid = 7 + + // Holds quota configuration and tracking + quotaTreeObjectid = 8 + + // For storing items that use the BTRFS_UUID_KEY* types + uuidTreeObjectid = 9 + + // Tracks free space in block groups. + freeSpaceTreeObjectid = 10 + + // Device stats in the device tree + devStatsObjectid = 0 + + // For storing balance parameters in the root tree + balanceObjectid = 0xfffffffffffffffc /* -4 */ + + // Orhpan objectid for tracking unlinked/truncated files + orphanObjectid = 0xfffffffffffffffb /* -5 */ + + // Does write ahead logging to speed up fsyncs + treeLogObjectid = 0xfffffffffffffffa /* -6 */ + treeLogFixupObjectid = 0xfffffffffffffff9 /* -7 */ + + // For space balancing + treeRelocObjectid = 0xfffffffffffffff8 /* -8 */ + dataRelocTreeObjectid = 0xfffffffffffffff7 /* -9 */ + + // Extent checksums all have this objectid + // this allows them to share the logging tree + // for fsyncs + extentCsumObjectid = 0xfffffffffffffff6 /* -10 */ + + // For storing free space cache + freeSpaceObjectid = 0xfffffffffffffff5 /* -11 */ + + // The inode number assigned to the special inode for storing + // free ino cache + freeInoObjectid = 0xfffffffffffffff4 /* -12 */ + + // Dummy objectid represents multiple objectids + multipleObjectids = 0xffffffffffffff01 /* -255 */ + + // All files have objectids in this range. + firstFreeObjectid = 256 + lastFreeObjectid = 0xffffffffffffff00 /* -256 */ + firstChunkTreeObjectid = 256 + + // The device items go into the chunk tree. The key is in the form + // [ 1 BTRFS_DEV_ITEM_KEY device_id ] + devItemsObjectid = 1 + + btreeInodeObjectid = 1 + + emptySubvolDirObjectid = 2 + + devReplaceDevid = 0 + + // Inode items have the data typically returned from stat and store other + // info about object characteristics. There is one for every file and dir in + // the FS + inodeItemKey = 1 + inodeRefKey = 12 + inodeExtrefKey = 13 + xattrItemKey = 24 + orphanItemKey = 48 + // Reserve 2-15 close to the inode for later flexibility + + // Dir items are the name -> inode pointers in a directory. There is one + // for every name in a directory. + dirLogItemKey = 60 + dirLogIndexKey = 72 + dirItemKey = 84 + dirIndexKey = 96 + // Extent data is for file data + extentDataKey = 108 + + // Extent csums are stored in a separate tree and hold csums for + // an entire extent on disk. + extentCsumKey = 128 + + // Root items point to tree roots. They are typically in the root + // tree used by the super block to find all the other trees + rootItemKey = 132 + + // Root backrefs tie subvols and snapshots to the directory entries that + // reference them + rootBackrefKey = 144 + + // Root refs make a fast index for listing all of the snapshots and + // subvolumes referenced by a given root. They point directly to the + // directory item in the root that references the subvol + rootRefKey = 156 + + // Extent items are in the extent map tree. These record which blocks + // are used, and how many references there are to each block + extentItemKey = 168 + + // The same as the BTRFS_EXTENT_ITEM_KEY, except it's metadata we already know + // the length, so we save the level in key->offset instead of the length. + metadataItemKey = 169 + + treeBlockRefKey = 176 + + extentDataRefKey = 178 + + sharedBlockRefKey = 182 + + sharedDataRefKey = 184 + + // Block groups give us hints into the extent allocation trees. Which + // blocks are free etc etc + blockGroupItemKey = 192 + + // Every block group is represented in the free space tree by a free space info + // item, which stores some accounting information. It is keyed on + // (block_group_start, FREE_SPACE_INFO, block_group_length). + freeSpaceInfoKey = 198 + + // A free space extent tracks an extent of space that is free in a block group. + // It is keyed on (start, FREE_SPACE_EXTENT, length). + freeSpaceExtentKey = 199 + + // When a block group becomes very fragmented, we convert it to use bitmaps + // instead of extents. A free space bitmap is keyed on + // (start, FREE_SPACE_BITMAP, length); the corresponding item is a bitmap with + // (length / sectorsize) bits. + freeSpaceBitmapKey = 200 + + devExtentKey = 204 + devItemKey = 216 + chunkItemKey = 228 + + // Records the overall state of the qgroups. + // There's only one instance of this key present, + // (0, BTRFS_QGROUP_STATUS_KEY, 0) + qgroupStatusKey = 240 + // Records the currently used space of the qgroup. + // One key per qgroup, (0, BTRFS_QGROUP_INFO_KEY, qgroupid). + qgroupInfoKey = 242 + // Contains the user configured limits for the qgroup. + // One key per qgroup, (0, BTRFS_QGROUP_LIMIT_KEY, qgroupid). + qgroupLimitKey = 244 + // Records the child-parent relationship of qgroups. For + // each relation, 2 keys are present: + // (childid, BTRFS_QGROUP_RELATION_KEY, parentid) + // (parentid, BTRFS_QGROUP_RELATION_KEY, childid) + qgroupRelationKey = 246 + + // Obsolete name, see BTRFS_TEMPORARY_ITEM_KEY. + balanceItemKey = 248 + + // The key type for tree items that are stored persistently, but do not need to + // exist for extended period of time. The items can exist in any tree. + // [subtype, BTRFS_TEMPORARY_ITEM_KEY, data] + // Existing items: + // - balance status item + // (BTRFS_BALANCE_OBJECTID, BTRFS_TEMPORARY_ITEM_KEY, 0) + temporaryItemKey = 248 + + // Obsolete name, see BTRFS_PERSISTENT_ITEM_KEY + devStatsKey = 249 + + // The key type for tree items that are stored persistently and usually exist + // for a long period, eg. filesystem lifetime. The item kinds can be status + // information, stats or preference values. The item can exist in any tree. + // [subtype, BTRFS_PERSISTENT_ITEM_KEY, data] + // Existing items: + // - device statistics, store IO stats in the device tree, one key for all + // stats + // (BTRFS_DEV_STATS_OBJECTID, BTRFS_DEV_STATS_KEY, 0) + persistentItemKey = 249 + + // Persistantly stores the device replace state in the device tree. + // The key is built like this: (0, BTRFS_DEV_REPLACE_KEY, 0). + devReplaceKey = 250 + + // Stores items that allow to quickly map UUIDs to something else. + // These items are part of the filesystem UUID tree. + // The key is built like this: + // (UUID_upper_64_bits, BTRFS_UUID_KEY*, UUID_lower_64_bits). + uuidKeySubvol = 251 + uuidKeyReceivedSubvol = 252 + + // String items are for debugging. They just store a short string of + // data in the FS + stringItemKey = 253 + + // 32 bytes in various csum fields + csumSize = 32 + + // Csum types + + // Flags definitions for directory entry item type + // Used by: + // struct btrfs_dir_item.type + ftUnknown = 0 + ftRegFile = 1 + ftDir = 2 + ftChrdev = 3 + ftBlkdev = 4 + ftFifo = 5 + ftSock = 6 + ftSymlink = 7 + ftXattr = 8 + ftMax = 9 + + // The key defines the order in the tree, and so it also defines (optimal) + // block layout. + // objectid corresponds to the inode number. + // type tells us things about the object, and is a kind of stream selector. + // so for a given inode, keys with type of 1 might refer to the inode data, + // type of 2 may point to file data in the btree and type == 3 may point to + // extents. + // offset is the starting byte offset for this key in the stream. + // btrfs_disk_key is in disk byte order. struct btrfs_key is always + // in cpu native order. Otherwise they are identical and their sizes + // should be the same (ie both packed) + + // The internal btrfs device id + + // Size of the device + + // Bytes used + + // Optimal io alignment for this device + + // Optimal io width for this device + + // Minimal io size for this device + + // Type and info about this device + + // Expected generation for this device + + // Starting byte of this partition on the device, + // to allow for stripe alignment in the future + + // Grouping information for allocation decisions + + // Seek speed 0-100 where 100 is fastest + + // Bandwidth 0-100 where 100 is fastest + + // Btrfs generated uuid for this device + + // Uuid of FS who owns this device + + // Size of this chunk in bytes + + // Objectid of the root referencing this chunk + + // Optimal io alignment for this chunk + + // Optimal io width for this chunk + + // Minimal io size for this chunk + + // 2^16 stripes is quite a lot, a second limit is the size of a single + // item in the btree + + // Sub stripes only matter for raid10 + // Additional stripes go here + + freeSpaceExtent = 1 + freeSpaceBitmap = 2 + + headerFlagWritten = (1 << 0) + headerFlagReloc = (1 << 1) + + // Super block flags + // Errors detected + superFlagError = (1 << 2) + + superFlagSeeding = (1 << 32) + superFlagMetadump = (1 << 33) + + // Items in the extent btree are used to record the objectid of the + // owner of the block and the number of references + + extentFlagData = (1 << 0) + extentFlagTreeBlock = (1 << 1) + + // Following flags only apply to tree blocks + + // Use full backrefs for extent pointers in the block + blockFlagFullBackref = (1 << 8) + + // This flag is only used internally by scrub and may be changed at any time + // it is only declared here to avoid collisions + extentFlagSuper = (1 << 48) + + // Old style backrefs item + + // Dev extents record free space on individual devices. The owner + // field points back to the chunk allocation mapping tree that allocated + // the extent. The chunk tree uuid field is a way to double check the owner + + // Name goes here + + // Name goes here + + // Nfs style generation number + // Transid that last touched this inode + + // Modification sequence number for NFS + + // A little future expansion, for more than this we can + // just grow the inode item and version it + + rootSubvolRdonly = (1 << 0) + + // Internal in-memory flag that a subvolume has been marked for deletion but + // still visible as a directory + rootSubvolDead = (1 << 48) + + // The following fields appear after subvol_uuids+subvol_times + // were introduced. + + // This generation number is used to test if the new fields are valid + // and up to date while reading the root item. Every time the root item + // is written out, the "generation" field is copied into this field. If + // anyone ever mounted the fs with an older kernel, we will have + // mismatching generation values here and thus must invalidate the + // new fields. See btrfs_update_root and btrfs_find_last_root for + // details. + // the offset of generation_v2 is also used as the start for the memset + // when invalidating the fields. + + // This is used for both forward and backward root refs + + // Profiles to operate on, single is denoted by + // BTRFS_AVAIL_ALLOC_BIT_SINGLE + + // Usage filter + // BTRFS_BALANCE_ARGS_USAGE with a single value means '0..N' + // BTRFS_BALANCE_ARGS_USAGE_RANGE - range syntax, min..max + + // Devid filter + + // Devid subset filter [pstart..pend) + + // Btrfs virtual address space subset filter [vstart..vend) + + // Profile to convert to, single is denoted by + // BTRFS_AVAIL_ALLOC_BIT_SINGLE + + // BTRFS_BALANCE_ARGS_* + + // BTRFS_BALANCE_ARGS_LIMIT with value 'limit' + // BTRFS_BALANCE_ARGS_LIMIT_RANGE - the extend version can use minimum + // and maximum + + // Process chunks that cross stripes_min..stripes_max devices, + // BTRFS_BALANCE_ARGS_STRIPES_RANGE + + // Store balance parameters to disk so that balance can be properly + // resumed after crash or unmount + // BTRFS_BALANCE_* + + fileExtentInline = 0 + fileExtentReg = 1 + fileExtentPrealloc = 2 + + // Transaction id that created this extent + // Max number of bytes to hold this extent in ram + // when we split a compressed extent we can't know how big + // each of the resulting pieces will be. So, this is + // an upper limit on the size of the extent in ram instead of + // an exact limit. + + // 32 bits for the various ways we might encode the data, + // including compression and encryption. If any of these + // are set to something a given disk format doesn't understand + // it is treated like an incompat flag for reading and writing, + // but not for stat. + + // Are we inline data or a real extent? + + // Disk space consumed by the extent, checksum blocks are included + // in these numbers + // At this offset in the structure, the inline extent data start. + // The logical offset in file blocks (no csums) + // this extent record is for. This allows a file extent to point + // into the middle of an existing extent on disk, sharing it + // between two snapshots (useful if some bytes in the middle of the + // extent have changed + // The logical number of file blocks (no csums included). This + // always reflects the size uncompressed and without encoding. + + // Grow this item struct at the end for future enhancements and keep + // the existing values unchanged + + devReplaceItemContReadingFromSrcdevModeAlways = 0 + devReplaceItemContReadingFromSrcdevModeAvoid = 1 + devReplaceItemStateNeverStarted = 0 + devReplaceItemStateStarted = 1 + devReplaceItemStateSuspended = 2 + devReplaceItemStateFinished = 3 + devReplaceItemStateCanceled = 4 + + // Grow this item struct at the end for future enhancements and keep + // the existing values unchanged + + // Different types of block groups (and chunks) + blockGroupData = (1 << 0) + blockGroupSystem = (1 << 1) + blockGroupMetadata = (1 << 2) + blockGroupDup = (1 << 5) + + // We need a bit for restriper to be able to tell when chunks of type + // SINGLE are available. This "extended" profile format is used in + // fs_info->avail_*_alloc_bits (in-memory) and balance item fields + // (on-disk). The corresponding on-disk bit in chunk.type is reserved + // to avoid remappings between two formats in future. + availAllocBitSingle = (1 << 48) + + // A fake block group type that is used to communicate global block reserve + // size to userspace via the SPACE_INFO ioctl. + spaceInfoGlobalRsv = (1 << 49) + + freeSpaceUsingBitmaps = (1 << 0) + + qgroupLevelShift = 48 + + // Is subvolume quota turned on? + qgroupStatusFlagOn = (1 << 0) + // RESCAN is set during the initialization phase + qgroupStatusFlagRescan = (1 << 1) + // Some qgroup entries are known to be out of date, + // either because the configuration has changed in a way that + // makes a rescan necessary, or because the fs has been mounted + // with a non-qgroup-aware version. + // Turning qouta off and on again makes it inconsistent, too. + qgroupStatusFlagInconsistent = (1 << 2) + + qgroupStatusVersion = 1 + + // The generation is updated during every commit. As older + // versions of btrfs are not aware of qgroups, it will be + // possible to detect inconsistencies by checking the + // generation on mount time + + // Flag definitions see above + + // Only used during scanning to record the progress + // of the scan. It contains a logical address + + // Only updated when any of the other values change + +) diff --git a/cmd/gbtrfs.go b/cmd/gbtrfs.go new file mode 100644 index 0000000..2389992 --- /dev/null +++ b/cmd/gbtrfs.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "github.com/dennwc/btrfs" + "github.com/spf13/cobra" + "os" +) + +func init() { + RootCmd.AddCommand( + SubvolumeCmd, + SendCmd, + ReceiveCmd, + ) + + SubvolumeCmd.AddCommand( + SubvolumeCreateCmd, + SubvolumeDeleteCmd, + ) +} + +var RootCmd = &cobra.Command{ + Use: "btrfs [--help] [--version] [...] []", + Short: "Use --help as an argument for information on a specific group or command.", +} + +var SubvolumeCmd = &cobra.Command{ + Use: "subvolume ", +} + +var SubvolumeCreateCmd = &cobra.Command{ + Use: "create [-i ] [/]", + Short: "Create a subvolume", + Long: `Create a subvolume in . If is not given subvolume will be created in the current directory.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("subvolume not specified") + } else if len(args) > 1 { + return fmt.Errorf("only one subvolume name is allowed") + } + return btrfs.CreateSubVolume(args[0]) + }, +} + +var SubvolumeDeleteCmd = &cobra.Command{ + Use: "delete [options] [...]", + Short: "Delete subvolume(s)", + Long: `Delete subvolumes from the filesystem. The corresponding directory +is removed instantly but the data blocks are removed later. +The deletion does not involve full commit by default due to +performance reasons (as a consequence, the subvolume may appear again +after a crash). Use one of the --commit options to wait until the +operation is safely stored on the media.`, + RunE: func(cmd *cobra.Command, args []string) error { + for _, arg := range args { + if err := btrfs.DeleteSubVolume(arg); err != nil { + return err + } + } + return nil + }, +} + +var SendCmd = &cobra.Command{ + Use: "send [-ve] [-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...) + }, +} + +var ReceiveCmd = &cobra.Command{ + Use: "receive [-ve] [-f ] [--max-errors ] ", + Short: "Receive subvolumes from stdin.", + Long: `Receives one or more subvolumes that were previously +sent with btrfs send. The received subvolumes are stored +into .`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("expected one destination argument") + } + return btrfs.Receive(os.Stdin, args[0]) + }, +} + +func main() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(-1) + } +} diff --git a/cmd/hgen.go b/cmd/hgen.go index 33bbc9a..02616a4 100644 --- a/cmd/hgen.go +++ b/cmd/hgen.go @@ -9,20 +9,47 @@ import ( "log" "os" "regexp" + "strconv" "strings" "unicode" ) var ( - f_pkg = flag.String("p", "main", "package name for generated file") - f_out = flag.String("o", "-", "output file") + f_pkg = flag.String("p", "main", "package name for generated file") + f_out = flag.String("o", "-", "output file") + f_unexport = flag.Bool("u", true, "make all definitions unexported") + f_goname = flag.Bool("g", true, "rename symbols to follow Go conventions") + f_trim = flag.String("t", "", "prefix to trim from names") ) var ( - reDefineIntConst = regexp.MustCompile(`#define\s+([A-Za-z_]+)\s+(\(?\d+(?:U?LL)?(?:\s*<<\s*\d+)?\)?)`) + reDefineIntConst = regexp.MustCompile(`#define\s+([A-Za-z_]+)\s+(\(?-?\d+(?:U?LL)?(?:\s*<<\s*\d+)?\)?)`) + reNegULL = regexp.MustCompile(`-(\d+)ULL`) ) func constName(s string) string { + s = strings.TrimPrefix(s, *f_trim) + if *f_goname { + buf := bytes.NewBuffer(nil) + buf.Grow(len(s)) + up := !*f_unexport + for _, r := range s { + if r == '_' { + up = true + continue + } + if up { + up = false + r = unicode.ToUpper(r) + } else { + r = unicode.ToLower(r) + } + buf.WriteRune(r) + } + s = buf.String() + } else if *f_unexport { + s = "_" + s + } return s } @@ -41,6 +68,7 @@ func process(w io.Writer, path string) error { ) nl := true + fmt.Fprint(w, "// This code was auto-generated; DO NOT EDIT!\n\n") defer fmt.Fprintln(w, ")") for { line, err := r.ReadBytes('\n') @@ -96,6 +124,15 @@ func process(w io.Writer, path string) error { sub := reDefineIntConst.FindStringSubmatch(string(line)) if len(sub) > 0 { name, val := sub[1], sub[2] + if sub := reNegULL.FindAllStringSubmatch(val, -1); len(sub) > 0 { + for _, s := range sub { + v, err := strconv.ParseInt(s[1], 10, 64) + if err != nil { + panic(err) + } + val = strings.Replace(val, s[0], fmt.Sprintf("0x%x /* -%s */", uint64(-v), s[1]), -1) + } + } val = strings.Replace(val, "ULL", "", -1) fmt.Fprintf(w, "\t%s = %s\n", constName(name), val) continue diff --git a/errors.go b/errors.go index 5259ae7..cda99f8 100644 --- a/errors.go +++ b/errors.go @@ -7,7 +7,7 @@ type ErrNotBtrfs struct { } func (e ErrNotBtrfs) Error() string { - return fmt.Sprintf("path %q is not a btrfs", e.Path) + return fmt.Sprintf("not a btrfs filesystem: %s", e.Path) } // Error codes as returned by the kernel diff --git a/headers.go b/headers.go new file mode 100644 index 0000000..f0b0a0f --- /dev/null +++ b/headers.go @@ -0,0 +1,4 @@ +package btrfs + +//go:generate go run ./cmd/hgen.go -u -g -t BTRFS_ -p btrfs -o btrfs_tree_hc.go btrfs_tree.h +//go:generate gofmt -l -w btrfs_tree_hc.go diff --git a/ioctl/ioctl.go b/ioctl/ioctl.go index d2061a0..0571c5b 100644 --- a/ioctl/ioctl.go +++ b/ioctl/ioctl.go @@ -65,10 +65,6 @@ func Ioctl(f *os.File, ioc uintptr, addr uintptr) error { return nil } -func SizeOf(arg interface{}) uintptr { - return reflect.TypeOf(arg).Size() -} - func Do(f *os.File, ioc uintptr, arg interface{}) error { var addr uintptr if arg != nil { diff --git a/ioctl_h.go b/ioctl_h.go index 7613fa9..386246d 100644 --- a/ioctl_h.go +++ b/ioctl_h.go @@ -64,10 +64,26 @@ type btrfs_ioctl_vol_args_v2_u1 struct { const subvolNameMax = 4039 +type subvolFlags uint64 + +// flags for subvolumes +// +// Used by: +// struct btrfs_ioctl_vol_args_v2.flags +// +// BTRFS_SUBVOL_RDONLY is also provided/consumed by the following ioctls: +// - BTRFS_IOC_SUBVOL_GETFLAGS +// - BTRFS_IOC_SUBVOL_SETFLAGS +const ( + subvolCreateAsync = subvolFlags(1 << 0) + subvolReadOnly = subvolFlags(1 << 1) + subvolQGroupInherit = subvolFlags(1 << 2) +) + type btrfs_ioctl_vol_args_v2 struct { fd int64 transid uint64 - flags uint64 + flags subvolFlags btrfs_ioctl_vol_args_v2_u1 unused [2]uint64 name [subvolNameMax + 1]byte @@ -267,7 +283,7 @@ type btrfs_ioctl_search_key struct { max_type uint32 // how many items did userland ask for, and how many are we returning nr_items uint32 - unused [4 + 4*8]byte + unused [36]byte } type btrfs_ioctl_search_header struct { @@ -507,6 +523,8 @@ var ( _BTRFS_IOC_SPACE_INFO = ioctl.IOWR(ioctlMagic, 20, unsafe.Sizeof(btrfs_ioctl_space_args{})) _BTRFS_IOC_START_SYNC = ioctl.IOR(ioctlMagic, 24, 8) // uint64 _BTRFS_IOC_WAIT_SYNC = ioctl.IOW(ioctlMagic, 22, 8) // uint64 + _BTRFS_IOC_SNAP_CREATE_V2 = ioctl.IOW(ioctlMagic, 23, unsafe.Sizeof(btrfs_ioctl_vol_args_v2{})) + _BTRFS_IOC_SUBVOL_CREATE_V2 = ioctl.IOW(ioctlMagic, 24, unsafe.Sizeof(btrfs_ioctl_vol_args_v2{})) _BTRFS_IOC_SUBVOL_GETFLAGS = ioctl.IOR(ioctlMagic, 25, 8) // uint64 _BTRFS_IOC_SUBVOL_SETFLAGS = ioctl.IOW(ioctlMagic, 26, 8) // uint64 _BTRFS_IOC_SCRUB = ioctl.IOWR(ioctlMagic, 27, unsafe.Sizeof(btrfs_ioctl_scrub_args{})) @@ -538,8 +556,12 @@ var ( _BTRFS_IOC_GET_SUPPORTED_FEATURES = ioctl.IOR(ioctlMagic, 57, unsafe.Sizeof([3]btrfs_ioctl_feature_flags{})) ) -func iocSnapCreate(f *os.File, out *btrfs_ioctl_vol_args) error { - return ioctl.Do(f, _BTRFS_IOC_SNAP_CREATE, out) +func iocSnapCreate(f *os.File, in *btrfs_ioctl_vol_args) error { + return ioctl.Do(f, _BTRFS_IOC_SNAP_CREATE, in) +} + +func iocSnapCreateV2(f *os.File, in *btrfs_ioctl_vol_args_v2) error { + return ioctl.Do(f, _BTRFS_IOC_SNAP_CREATE_V2, in) } func iocDefrag(f *os.File, out *btrfs_ioctl_vol_args) error { @@ -586,12 +608,16 @@ func iocCloneRange(f *os.File, out *btrfs_ioctl_clone_range_args) error { return ioctl.Do(f, _BTRFS_IOC_CLONE_RANGE, out) } -func iocSubvolCreate(f *os.File, out *btrfs_ioctl_vol_args) error { - return ioctl.Do(f, _BTRFS_IOC_SUBVOL_CREATE, out) +func iocSubvolCreate(f *os.File, in *btrfs_ioctl_vol_args) error { + return ioctl.Do(f, _BTRFS_IOC_SUBVOL_CREATE, in) } -func iocSnapDestroy(f *os.File, out *btrfs_ioctl_vol_args) error { - return ioctl.Do(f, _BTRFS_IOC_SNAP_DESTROY, out) +func iocSubvolCreateV2(f *os.File, in *btrfs_ioctl_vol_args_v2) error { + return ioctl.Do(f, _BTRFS_IOC_SUBVOL_CREATE, in) +} + +func iocSnapDestroy(f *os.File, in *btrfs_ioctl_vol_args) error { + return ioctl.Do(f, _BTRFS_IOC_SNAP_DESTROY, in) } func iocDefragRange(f *os.File, out *btrfs_ioctl_defrag_range_args) error { diff --git a/receive.go b/receive.go new file mode 100644 index 0000000..3e2a74a --- /dev/null +++ b/receive.go @@ -0,0 +1,32 @@ +package btrfs + +import ( + "bytes" + "errors" + "io" + "os/exec" +) + +func Receive(r io.Reader, mount string) error { + // TODO: write a native implementation? + //tf, err := ioutil.TempFile("","btrfs_snap") + //if err != nil { + // return err + //} + //defer func(){ + // name := tf.Name() + // tf.Close() + // os.Remove(name) + //}() + buf := bytes.NewBuffer(nil) + cmd := exec.Command("btrfs", "receive", mount) + cmd.Stdin = r + cmd.Stderr = buf + if err := cmd.Run(); err != nil { + if buf.Len() != 0 { + return errors.New(buf.String()) + } + return err + } + return nil +} diff --git a/send.go b/send.go new file mode 100644 index 0000000..4c4b584 --- /dev/null +++ b/send.go @@ -0,0 +1,46 @@ +package btrfs + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "os" + "os/exec" +) + +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") + 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()) + } + return err + } + tf.Seek(0, 0) + _, err = io.Copy(w, tf) + return err +} diff --git a/subvolume.go b/subvolume.go index be7db56..42ee9c2 100644 --- a/subvolume.go +++ b/subvolume.go @@ -1,21 +1,141 @@ package btrfs import ( + "fmt" + "os" + "path/filepath" + "strings" "syscall" ) +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, err } - if st.Ino != BTRFS_FIRST_FREE_OBJECTID || + if st.Ino != firstFreeObjectid || st.Mode&syscall.S_IFMT != syscall.S_IFDIR { return false, nil } - var stfs syscall.Statfs_t - if err := syscall.Statfs(path, &stfs); err != nil { - return false, err - } - return stfs.Type == SuperMagic, 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 + } + // TODO: make SnapshotSubVolume a method on FS to use existing fd + f, err := openDir(subvol) + if err != nil { + return err + } + 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) + return iocSnapCreateV2(fdst, &args) } diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..9a6fe0d --- /dev/null +++ b/utils.go @@ -0,0 +1,37 @@ +package btrfs + +import ( + "fmt" + "os" + "syscall" +) + +func isBtrfs(path string) (bool, error) { + var stfs syscall.Statfs_t + if err := syscall.Statfs(path, &stfs); err != nil { + return false, err + } + return stfs.Type == SuperMagic, nil +} + +// openDir does the following checks before calling Open: +// 1: path is in a btrfs filesystem +// 2: path is a directory +func openDir(path string) (*os.File, error) { + if ok, err := isBtrfs(path); err != nil { + return nil, err + } else if !ok { + return nil, ErrNotBtrfs{Path: path} + } + file, err := os.Open(path) + if err != nil { + return nil, err + } else if st, err := file.Stat(); err != nil { + file.Close() + return nil, err + } else if !st.IsDir() { + file.Close() + return nil, fmt.Errorf("not a directory: %s", path) + } + return file, nil +}