//go:build linux package storage import ( "encoding/json" "fmt" "os/exec" "strings" ) // lsblkOutput matches the top-level JSON from lsblk -J. type lsblkOutput struct { BlockDevices []lsblkDevice `json:"blockdevices"` } // lsblkDevice is the raw JSON structure from lsblk. type lsblkDevice struct { Name string `json:"name"` Path string `json:"path"` Size interface{} `json:"size"` // may be float64 or string Type string `json:"type"` FSType *string `json:"fstype"` MountPoint *string `json:"mountpoint"` Model *string `json:"model"` RM interface{} `json:"rm"` // removable: bool or "0"/"1" Children []lsblkDevice `json:"children"` } func (d *lsblkDevice) sizeBytes() int64 { switch v := d.Size.(type) { case float64: return int64(v) } return 0 } func (d *lsblkDevice) sizeHuman() string { bytes := d.sizeBytes() const ( GB = 1024 * 1024 * 1024 TB = GB * 1024 ) switch { case bytes >= TB: return fmt.Sprintf("%.1f TB", float64(bytes)/float64(TB)) case bytes >= GB: return fmt.Sprintf("%.1f GB", float64(bytes)/float64(GB)) default: return fmt.Sprintf("%d MB", bytes/(1024*1024)) } } func (d *lsblkDevice) isRemovable() bool { switch v := d.RM.(type) { case bool: return v case float64: return v != 0 case string: return v == "1" || strings.EqualFold(v, "true") } return false } func (d *lsblkDevice) fsType() string { if d.FSType != nil { return *d.FSType } return "" } func (d *lsblkDevice) mountPoint() string { if d.MountPoint != nil { return *d.MountPoint } return "" } func (d *lsblkDevice) model() string { if d.Model != nil { return strings.TrimSpace(*d.Model) } return "" } // ScanDisks detects all block devices and classifies them into // available (not mounted, not system) and system/mounted disks. func ScanDisks() (*ScanResult, error) { out, err := exec.Command( "lsblk", "-J", "-b", "-o", "NAME,PATH,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL,RM", ).Output() if err != nil { return nil, fmt.Errorf("lsblk failed: %w", err) } var parsed lsblkOutput if err := json.Unmarshal(out, &parsed); err != nil { return nil, fmt.Errorf("lsblk JSON parse failed: %w", err) } result := &ScanResult{} for _, dev := range parsed.BlockDevices { if dev.Type != "disk" { continue } bd := BlockDevice{ Name: dev.Name, Path: dev.Path, Size: dev.sizeHuman(), SizeBytes: dev.sizeBytes(), Model: dev.model(), Type: dev.Type, Removable: dev.isRemovable(), } if bd.Path == "" { bd.Path = "/dev/" + bd.Name } isSystem := false anyMounted := false for _, child := range dev.Children { if child.Type != "part" && child.Type != "lvm" && child.Type != "crypt" { continue } part := Partition{ Name: child.Name, Path: child.Path, Size: child.sizeHuman(), SizeBytes: child.sizeBytes(), FSType: child.fsType(), MountPoint: child.mountPoint(), } if part.Path == "" { part.Path = "/dev/" + part.Name } bd.Partitions = append(bd.Partitions, part) if part.MountPoint != "" { anyMounted = true if part.MountPoint == "/" || part.MountPoint == "/boot" || part.MountPoint == "/boot/efi" { isSystem = true } } } // Also check if the disk itself is directly mounted (no partition table) if dev.mountPoint() != "" { anyMounted = true mp := dev.mountPoint() if mp == "/" || mp == "/boot" { isSystem = true } } bd.Mounted = anyMounted if isSystem { result.SystemDisks = append(result.SystemDisks, bd) } else { result.AvailableDisks = append(result.AvailableDisks, bd) } } return result, nil }