mirror of
https://github.com/dennwc/btrfs
synced 2025-01-18 05:20:46 +00:00
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
This commit is contained in:
parent
2d0abe4795
commit
b300237e77
48
btrfs.go
48
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))
|
||||
}
|
||||
|
13
btrfs_list.go
Normal file
13
btrfs_list.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
)
|
482
btrfs_tree_hc.go
Normal file
482
btrfs_tree_hc.go
Normal file
@ -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
|
||||
|
||||
)
|
94
cmd/gbtrfs.go
Normal file
94
cmd/gbtrfs.go
Normal file
@ -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] <group> [<group>...] <command> [<args>]",
|
||||
Short: "Use --help as an argument for information on a specific group or command.",
|
||||
}
|
||||
|
||||
var SubvolumeCmd = &cobra.Command{
|
||||
Use: "subvolume <command> <args>",
|
||||
}
|
||||
|
||||
var SubvolumeCreateCmd = &cobra.Command{
|
||||
Use: "create [-i <qgroupid>] [<dest>/]<name>",
|
||||
Short: "Create a subvolume",
|
||||
Long: `Create a subvolume <name> in <dest>. If <dest> is not given subvolume <name> 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] <subvolume> [<subvolume>...]",
|
||||
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 <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...)
|
||||
},
|
||||
}
|
||||
|
||||
var ReceiveCmd = &cobra.Command{
|
||||
Use: "receive [-ve] [-f <infile>] [--max-errors <N>] <mount>",
|
||||
Short: "Receive subvolumes from stdin.",
|
||||
Long: `Receives one or more subvolumes that were previously
|
||||
sent with btrfs send. The received subvolumes are stored
|
||||
into <mount>.`,
|
||||
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)
|
||||
}
|
||||
}
|
43
cmd/hgen.go
43
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
|
||||
|
@ -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
|
||||
|
4
headers.go
Normal file
4
headers.go
Normal file
@ -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
|
@ -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 {
|
||||
|
42
ioctl_h.go
42
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 {
|
||||
|
32
receive.go
Normal file
32
receive.go
Normal file
@ -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
|
||||
}
|
46
send.go
Normal file
46
send.go
Normal file
@ -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
|
||||
}
|
132
subvolume.go
132
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)
|
||||
}
|
||||
|
37
utils.go
Normal file
37
utils.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user