diff --git a/btrfs.go b/btrfs.go index a99204b..4477f86 100644 --- a/btrfs.go +++ b/btrfs.go @@ -1,7 +1,6 @@ package btrfs import ( - "bytes" "fmt" "github.com/dennwc/btrfs/ioctl" "io" @@ -12,8 +11,6 @@ import ( const SuperMagic = 0x9123683E -const xattrPrefix = "btrfs." - func Open(path string, ro bool) (*FS, error) { if ok, err := IsSubVolume(path); err != nil { return nil, err @@ -60,16 +57,16 @@ type Info struct { func (f *FS) Info() (out Info, err error) { var arg btrfs_ioctl_fs_info_args - if err = ioctl.Do(f.f, _BTRFS_IOC_FS_INFO, &arg); err != nil { - return - } - out = Info{ - MaxID: arg.max_id, - NumDevices: arg.num_devices, - FSID: arg.fsid, - NodeSize: arg.nodesize, - SectorSize: arg.sectorsize, - CloneAlignment: arg.clone_alignment, + arg, err = iocFsInfo(f.f) + if err == nil { + out = Info{ + MaxID: arg.max_id, + NumDevices: arg.num_devices, + FSID: arg.fsid, + NodeSize: arg.nodesize, + SectorSize: arg.sectorsize, + CloneAlignment: arg.clone_alignment, + } } return } @@ -160,10 +157,10 @@ func (f *FS) SetFlags(flags SubvolFlags) error { } func (f *FS) Sync() (err error) { - if err = ioctl.Do(f.f, _BTRFS_IOC_START_SYNC, nil); err != nil { + if err = ioctl.Ioctl(f.f, _BTRFS_IOC_START_SYNC, 0); err != nil { return } - return ioctl.Do(f.f, _BTRFS_IOC_WAIT_SYNC, nil) + return ioctl.Ioctl(f.f, _BTRFS_IOC_WAIT_SYNC, 0) } func (f *FS) CreateSubVolume(name string) error { @@ -221,58 +218,4 @@ func (f *FS) ListSubvolumes(filter func(Subvolume) bool) ([]Subvolume, error) { return out, nil } -type Compression string - -const ( - CompressionNone = Compression("") - LZO = Compression("lzo") - ZLIB = Compression("zlib") -) - -const xattrCompression = xattrPrefix + "compression" - -func SetCompression(path string, v Compression) error { - var value []byte - if v != CompressionNone { - var err error - value, err = syscall.ByteSliceFromString(string(v)) - if err != nil { - return err - } - } - err := syscall.Setxattr(path, xattrCompression, value, 0) - if err != nil { - return &os.PathError{Op: "setxattr", Path: path, Err: err} - } - return nil -} - -func GetCompression(path string) (Compression, error) { - var buf []byte - for { - sz, err := syscall.Getxattr(path, xattrCompression, nil) - if err == syscall.ENODATA || sz == 0 { - return CompressionNone, nil - } else if err != nil { - return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err} - } - if cap(buf) < sz { - buf = make([]byte, sz) - } else { - buf = buf[:sz] - } - sz, err = syscall.Getxattr(path, xattrCompression, buf) - if err == syscall.ENODATA { - return CompressionNone, nil - } else if err == syscall.ERANGE { - // xattr changed by someone else, and is larger than our current buffer - continue - } else if err != nil { - return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err} - } - buf = buf[:sz] - break - } - buf = bytes.TrimSuffix(buf, []byte{0}) - return Compression(buf), nil -} +func (f *FS) Usage() (UsageInfo, error) { return spaceUsage(f.f) } diff --git a/btrfs_tree.go b/btrfs_tree.go index a7e6f6b..5d0ebca 100644 --- a/btrfs_tree.go +++ b/btrfs_tree.go @@ -6,6 +6,9 @@ import ( ) const ( + _BTRFS_BLOCK_GROUP_TYPE_MASK = (blockGroupData | + blockGroupSystem | + blockGroupMetadata) _BTRFS_BLOCK_GROUP_PROFILE_MASK = (blockGroupRaid0 | blockGroupRaid1 | blockGroupRaid5 | diff --git a/ioctl_h.go b/ioctl_h.go index 8a8ef98..0691e89 100644 --- a/ioctl_h.go +++ b/ioctl_h.go @@ -678,8 +678,49 @@ func iocDefaultSubvol(f *os.File, out *uint64) error { return ioctl.Do(f, _BTRFS_IOC_DEFAULT_SUBVOL, out) } -func iocSpaceInfo(f *os.File, out *btrfs_ioctl_space_args) error { - return ioctl.Do(f, _BTRFS_IOC_SPACE_INFO, out) +type spaceInfo struct { + Flags uint64 + TotalBytes uint64 + UsedBytes uint64 +} + +func iocSpaceInfo(f *os.File) ([]spaceInfo, error) { + arg := &btrfs_ioctl_space_args{} + if err := ioctl.Do(f, _BTRFS_IOC_SPACE_INFO, arg); err != nil { + return nil, err + } + n := arg.total_spaces + if n == 0 { + return nil, nil + } + const ( + argSize = unsafe.Sizeof(btrfs_ioctl_space_args{}) + infoSize = unsafe.Sizeof(btrfs_ioctl_space_info{}) + ) + buf := make([]byte, argSize+uintptr(n)*infoSize) + basePtr := unsafe.Pointer(&buf[0]) + arg = (*btrfs_ioctl_space_args)(basePtr) + arg.space_slots = n + if err := ioctl.Do(f, _BTRFS_IOC_SPACE_INFO, arg); err != nil { + return nil, err + } else if arg.total_spaces == 0 { + return nil, nil + } + if n > arg.total_spaces { + n = arg.total_spaces + } + out := make([]spaceInfo, n) + ptr := uintptr(basePtr) + argSize + for i := 0; i < int(n); i++ { + info := (*btrfs_ioctl_space_info)(unsafe.Pointer(ptr)) + out[i] = spaceInfo{ + Flags: info.flags, + TotalBytes: info.total_bytes, + UsedBytes: info.used_bytes, + } + ptr += infoSize + } + return out, nil } func iocStartSync(f *os.File, out *uint64) error { @@ -712,8 +753,16 @@ func iocScrubProgress(f *os.File, out *btrfs_ioctl_scrub_args) error { return ioctl.Do(f, _BTRFS_IOC_SCRUB_PROGRESS, out) } -func iocDevInfo(f *os.File, out *btrfs_ioctl_dev_info_args) error { - return ioctl.Do(f, _BTRFS_IOC_DEV_INFO, out) +func iocFsInfo(f *os.File) (out btrfs_ioctl_fs_info_args, err error) { + err = ioctl.Do(f, _BTRFS_IOC_FS_INFO, &out) + return +} + +func iocDevInfo(f *os.File, devid uint64, uuid UUID) (out btrfs_ioctl_dev_info_args, err error) { + out.devid = devid + out.uuid = uuid + err = ioctl.Do(f, _BTRFS_IOC_DEV_INFO, &out) + return } func iocBalanceCtl(f *os.File, out *int32) error { diff --git a/usage.go b/usage.go new file mode 100644 index 0000000..6f10f6a --- /dev/null +++ b/usage.go @@ -0,0 +1,175 @@ +package btrfs + +import ( + "os" + "sort" + "syscall" +) + +func cmpChunkBlockGroup(f1, f2 uint64) int { + var mask uint64 + + if (f1 & _BTRFS_BLOCK_GROUP_TYPE_MASK) == + (f2 & _BTRFS_BLOCK_GROUP_TYPE_MASK) { + mask = _BTRFS_BLOCK_GROUP_PROFILE_MASK + } else if f2&blockGroupSystem != 0 { + return -1 + } else if f1&blockGroupSystem != 0 { + return +1 + } else { + mask = _BTRFS_BLOCK_GROUP_TYPE_MASK + } + + if (f1 & mask) > (f2 & mask) { + return +1 + } else if (f1 & mask) < (f2 & mask) { + return -1 + } else { + return 0 + } +} + +type spaceInfoByBlockGroup []spaceInfo + +func (a spaceInfoByBlockGroup) Len() int { return len(a) } +func (a spaceInfoByBlockGroup) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a spaceInfoByBlockGroup) Less(i, j int) bool { + return cmpChunkBlockGroup(a[i].Flags, a[j].Flags) < 0 +} + +type UsageInfo struct { + Total uint64 + TotalUnused uint64 + TotalUsed uint64 + TotalChunks uint64 + + FreeEstimated uint64 + FreeMin uint64 + + LogicalDataChunks uint64 + RawDataChunks uint64 + RawDataUsed uint64 + + LogicalMetaChunks uint64 + RawMetaChunks uint64 + RawMetaUsed uint64 + + SystemUsed uint64 + SystemChunks uint64 + + DataRatio float64 + MetadataRatio float64 + + GlobalReserve uint64 + GlobalReserveUsed uint64 +} + +const minUnallocatedThreshold = 16 * 1024 * 1024 + +func spaceUsage(f *os.File) (UsageInfo, error) { + info, err := iocFsInfo(f) + if err != nil { + return UsageInfo{}, err + } + var u UsageInfo + for i := uint64(0); i <= info.max_id; i++ { + dev, err := iocDevInfo(f, i, UUID{}) + if err == syscall.ENODEV { + continue + } else if err != nil { + return UsageInfo{}, err + } + u.Total += dev.total_bytes + } + + spaces, err := iocSpaceInfo(f) + if err != nil { + return UsageInfo{}, err + } + sort.Sort(spaceInfoByBlockGroup(spaces)) + var ( + maxDataRatio int = 1 + mixed bool + ) + for _, s := range spaces { + ratio := 1 + switch { + case s.Flags&blockGroupRaid0 != 0: + ratio = 1 + case s.Flags&blockGroupRaid1 != 0: + ratio = 2 + case s.Flags&blockGroupRaid5 != 0: + ratio = 0 + case s.Flags&blockGroupRaid6 != 0: + ratio = 0 + case s.Flags&blockGroupDup != 0: + ratio = 2 + case s.Flags&blockGroupRaid10 != 0: + ratio = 2 + } + if ratio > maxDataRatio { + maxDataRatio = ratio + } + if s.Flags&spaceInfoGlobalRsv != 0 { + u.GlobalReserve = s.TotalBytes + u.GlobalReserveUsed = s.UsedBytes + } + if s.Flags&(blockGroupData|blockGroupMetadata) == (blockGroupData | blockGroupMetadata) { + mixed = true + } + if s.Flags&blockGroupData != 0 { + u.RawDataUsed += s.UsedBytes * uint64(ratio) + u.RawDataChunks += s.TotalBytes * uint64(ratio) + u.LogicalDataChunks += s.TotalBytes + } + if s.Flags&blockGroupMetadata != 0 { + u.RawMetaUsed += s.UsedBytes * uint64(ratio) + u.RawMetaChunks += s.TotalBytes * uint64(ratio) + u.LogicalMetaChunks += s.TotalBytes + } + if s.Flags&blockGroupSystem != 0 { + u.SystemUsed += s.UsedBytes * uint64(ratio) + u.SystemChunks += s.TotalBytes * uint64(ratio) + } + } + u.TotalChunks = u.RawDataChunks + u.SystemChunks + u.TotalUsed = u.RawDataUsed + u.SystemUsed + if !mixed { + u.TotalChunks += u.RawMetaChunks + u.TotalUsed += u.RawMetaUsed + } + u.TotalUnused = u.Total - u.TotalChunks + + u.DataRatio = float64(u.RawDataChunks) / float64(u.LogicalDataChunks) + if mixed { + u.MetadataRatio = u.DataRatio + } else { + u.MetadataRatio = float64(u.RawMetaChunks) / float64(u.LogicalMetaChunks) + } + + // We're able to fill at least DATA for the unused space + // + // With mixed raid levels, this gives a rough estimate but more + // accurate than just counting the logical free space + // (l_data_chunks - l_data_used) + // + // In non-mixed case there's no difference. + u.FreeEstimated = uint64(float64(u.RawDataChunks-u.RawDataUsed) / u.DataRatio) + + // For mixed-bg the metadata are left out in calculations thus global + // reserve would be lost. Part of it could be permanently allocated, + // we have to subtract the used bytes so we don't go under zero free. + if mixed { + u.FreeEstimated -= u.GlobalReserve - u.GlobalReserveUsed + } + u.FreeMin = u.FreeEstimated + + // Chop unallocatable space + // FIXME: must be applied per device + if u.TotalUnused >= minUnallocatedThreshold { + u.FreeEstimated += uint64(float64(u.TotalUnused) / u.DataRatio) + // Match the calculation of 'df', use the highest raid ratio + u.FreeMin += u.TotalUnused / uint64(maxDataRatio) + } + return u, nil +} diff --git a/xattr.go b/xattr.go new file mode 100644 index 0000000..ae4b7d2 --- /dev/null +++ b/xattr.go @@ -0,0 +1,66 @@ +package btrfs + +import ( + "bytes" + "os" + "syscall" +) + +const ( + xattrPrefix = "btrfs." + xattrCompression = xattrPrefix + "compression" +) + +type Compression string + +const ( + CompressionNone = Compression("") + LZO = Compression("lzo") + ZLIB = Compression("zlib") +) + +func SetCompression(path string, v Compression) error { + var value []byte + if v != CompressionNone { + var err error + value, err = syscall.ByteSliceFromString(string(v)) + if err != nil { + return err + } + } + err := syscall.Setxattr(path, xattrCompression, value, 0) + if err != nil { + return &os.PathError{Op: "setxattr", Path: path, Err: err} + } + return nil +} + +func GetCompression(path string) (Compression, error) { + var buf []byte + for { + sz, err := syscall.Getxattr(path, xattrCompression, nil) + if err == syscall.ENODATA || sz == 0 { + return CompressionNone, nil + } else if err != nil { + return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err} + } + if cap(buf) < sz { + buf = make([]byte, sz) + } else { + buf = buf[:sz] + } + sz, err = syscall.Getxattr(path, xattrCompression, buf) + if err == syscall.ENODATA { + return CompressionNone, nil + } else if err == syscall.ERANGE { + // xattr changed by someone else, and is larger than our current buffer + continue + } else if err != nil { + return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err} + } + buf = buf[:sz] + break + } + buf = bytes.TrimSuffix(buf, []byte{0}) + return Compression(buf), nil +}