package service import ( "bufio" "fmt" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "os" "os/exec" "regexp" "strconv" "strings" ) func organizeDiskInfo(diskInfos []response.DiskBasicInfo) response.CompleteDiskInfo { var result response.CompleteDiskInfo var systemDisk *response.DiskInfo diskMap := make(map[string]*response.DiskInfo) partitions := make(map[string][]response.DiskBasicInfo) for _, info := range diskInfos { isPartition := isPartitionDevice(info.Device) if isPartition { parentDevice := getParentDevice(info.Device) partitions[parentDevice] = append(partitions[parentDevice], info) } else { disk := &response.DiskInfo{ DiskBasicInfo: info, Partitions: []response.DiskBasicInfo{}, } diskMap[info.Device] = disk } } for parentDevice, partList := range partitions { if disk, exists := diskMap[parentDevice]; exists { for index, part := range partList { part.Device = fmt.Sprintf("/dev/%s", part.Device) if part.IsSystem { disk.IsSystem = true } partList[index] = part } disk.Partitions = partList } } var totalCapacity int64 for _, disk := range diskMap { capacity := parseSizeToBytes(disk.Size) totalCapacity += capacity if disk.IsSystem { systemDisk = disk } else if len(disk.Partitions) == 0 { result.UnpartitionedDisks = append(result.UnpartitionedDisks, disk.DiskBasicInfo) } else { result.Disks = append(result.Disks, *disk) } } result.SystemDisk = systemDisk result.TotalDisks = len(diskMap) result.TotalCapacity = totalCapacity return result } func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) { lines := strings.Split(strings.TrimSpace(output), "\n") if len(lines) == 0 { return nil, fmt.Errorf("invalid lsblk output") } var diskInfos []response.DiskBasicInfo lvmMap := make(map[string]response.DiskBasicInfo) var pendingDevices []map[string]string for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } fields := parseKeyValuePairs(line) name, ok := fields["NAME"] if !ok { continue } if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "dm-") { continue } diskType := fields["TYPE"] mountPoint := fields["MOUNTPOINT"] fsType := fields["FSTYPE"] size := fields["SIZE"] if diskType == "lvm" { total, used, avail, usePercent, _ := getDiskUsageInfo("/dev/mapper/" + name) if total != "" { size = total } lvmInfo := response.DiskBasicInfo{ Device: name, Size: size, Model: fields["MODEL"], DiskType: "LVM", IsRemovable: false, IsSystem: isSystemDisk(mountPoint), Filesystem: fsType, Used: used, Avail: avail, UsePercent: usePercent, MountPoint: mountPoint, IsMounted: mountPoint != "" && mountPoint != "-", Serial: fields["SERIAL"], } lvmMap[name] = lvmInfo } else if diskType == "disk" || diskType == "part" { pendingDevices = append(pendingDevices, fields) } } for _, fields := range pendingDevices { name := fields["NAME"] size := fields["SIZE"] mountPoint := fields["MOUNTPOINT"] fsType := fields["FSTYPE"] model := fields["MODEL"] serial := fields["SERIAL"] tran := fields["TRAN"] rota := fields["ROTA"] totalSize, used, avail, usePercent, _ := getDiskUsageInfo("/dev/" + name) if totalSize != "" && fsType != "" { size = totalSize } actualMountPoint := mountPoint actualFsType := fsType actualUsed := used actualAvail := avail actualUsePercent := usePercent isMounted := mountPoint != "" && mountPoint != "-" isSystemPartition := isSystemDisk(mountPoint) if fsType == "LVM2_member" { for _, lvmInfo := range lvmMap { if lvmInfo.IsMounted { actualMountPoint = lvmInfo.MountPoint actualFsType = lvmInfo.Filesystem actualUsed = lvmInfo.Used size = lvmInfo.Size actualAvail = lvmInfo.Avail actualUsePercent = lvmInfo.UsePercent isMounted = true isSystemPartition = lvmInfo.IsSystem break } } } info := response.DiskBasicInfo{ Device: name, Size: size, Model: model, DiskType: getDiskType(rota), IsRemovable: tran == "usb", IsSystem: isSystemPartition, Filesystem: actualFsType, Used: actualUsed, Avail: actualAvail, UsePercent: actualUsePercent, MountPoint: actualMountPoint, IsMounted: isMounted, Serial: serial, } diskInfos = append(diskInfos, info) } return diskInfos, nil } func getDiskType(rota string) string { if rota == "0" { return "SSD" } else if rota == "1" { return "HDD" } return "Unknown" } var kvRe = regexp.MustCompile(`([A-Za-z0-9_]+)=("([^"\\]|\\.)*"|[^ \t]+)`) func parseKeyValuePairs(line string) map[string]string { fields := make(map[string]string) matches := kvRe.FindAllStringSubmatch(line, -1) for _, m := range matches { key := m[1] raw := m[2] val := raw if len(val) >= 2 && val[0] == '"' && val[len(val)-1] == '"' { if unq, err := strconv.Unquote(val); err == nil { val = unq } else { val = val[1 : len(val)-1] } } fields[key] = val } return fields } func isPartitionDevice(device string) bool { if strings.Contains(device, "nvme") { return strings.Contains(device, "p") && strings.ContainsAny(device[strings.LastIndex(device, "p")+1:], "0123456789") } else if strings.HasPrefix(device, "sd") || strings.HasPrefix(device, "hd") { return len(device) > 3 && strings.ContainsAny(device[len(device)-1:], "0123456789") } else if strings.HasPrefix(device, "vd") { return len(device) > 3 && strings.ContainsAny(device[len(device)-1:], "0123456789") } return false } func getParentDevice(device string) string { if strings.Contains(device, "nvme") { if idx := strings.LastIndex(device, "p"); idx != -1 { return device[:idx] } } else { return strings.TrimRight(device, "0123456789") } return device } func getDiskUsageInfo(device string) (size, used, avail string, usePercent int, err error) { output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("df -h %s | tail -1", device)) if err != nil { return "", "", "", 0, nil } fields := strings.Fields(output) if len(fields) >= 5 { size = fields[1] used = fields[2] avail = fields[3] usePercentStr := strings.TrimSuffix(fields[4], "%") usePercent, _ = strconv.Atoi(usePercentStr) } return size, used, avail, usePercent, nil } func formatDisk(req dto.DiskFormatRequest) error { var mkfsCmd *exec.Cmd switch req.Filesystem { case "ext4": mkfsCmd = exec.Command("mkfs.ext4", "-F", req.Device) case "xfs": if !cmd.Which("mkfs.xfs") { return buserr.New("XfsNotFound") } mkfsCmd = exec.Command("mkfs.xfs", "-f", req.Device) default: return fmt.Errorf("unsupport type: %s", req.Filesystem) } if err := mkfsCmd.Run(); err != nil { return err } return nil } func deviceExists(device string) bool { _, err := os.Stat(device) return err == nil } func isDeviceMounted(device string) bool { file, err := os.Open("/proc/mounts") if err != nil { return false } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) >= 2 && fields[0] == device { return true } } return false } func isPointMounted(mountPoint string) bool { file, err := os.Open("/proc/mounts") if err != nil { return false } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { fields := strings.Fields(scanner.Text()) if len(fields) >= 2 && fields[1] == mountPoint { return true } } return false } func addToFstabWithOptions(device, mountPoint, filesystem, options string) error { if filesystem == "" { fsType, err := getFilesystemType(device) if err != nil { filesystem = "auto" } else { filesystem = fsType } } if options == "" { options = "defaults" } entry := fmt.Sprintf("%s %s %s %s 0 2\n", device, mountPoint, filesystem, options) file, err := os.OpenFile("/etc/fstab", os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return err } defer file.Close() _, err = file.WriteString(entry) return err } func removeFromFstab(mountPoint string) error { file, err := os.Open("/etc/fstab") if err != nil { return err } defer file.Close() var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() fields := strings.Fields(line) if len(fields) >= 2 && fields[1] == mountPoint { continue } lines = append(lines, line) } return os.WriteFile("/etc/fstab", []byte(strings.Join(lines, "\n")+"\n"), 0644) } func getFilesystemType(device string) (string, error) { output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("blkid -o value -s TYPE %s", device)) if err != nil { return "", err } return strings.TrimSpace(output), nil } func isSystemDisk(mountPoint string) bool { systemMountPoints := []string{ "/", "/boot", "/boot/efi", "/usr", "/var", "/home", } for _, sysMount := range systemMountPoints { if mountPoint == sysMount { return true } } return false } func parseSizeToBytes(sizeStr string) int64 { if sizeStr == "" || sizeStr == "-" { return 0 } sizeStr = strings.TrimSpace(sizeStr) if len(sizeStr) < 2 { return 0 } unit := strings.ToUpper(sizeStr[len(sizeStr)-1:]) valueStr := sizeStr[:len(sizeStr)-1] value, err := strconv.ParseFloat(valueStr, 64) if err != nil { return 0 } switch unit { case "K": return int64(value * 1024) case "M": return int64(value * 1024 * 1024) case "G": return int64(value * 1024 * 1024 * 1024) case "T": return int64(value * 1024 * 1024 * 1024 * 1024) default: val, _ := strconv.ParseInt(sizeStr, 10, 64) return val } }