//go:build linux package system import ( "fmt" "os" "os/exec" "path/filepath" "strings" "syscall" ) // IsMountPoint checks if a path is on a different device than its parent. // Returns true if the path is a mount point (different device ID from parent). func IsMountPoint(path string) bool { var pathStat, parentStat syscall.Stat_t if err := syscall.Stat(path, &pathStat); err != nil { return false } parent := filepath.Dir(path) if err := syscall.Stat(parent, &parentStat); err != nil { return false } return pathStat.Dev != parentStat.Dev } // IsWritable checks if the given path is writable by attempting to create+remove a temp file. func IsWritable(path string) bool { testFile := filepath.Join(path, ".felhom-write-test") f, err := os.Create(testFile) if err != nil { return false } f.Close() os.Remove(testFile) return true } // PathsOverlap returns true if one path is a parent or child of the other. func PathsOverlap(a, b string) bool { a = filepath.Clean(a) b = filepath.Clean(b) if a == b { return true } aSep := a + string(os.PathSeparator) bSep := b + string(os.PathSeparator) return strings.HasPrefix(aSep, bSep) || strings.HasPrefix(bSep, aSep) } // DiskUsageInfo holds disk usage statistics for a path. type DiskUsageInfo struct { TotalGB float64 UsedGB float64 AvailGB float64 UsedPercent float64 TotalHuman string UsedHuman string } // GetDiskUsage returns disk usage info for a path, or nil on error. func GetDiskUsage(path string) *DiskUsageInfo { var stat syscall.Statfs_t if err := syscall.Statfs(path, &stat); err != nil { return nil } bsize := uint64(stat.Bsize) total := stat.Blocks * bsize avail := stat.Bavail * bsize used := total - (stat.Bfree * bsize) const gb = 1024 * 1024 * 1024 info := &DiskUsageInfo{ TotalGB: float64(total) / float64(gb), UsedGB: float64(used) / float64(gb), AvailGB: float64(avail) / float64(gb), } if total > 0 { info.UsedPercent = float64(used) / float64(total) * 100 } info.TotalHuman = formatGB(info.TotalGB) info.UsedHuman = formatGB(info.UsedGB) return info } func formatGB(gb float64) string { if gb >= 1000 { return fmt.Sprintf("%.1f TB", gb/1024) } return fmt.Sprintf("%.1f GB", gb) } // FSInfo holds filesystem type, device, and disk model info. type FSInfo struct { FSType string // "ext4", "btrfs" Device string // "/dev/sda1" Model string // "WD Elements 25A2" (best-effort from sysfs) } // GetFSInfo returns filesystem info for a path using findmnt, or nil on error. func GetFSInfo(path string) *FSInfo { out, err := exec.Command("findmnt", "-n", "-o", "SOURCE,FSTYPE", "--target", path).Output() if err != nil { return nil } fields := strings.Fields(strings.TrimSpace(string(out))) if len(fields) < 2 { return nil } info := &FSInfo{ Device: fields[0], FSType: fields[1], } // Try to get disk model from sysfs info.Model = diskModel(info.Device) return info } // diskModel reads the disk model from /sys/block//device/model. func diskModel(device string) string { // /dev/sda1 → sda, /dev/nvme0n1p1 → nvme0n1 base := filepath.Base(device) // Strip partition number: sda1 → sda, nvme0n1p1 → nvme0n1 disk := base if strings.HasPrefix(base, "nvme") { // nvme0n1p1 → find last 'p' followed by digits if idx := strings.LastIndex(base, "p"); idx > 4 { disk = base[:idx] } } else { // sda1 → sda: strip trailing digits disk = strings.TrimRight(base, "0123456789") } modelPath := "/sys/block/" + disk + "/device/model" data, err := os.ReadFile(modelPath) if err != nil { return "" } return strings.TrimSpace(string(data)) }