From 9c22005b79cbb41cfff976caef8a9dc0d775b16b Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:15:58 +0800 Subject: [PATCH] feat: Add disk management (#10282) Co-authored-by: wanghe-fit2cloud --- agent/app/api/v2/disk.go | 99 ++++ agent/app/api/v2/entry.go | 2 + agent/app/dto/disk.go | 25 + agent/app/dto/request/disk.go | 19 + agent/app/dto/response/disk.go | 37 ++ agent/app/service/disk.go | 554 ++++++++++++++++++ agent/i18n/lang/en.yaml | 11 +- agent/i18n/lang/ja.yaml | 11 +- agent/i18n/lang/ko.yaml | 11 +- agent/i18n/lang/ms.yaml | 11 +- agent/i18n/lang/pt-BR.yaml | 11 +- agent/i18n/lang/ru.yaml | 11 +- agent/i18n/lang/tr.yaml | 9 + agent/i18n/lang/zh-Hant.yaml | 9 + agent/i18n/lang/zh.yaml | 11 +- agent/router/ro_host.go | 5 + core/init/migration/migrate.go | 1 + core/init/migration/migrations/init.go | 14 + frontend/src/api/interface/host.ts | 46 ++ frontend/src/api/modules/host.ts | 18 +- frontend/src/lang/modules/en.ts | 21 + frontend/src/lang/modules/ja.ts | 21 + frontend/src/lang/modules/ko.ts | 21 + frontend/src/lang/modules/ms.ts | 13 + frontend/src/lang/modules/pt-br.ts | 21 + frontend/src/lang/modules/ru.ts | 21 + frontend/src/lang/modules/tr.ts | 21 + frontend/src/lang/modules/zh-Hant.ts | 20 + frontend/src/lang/modules/zh.ts | 21 + frontend/src/routers/modules/host.ts | 10 + .../disk-management/components/disk-card.vue | 133 +++++ .../views/host/disk-management/disk/index.vue | 59 ++ .../src/views/host/disk-management/index.vue | 19 + .../host/disk-management/partition/index.vue | 118 ++++ 34 files changed, 1426 insertions(+), 8 deletions(-) create mode 100644 agent/app/api/v2/disk.go create mode 100644 agent/app/dto/disk.go create mode 100644 agent/app/dto/request/disk.go create mode 100644 agent/app/dto/response/disk.go create mode 100644 agent/app/service/disk.go create mode 100644 frontend/src/views/host/disk-management/components/disk-card.vue create mode 100644 frontend/src/views/host/disk-management/disk/index.vue create mode 100644 frontend/src/views/host/disk-management/index.vue create mode 100644 frontend/src/views/host/disk-management/partition/index.vue diff --git a/agent/app/api/v2/disk.go b/agent/app/api/v2/disk.go new file mode 100644 index 000000000..e1f26daa0 --- /dev/null +++ b/agent/app/api/v2/disk.go @@ -0,0 +1,99 @@ +package v2 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/gin-gonic/gin" +) + +// @Tags Disk Management +// @Summary Get complete disk information +// @Description Get information about all disks including partitioned and unpartitioned disks +// @Produce json +// @Success 200 {object} dto.CompleteDiskInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /disks [get] +func (b *BaseApi) GetCompleteDiskInfo(c *gin.Context) { + diskInfo, err := diskService.GetCompleteDiskInfo() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, diskInfo) +} + +// @Tags Disk Management +// @Summary Partition disk +// @Description Create partition and format disk with specified filesystem +// @Accept json +// @Param request body request.DiskPartitionRequest true "partition request" +// @Success 200 {string} string "Partition created successfully" +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /disks/partition [post] +// @x-panel-log {"bodyKeys":["device", "filesystem", "mountPoint"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"对磁盘 [device] 进行分区,文件系统 [filesystem],挂载点 [mountPoint]","formatEN":"Partition disk [device] with filesystem [filesystem], mount point [mountPoint]"} +func (b *BaseApi) PartitionDisk(c *gin.Context) { + var req request.DiskPartitionRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + result, err := diskService.PartitionDisk(req) + if err != nil { + helper.InternalServer(c, err) + return + } + + helper.SuccessWithData(c, result) +} + +// @Tags Disk Management +// @Summary Mount disk +// @Description Mount partition to specified mount point +// @Accept json +// @Param request body request.DiskMountRequest true "mount request" +// @Success 200 {string} string "Disk mounted successfully" +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /disks/mount [post] +// @x-panel-log {"bodyKeys":["device", "mountPoint"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"挂载磁盘 [device] 到 [mountPoint]","formatEN":"Mount disk [device] to [mountPoint]"} +func (b *BaseApi) MountDisk(c *gin.Context) { + var req request.DiskMountRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + err := diskService.MountDisk(req) + if err != nil { + helper.InternalServer(c, err) + return + } + + helper.Success(c) +} + +// @Tags Disk Management +// @Summary Unmount disk +// @Description Unmount partition from mount point +// @Accept json +// @Param request body request.DiskUnmountRequest true "unmount request" +// @Success 200 {string} string "Disk unmounted successfully" +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /disks/unmount [post] +// @x-panel-log {"bodyKeys":["device", "mountPoint"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"卸载磁盘 [device] 从 [mountPoint]","formatEN":"Unmount disk [device] from [mountPoint]"} +func (b *BaseApi) UnmountDisk(c *gin.Context) { + var req request.DiskUnmountRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + err := diskService.UnmountDisk(req) + if err != nil { + helper.InternalServer(c, err) + return + } + + helper.Success(c) +} diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index b398673a7..1970bd5c2 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -71,4 +71,6 @@ var ( taskService = service.NewITaskService() groupService = service.NewIGroupService() alertService = service.NewIAlertService() + + diskService = service.NewIDiskService() ) diff --git a/agent/app/dto/disk.go b/agent/app/dto/disk.go new file mode 100644 index 000000000..3fa6afe79 --- /dev/null +++ b/agent/app/dto/disk.go @@ -0,0 +1,25 @@ +package dto + +type LsblkDevice struct { + Name string `json:"name"` + Size string `json:"size"` + Type string `json:"type"` + MountPoint *string `json:"mountpoint"` + FsType *string `json:"fstype"` + Model *string `json:"model"` + Serial string `json:"serial"` + Tran string `json:"tran"` + Rota bool `json:"rota"` + Children []LsblkDevice `json:"children,omitempty"` +} + +type LsblkOutput struct { + BlockDevices []LsblkDevice `json:"blockdevices"` +} + +type DiskFormatRequest struct { + Device string `json:"device" ` + Filesystem string `json:"filesystem" ` + Label string `json:"label,omitempty" ` + QuickFormat bool `json:"quickFormat,omitempty"` +} diff --git a/agent/app/dto/request/disk.go b/agent/app/dto/request/disk.go new file mode 100644 index 000000000..4728cc0c0 --- /dev/null +++ b/agent/app/dto/request/disk.go @@ -0,0 +1,19 @@ +package request + +type DiskPartitionRequest struct { + Device string `json:"device" validate:"required"` + Filesystem string `json:"filesystem" validate:"required,oneof=ext4 xfs"` + Label string `json:"label"` + AutoMount bool `json:"autoMount"` + MountPoint string `json:"mountPoint"` +} + +type DiskMountRequest struct { + Device string `json:"device" validate:"required"` + MountPoint string `json:"mountPoint" validate:"required"` + Filesystem string `json:"filesystem" validate:"required,oneof=ext4 xfs"` +} + +type DiskUnmountRequest struct { + MountPoint string `json:"mountPoint" validate:"required"` +} diff --git a/agent/app/dto/response/disk.go b/agent/app/dto/response/disk.go new file mode 100644 index 000000000..40a1cf666 --- /dev/null +++ b/agent/app/dto/response/disk.go @@ -0,0 +1,37 @@ +package response + +type DiskInfo struct { + DiskBasicInfo + Partitions []DiskBasicInfo `json:"partitions"` +} + +type DiskBasicInfo struct { + Device string `json:"device"` + Size string `json:"size"` + Model string `json:"model"` + DiskType string `json:"diskType"` + IsRemovable bool `json:"isRemovable"` + IsSystem bool `json:"isSystem"` + Filesystem string `json:"filesystem"` + Used string `json:"used"` + Avail string `json:"avail"` + UsePercent int `json:"usePercent"` + MountPoint string `json:"mountPoint"` + IsMounted bool `json:"isMounted"` + Serial string `json:"serial"` +} + +type CompleteDiskInfo struct { + Disks []DiskInfo `json:"disks"` + UnpartitionedDisks []DiskBasicInfo `json:"unpartitionedDisks"` + SystemDisk *DiskInfo `json:"systemDisk"` + TotalDisks int `json:"totalDisks"` + TotalCapacity int64 `json:"totalCapacity"` +} + +type MountInfo struct { + Device string `json:"device"` + MountPoint string `json:"mountPoint"` + Filesystem string `json:"filesystem"` + Options string `json:"options"` +} diff --git a/agent/app/service/disk.go b/agent/app/service/disk.go new file mode 100644 index 000000000..19abf39ac --- /dev/null +++ b/agent/app/service/disk.go @@ -0,0 +1,554 @@ +package service + +import ( + "bufio" + "encoding/json" + "fmt" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto/request" + "github.com/1Panel-dev/1Panel/agent/app/dto/response" +) + +type DiskService struct{} + +type IDiskService interface { + GetCompleteDiskInfo() (*response.CompleteDiskInfo, error) + PartitionDisk(req request.DiskPartitionRequest) (string, error) + MountDisk(req request.DiskMountRequest) error + UnmountDisk(req request.DiskUnmountRequest) error +} + +func NewIDiskService() IDiskService { + return &DiskService{} +} + +func (s *DiskService) GetCompleteDiskInfo() (*response.CompleteDiskInfo, error) { + disksWithPartitions, unpartitionedDisks, err := getAllDisksInfo() + if err != nil { + return nil, err + } + var systemDisk *response.DiskInfo + var totalCapacity int64 + var filteredDisksWithPartitions []response.DiskInfo + + for i, disk := range disksWithPartitions { + if disk.IsSystem { + if systemDisk == nil { + systemDisk = &disksWithPartitions[i] + } + } else { + filteredDisksWithPartitions = append(filteredDisksWithPartitions, disk) + } + } + + completeDiskInfo := &response.CompleteDiskInfo{ + Disks: filteredDisksWithPartitions, + UnpartitionedDisks: unpartitionedDisks, + SystemDisk: systemDisk, + TotalDisks: len(filteredDisksWithPartitions) + len(unpartitionedDisks), + TotalCapacity: totalCapacity, + } + return completeDiskInfo, nil +} + +func getDiskType(rota bool, tran string) string { + if tran == "" { + return "" + } + if !rota { + return "SSD" + } + return "HDD" +} + +func getAllDisksInfo() ([]response.DiskInfo, []response.DiskBasicInfo, error) { + var disksWithPartitions []response.DiskInfo + var unpartitionedDisks []response.DiskBasicInfo + + output, err := cmd.RunDefaultWithStdoutBashC("lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,MODEL,SERIAL,TRAN,ROTA") + if err != nil { + return nil, nil, err + } + + var lsblkOutput dto.LsblkOutput + if err = json.Unmarshal([]byte(output), &lsblkOutput); err != nil { + return nil, nil, err + } + + for _, device := range lsblkOutput.BlockDevices { + if device.Type != "disk" { + continue + } + + devicePath := "/dev/" + device.Name + model := "" + isPhysical := false + + if device.Tran != "" { + isPhysical = true + if device.Model != nil { + model = *device.Model + } + } + + hasPartitions := len(device.Children) > 0 + + if hasPartitions { + disk := response.DiskInfo{ + DiskBasicInfo: response.DiskBasicInfo{ + Device: devicePath, + Size: device.Size, + DiskType: getDiskType(device.Rota, device.Tran), + IsRemovable: isRemovableDisk(devicePath), + IsSystem: false, + }, + Partitions: []response.DiskBasicInfo{}, + } + + if isPhysical { + disk.Serial = device.Serial + disk.Model = model + } + + for _, partition := range device.Children { + partitionInfo := processPartition(partition) + if partitionInfo != nil { + disk.Partitions = append(disk.Partitions, *partitionInfo) + if partitionInfo.IsSystem { + disk.IsSystem = true + } + } + } + + if len(disk.Partitions) > 0 { + disksWithPartitions = append(disksWithPartitions, disk) + } + } else { + if isSystemDiskByDevice(device) { + continue + } + + unpartitionedDisk := response.DiskBasicInfo{ + Device: devicePath, + Size: device.Size, + Model: model, + Serial: device.Serial, + DiskType: getDiskType(device.Rota, device.Tran), + IsRemovable: isRemovableDisk(devicePath), + } + unpartitionedDisks = append(unpartitionedDisks, unpartitionedDisk) + } + } + + return disksWithPartitions, unpartitionedDisks, nil +} + +func (s *DiskService) PartitionDisk(req request.DiskPartitionRequest) (string, error) { + if !deviceExists(req.Device) { + return "", buserr.WithName("DeviceNotFound", req.Device) + } + + if isDeviceMounted(req.Device) { + return "", buserr.WithName("DeviceIsMounted", req.Device) + } + + cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(10 * time.Second)) + if err := cmdMgr.RunBashC(fmt.Sprintf("partprobe %s", req.Device)); err != nil { + return "", buserr.WithErr("PartitionDiskErr", err) + } + + if err := cmdMgr.RunBashC(fmt.Sprintf("parted -s %s mklabel gpt", req.Device)); err != nil { + return "", buserr.WithErr("PartitionDiskErr", err) + } + + if err := cmdMgr.RunBashC(fmt.Sprintf("parted -s %s mkpart primary 1MiB 100%%", req.Device)); err != nil { + return "", buserr.WithErr("PartitionDiskErr", err) + } + + if err := cmdMgr.RunBashC(fmt.Sprintf("partprobe %s", req.Device)); err != nil { + return "", buserr.WithErr("PartitionDiskErr", err) + } + partition := req.Device + "1" + + formatReq := dto.DiskFormatRequest{ + Device: partition, + Filesystem: req.Filesystem, + Label: req.Label, + } + if err := formatDisk(formatReq); err != nil { + return "", buserr.WithErr("FormatDiskErr", err) + } + + if req.AutoMount && req.MountPoint != "" { + mountReq := request.DiskMountRequest{ + Device: partition, + MountPoint: req.MountPoint, + Filesystem: req.Filesystem, + } + if err := s.MountDisk(mountReq); err != nil { + return "", buserr.WithErr("MountDiskErr", err) + } + } + + return partition, nil +} + +func (s *DiskService) MountDisk(req request.DiskMountRequest) error { + if !deviceExists(req.Device) { + return buserr.WithName("DeviceNotFound", req.Device) + } + + if err := os.MkdirAll(req.MountPoint, 0755); err != nil { + return err + } + + fileSystem, err := getFilesystemType(req.Device) + if err != nil { + return err + } + if fileSystem == "" { + formatReq := dto.DiskFormatRequest{ + Device: req.Device, + Filesystem: req.Filesystem, + } + if err := formatDisk(formatReq); err != nil { + return buserr.WithErr("FormatDiskErr", err) + } + } + + cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Minute)) + if err := cmdMgr.RunBashC(fmt.Sprintf("mount -t %s %s %s", req.Filesystem, req.Device, req.MountPoint)); err != nil { + return buserr.WithErr("MountDiskErr", err) + } + + if err := addToFstabWithOptions(req.Device, req.MountPoint, req.Filesystem, ""); err != nil { + return buserr.WithErr("MountDiskErr", err) + } + + return nil +} + +func (s *DiskService) UnmountDisk(req request.DiskUnmountRequest) error { + if !isPointMounted(req.MountPoint) { + return buserr.New("MountDiskErr") + } + if err := cmd.RunDefaultBashC(fmt.Sprintf("umount -f %s", req.MountPoint)); err != nil { + return buserr.WithErr("MountDiskErr", err) + } + if err := removeFromFstab(req.MountPoint); err != nil { + global.LOG.Errorf("remove %s mountPoint err: %v", req.MountPoint, err) + } + return 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 parseDiskInfoLinux(fields []string) (response.DiskInfo, error) { + device := fields[0] + filesystem := fields[1] + sizeStr := fields[2] + usedStr := fields[3] + availStr := fields[4] + usePercentStr := fields[5] + mountPoint := fields[6] + + usePercent := 0 + if strings.HasSuffix(usePercentStr, "%") { + if percent, err := strconv.Atoi(strings.TrimSuffix(usePercentStr, "%")); err == nil { + usePercent = percent + } + } + + return response.DiskInfo{ + DiskBasicInfo: response.DiskBasicInfo{ + Device: device, + Filesystem: filesystem, + Size: sizeStr, + Used: usedStr, + Avail: availStr, + UsePercent: usePercent, + MountPoint: mountPoint, + }, + }, 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 isSystemDiskByDevice(device dto.LsblkDevice) bool { + for _, child := range device.Children { + if child.Type == "part" { + if child.MountPoint != nil && isSystemDisk(*child.MountPoint) { + return true + } + for _, lvmChild := range child.Children { + if lvmChild.Type == "lvm" && lvmChild.MountPoint != nil { + if isSystemDisk(*lvmChild.MountPoint) { + return true + } + } + } + } + } + return false +} + +func isRemovableDisk(device string) bool { + deviceName := strings.TrimPrefix(device, "/dev/") + for i := len(deviceName) - 1; i >= 0; i-- { + if deviceName[i] < '0' || deviceName[i] > '9' { + deviceName = deviceName[:i+1] + break + } + } + + removablePath := filepath.Join("/sys/block", deviceName, "removable") + if data, err := os.ReadFile(removablePath); err == nil { + removable := strings.TrimSpace(string(data)) + return removable == "1" + } + + return false +} + +func processPartition(partition dto.LsblkDevice) *response.DiskBasicInfo { + if partition.Type != "part" { + return nil + } + + devicePath := "/dev/" + partition.Name + + var mountPoint, filesystem string + if partition.MountPoint != nil { + mountPoint = *partition.MountPoint + } + if partition.FsType != nil { + filesystem = *partition.FsType + } + + var ( + actualMountPoint, actualFilesystem string + size, used, avail string + usePercent int + isMounted bool + isSystem bool + ) + + if len(partition.Children) > 0 { + for _, child := range partition.Children { + if child.Type == "lvm" { + lvmDevicePath := "/dev/mapper/" + child.Name + + if child.MountPoint != nil { + actualMountPoint = *child.MountPoint + isMounted = true + } + if child.FsType != nil { + actualFilesystem = *child.FsType + } + + if actualMountPoint != "" { + diskUsage, err := getDiskUsageInfo(lvmDevicePath) + if err == nil { + used = diskUsage.Used + avail = diskUsage.Avail + size = diskUsage.Size + usePercent = diskUsage.UsePercent + } + } + + isSystem = isSystemDisk(actualMountPoint) + break + } + } + } else { + actualMountPoint = mountPoint + actualFilesystem = filesystem + isMounted = mountPoint != "" + + if actualMountPoint != "" { + diskUsage, err := getDiskUsageInfo(devicePath) + if err == nil { + used = diskUsage.Used + avail = diskUsage.Avail + size = diskUsage.Size + usePercent = diskUsage.UsePercent + } + } + + isSystem = isSystemDisk(actualMountPoint) + } + + if size == "" { + size = partition.Size + } + + return &response.DiskBasicInfo{ + Device: devicePath, + Size: size, + Used: used, + Avail: avail, + UsePercent: usePercent, + MountPoint: actualMountPoint, + Filesystem: actualFilesystem, + IsMounted: isMounted, + IsSystem: isSystem, + } +} + +func getDiskUsageInfo(device string) (response.DiskInfo, error) { + output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("df -hT -P %s", device)) + if err != nil { + return response.DiskInfo{}, err + } + + lines := strings.Split(output, "\n") + if len(lines) < 2 { + return response.DiskInfo{}, err + } + + fields := strings.Fields(lines[1]) + if len(fields) < 7 { + return response.DiskInfo{}, err + } + + return parseDiskInfoLinux(fields) +} diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index e4265f3e4..b148bce62 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "Your 1Panel password will expire in {{ .day }} days. P CommonAlert: "Your 1Panel, {{ .msg }}, please log in to the panel for details." NodeExceptionAlert: "Your 1Panel, {{ .num }} nodes are abnormal, please log in to the panel for details." LicenseExceptionAlert: "Your 1Panel, {{ .num }} licenses are abnormal, please log in to the panel for details." -SSHAndPanelLoginAlert: "Your 1Panel, abnormal panel {{ .name }} login from {{ .ip }}, please log in to the panel for details." \ No newline at end of file +SSHAndPanelLoginAlert: "Your 1Panel, abnormal panel {{ .name }} login from {{ .ip }}, please log in to the panel for details." + +#disk +DeviceNotFound: "Device {{ .name }} not found" +DeviceIsMounted: "Device {{ .name }} is mounted, please unmount first" +PartitionDiskErr: "Failed to partition, {{ .err }}" +FormatDiskErr: "Failed to format disk, {{ .err }}" +MountDiskErr: "Failed to mount disk, {{ .err }}" +UnMountDiskErr: "Failed to unmount disk, {{ .err }}" +XfsNotFound: "xfs filesystem not detected, please install xfsprogs first" \ No newline at end of file diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index e382b660a..0adedb20f 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "1Panel のパスワードは {{ .day }} 日後に期 CommonAlert: "お使いの1Panel、{{ .msg }}、詳細はパネルにログインしてご確認ください。" NodeExceptionAlert: "お使いの1Panel、{{ .num }}個のノードに異常があります。詳細はパネルにログインしてご確認ください。" LicenseExceptionAlert: "お使いの1Panel、{{ .num }}個のライセンスに異常があります。詳細はパネルにログインしてご確認ください。" -SSHAndPanelLoginAlert: "お使いの1Panel、パネル{{ .name }}が{{ .ip }}から異常ログインしました。詳細はパネルにログインしてご確認ください。" \ No newline at end of file +SSHAndPanelLoginAlert: "お使いの1Panel、パネル{{ .name }}が{{ .ip }}から異常ログインしました。詳細はパネルにログインしてご確認ください。" + +#disk +DeviceNotFound: "デバイス {{ .name }} が見つかりません" +DeviceIsMounted: "デバイス {{ .name }} はマウントされています、まずアンマウントしてください" +PartitionDiskErr: "パーティションに失敗しました、{{ .err }}" +FormatDiskErr: "ディスクのフォーマットに失敗しました、{{ .err }}" +MountDiskErr: "ディスクのマウントに失敗しました、{{ .err }}" +UnMountDiskErr: "ディスクのアンマウントに失敗しました、{{ .err }}" +XfsNotFound: "xfs ファイルシステムが検出されませんでした、最初に xfsprogs をインストールしてください" \ No newline at end of file diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 645b97566..cf753a939 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "1Panel 비밀번호가 {{ .day }}일 후에 만료됩 CommonAlert: "귀하의 1Panel, {{ .msg }}. 자세한 내용은 패널에 로그인하여 확인하세요." NodeExceptionAlert: "귀하의 1Panel, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하여 확인하세요." LicenseExceptionAlert: "귀하의 1Panel, {{ .num }}개의 라이선스에 이상이 있습니다. 자세한 내용은 패널에 로그인하여 확인하세요." -SSHAndPanelLoginAlert: "귀하의 1Panel, 패널 {{ .name }}이(가) {{ .ip }}에서 비정상 로그인했습니다. 자세한 내용은 패널에 로그인하여 확인하세요." \ No newline at end of file +SSHAndPanelLoginAlert: "귀하의 1Panel, 패널 {{ .name }}이(가) {{ .ip }}에서 비정상 로그인했습니다. 자세한 내용은 패널에 로그인하여 확인하세요." + +#disk +DeviceNotFound: "장치 {{ .name }} 을(를) 찾을 수 없습니다" +DeviceIsMounted: "장치 {{ .name }} 이(가) 마운트되었습니다, 먼저 마운트 해제하세요" +PartitionDiskErr: "파티션 분할에 실패했습니다, {{ .err }}" +FormatDiskErr: "디스크 포맷에 실패했습니다, {{ .err }}" +MountDiskErr: "디스크 마운트에 실패했습니다, {{ .err }}" +UnMountDiskErr: "디스크 마운트 해제에 실패했습니다, {{ .err }}" +XfsNotFound: "xfs 파일 시스템이 감지되지 않았습니다, 먼저 xfsprogs 를 설치하세요" \ No newline at end of file diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 2c3c89747..fbe68abb2 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "Kata laluan 1Panel anda akan tamat dalam {{ .day }} ha CommonAlert: "1Panel anda, {{ .msg }}, sila log masuk ke panel untuk maklumat lanjut." NodeExceptionAlert: "1Panel anda, {{ .num }} nod bermasalah, sila log masuk ke panel untuk maklumat lanjut." LicenseExceptionAlert: "1Panel anda, {{ .num }} lesen bermasalah, sila log masuk ke panel untuk maklumat lanjut." -SSHAndPanelLoginAlert: "1Panel anda, log masuk panel {{ .name }} yang tidak normal dari {{ .ip }}, sila log masuk ke panel untuk maklumat lanjut." \ No newline at end of file +SSHAndPanelLoginAlert: "1Panel anda, log masuk panel {{ .name }} yang tidak normal dari {{ .ip }}, sila log masuk ke panel untuk maklumat lanjut." + +#disk +DeviceNotFound: "Peranti {{ .name }} tidak ditemui" +DeviceIsMounted: "Peranti {{ .name }} telah dikaitkan, sila nyahkaitkan terlebih dahulu" +PartitionDiskErr: "Gagal membahagikan, {{ .err }}" +FormatDiskErr: "Gagal memformat cakera, {{ .err }}" +MountDiskErr: "Gagal mengkaitkan cakera, {{ .err }}" +UnMountDiskErr: "Gagal nyahkaitkan cakera, {{ .err }}" +XfsNotFound: "Sistem fail xfs tidak dikesan, sila pasang xfsprogs terlebih dahulu" \ No newline at end of file diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 3e17107fd..4b0f7bec9 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "A senha do 1Panel expirará em {{ .day }} dias. Acesse CommonAlert: "Seu 1Panel, {{ .msg }}. Para mais detalhes, faça login no painel." NodeExceptionAlert: "Seu 1Panel, {{ .num }} nós estão com problemas. Para mais detalhes, faça login no painel." LicenseExceptionAlert: "Seu 1Panel, {{ .num }} licenças estão com problemas. Para mais detalhes, faça login no painel." -SSHAndPanelLoginAlert: "Seu 1Panel, login anormal no painel {{ .name }} a partir de {{ .ip }}. Para mais detalhes, faça login no painel." \ No newline at end of file +SSHAndPanelLoginAlert: "Seu 1Panel, login anormal no painel {{ .name }} a partir de {{ .ip }}. Para mais detalhes, faça login no painel." + +#disk +DeviceNotFound: "Dispositivo {{ .name }} não encontrado" +DeviceIsMounted: "Dispositivo {{ .name }} está montado, por favor desmonte primeiro" +PartitionDiskErr: "Falha ao particionar, {{ .err }}" +FormatDiskErr: "Falha ao formatar disco, {{ .err }}" +MountDiskErr: "Falha ao montar disco, {{ .err }}" +UnMountDiskErr: "Falha ao desmontar disco, {{ .err }}" +XfsNotFound: "Sistema de arquivos xfs não detectado, por favor instale xfsprogs primeiro" \ No newline at end of file diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index fe35d0974..62e91f761 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "Пароль для 1Panel истекает через CommonAlert: "Ваш 1Panel, {{ .msg }}. Подробности смотрите, войдя в панель." NodeExceptionAlert: "Ваш 1Panel, {{ .num }} узлов работают неправильно. Подробности смотрите, войдя в панель." LicenseExceptionAlert: "Ваш 1Panel, {{ .num }} лицензий работают неправильно. Подробности смотрите, войдя в панель." -SSHAndPanelLoginAlert: "Ваш 1Panel, обнаружен аномальный вход в панель {{ .name }} с {{ .ip }}. Подробности смотрите, войдя в панель." \ No newline at end of file +SSHAndPanelLoginAlert: "Ваш 1Panel, обнаружен аномальный вход в панель {{ .name }} с {{ .ip }}. Подробности смотрите, войдя в панель." + +#disk +DeviceNotFound: "Устройство {{ .name }} не найдено" +DeviceIsMounted: "Устройство {{ .name }} подключено, сначала отключите" +PartitionDiskErr: "Не удалось разделить, {{ .err }}" +FormatDiskErr: "Не удалось отформатировать диск, {{ .err }}" +MountDiskErr: "Не удалось подключить диск, {{ .err }}" +UnMountDiskErr: "Не удалось отключить диск, {{ .err }}" +XfsNotFound: "Файловая система xfs не обнаружена, сначала установите xfsprogs" \ No newline at end of file diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 4a684dc45..692f7ab5b 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -476,3 +476,12 @@ CommonAlert: "1Panel'iniz, {{ .msg }}. Detaylar için panele giriş yapınız." NodeExceptionAlert: "1Panel'iniz, {{ .num }} düğümde sorun var. Detaylar için panele giriş yapınız." LicenseExceptionAlert: "1Panel'iniz, {{ .num }} lisansda sorun var. Detaylar için panele giriş yapınız." SSHAndPanelLoginAlert: "1Panel'iniz, {{ .ip }} adresinden {{ .name }} paneline anormal giriş tespit edildi. Detaylar için panele giriş yapınız." + +#disk +DeviceNotFound: "Cihaz {{ .name }} bulunamadı" +DeviceIsMounted: "Cihaz {{ .name }} bağlandı, önce çıkarın" +PartitionDiskErr: "Bölümleme başarısız, {{ .err }}" +FormatDiskErr: "Disk biçimlendirme başarısız, {{ .err }}" +MountDiskErr: "Disk bağlama başarısız, {{ .err }}" +UnMountDiskErr: "Disk bağının kaldırılması başarısız, {{ .err }}" +XfsNotFound: "XFS dosya sistemi algılanmadı, lütfen önce xfsprogs'i yükleyin" diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index c8c98ac9e..4617d64c1 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -474,3 +474,12 @@ CommonAlert: "您的 1Panel 面板,{{ .msg }},詳情請登入面板查看。 NodeExceptionAlert: "您的 1Panel 面板,{{ .num }} 個節點存在異常,詳情請登入面板查看。" LicenseExceptionAlert: "您的 1Panel 面板,{{ .num }} 個許可證存在異常,詳情請登入面板查看。" SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}從 {{ .ip }} 登錄異常,詳情請登入面板查看。" + +#disk +DeviceNotFound: "設備 {{ .name }} 未找到" +DeviceIsMounted: "設備 {{ .name }} 已掛載,請先卸載" +PartitionDiskErr: "分區失敗,{{ .err }}" +FormatDiskErr: "格式化磁盤失敗,{{ .err }}" +MountDiskErr: "掛載磁盤失敗,{{ .err }}" +UnMountDiskErr: "卸載磁盤失敗,{{ .err }}" +XfsNotFound: "未檢測到 xfs 文件系統,請先安裝 xfsprogs" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 46a9ded50..9ecc22285 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -474,4 +474,13 @@ PanelPwdExpirationAlert: "您的 1Panel 面板,面板密码将在 {{ .day }} CommonAlert: "您的 1Panel 面板,{{ .msg }},详情请登录面板查看。" NodeExceptionAlert: "您的 1Panel 面板,{{ .num }} 个节点存在异常,详情请登录面板查看。" LicenseExceptionAlert: "您的 1Panel 面板,{{ .num }} 个许可证存在异常,详情请登录面板查看。" -SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}登录{{ .ip }}异常,详情请登录面板查看。" \ No newline at end of file +SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}登录{{ .ip }}异常,详情请登录面板查看。" + +#disk +DeviceNotFound: "设备 {{ .name }} 未找到" +DeviceIsMounted: "设备 {{ .name }} 已挂载,请先卸载" +PartitionDiskErr: "分区失败,{{ .err }}" +FormatDiskErr: "格式化磁盘失败,{{ .err }}" +MountDiskErr: "挂载磁盘失败,{{ .err }}" +UnMountDiskErr: "卸载磁盘失败,{{ .err }}" +XfsNotFound: "未检测到 xfs 文件系统,请先安装 xfsprogs" \ No newline at end of file diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index b70a21f41..102132ce7 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -52,5 +52,10 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile) hostRouter.GET("/terminal", baseApi.WsSSH) + + hostRouter.GET("/disks", baseApi.GetCompleteDiskInfo) + hostRouter.POST("/disks/partition", baseApi.PartitionDisk) + hostRouter.POST("/disks/mount", baseApi.MountDisk) + hostRouter.POST("/disks/unmount", baseApi.UnmountDisk) } } diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index a27f26518..805a228c1 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -21,6 +21,7 @@ func Init() { migrations.AddClusterMenu, migrations.DeleteXpackHideMenu, migrations.AddCronjobGroup, + migrations.AddDiskMenu, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index e061a9989..d1d5a29c0 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -511,3 +511,17 @@ var AddCronjobGroup = &gormigrate.Migration{ return nil }, } + +var AddDiskMenu = &gormigrate.Migration{ + ID: "20250811-add-disk-menu", + Migrate: func(tx *gorm.DB) error { + return helper.AddMenu(dto.ShowMenu{ + ID: "77", + Disabled: false, + Title: "menu.disk", + IsShow: true, + Label: "Disk", + Path: "/hosts/disk", + }, "7", tx) + }, +} diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index b11646189..47077a708 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -204,4 +204,50 @@ export namespace Host { status: string; message: string; } + + export interface DiskBasicInfo { + device: string; + size: string; + model: string; + diskType: string; + isRemovable: boolean; + isSystem: boolean; + filesystem: string; + used: string; + avail: string; + usePercent: number; + mountPoint: string; + isMounted: boolean; + serial: string; + } + + export interface DiskInfo extends DiskBasicInfo { + partitions?: DiskBasicInfo[]; + } + + export interface CompleteDiskInfo { + disks: DiskInfo[]; + unpartitionedDisks: DiskBasicInfo[]; + systemDisk?: DiskInfo; + totalDisks: number; + totalCapacity: number; + } + + export interface DiskPartition { + device: string; + filesystem: string; + label: string; + autoMount: boolean; + mountPoint: string; + } + + export interface DiskMount { + device: string; + mountPoint: string; + filesystem?: string; + } + + export interface DiskUmount { + mountPoint: string; + } } diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index dceb0c4ba..cb8714a83 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -106,5 +106,21 @@ export const loadSSHLogs = (params: Host.searchSSHLog) => { return http.post>(`/hosts/ssh/log`, params); }; export const exportSSHLogs = (params: Host.searchSSHLog) => { - return http.post('/hosts/ssh/log/export', params, TimeoutEnum.T_40S); + return http.post(`/hosts/ssh/log/export`, params, TimeoutEnum.T_40S); +}; + +export const listDisks = () => { + return http.get(`/hosts/disks`); +}; + +export const partitionDisk = (params: Host.DiskPartition) => { + return http.post(`/hosts/disks/partition`, params, TimeoutEnum.T_60S); +}; + +export const mountDisk = (params: Host.DiskMount) => { + return http.post(`/hosts/disks/mount`, params, TimeoutEnum.T_60S); +}; + +export const unmountDisk = (params: Host.DiskUmount) => { + return http.post(`/hosts/disks/unmount`, params, TimeoutEnum.T_60S); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index e5336ddb3..9ccb635ac 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2927,6 +2927,27 @@ const message = { autoStartHelper: 'Whether to automatically start the service after Supervisor starts', }, }, + disk: { + management: 'Disk Management', + partition: 'Partition', + unmount: 'Unmount', + unmountHelper: 'Do you want to unmount the partition {0}?', + mount: 'Mount', + partitionAlert: + 'Disk partitioning requires formatting the disk, and existing data will be deleted. Please save or take snapshots of your data in advance.', + mountPoint: 'Mount Directory', + systemDisk: 'System Disk', + unpartitionedDisk: 'Unpartitioned Disk', + handlePartition: 'Partition Now', + filesystem: 'Filesystem', + unmounted: 'Unmounted', + cannotOperate: 'Cannot Operate', + systemDiskHelper: 'Hint: The current disk is the system disk. It cannot be operated on.', + autoMount: 'Auto Mount', + model: 'Device Model', + diskType: 'Disk Type', + serial: 'Serial Number', + }, xpack: { expiresTrialAlert: 'Friendly reminder: Your Pro trial will expire in {0} days, and all Pro features will no longer be accessible. Please renew or upgrade to the full version in a timely manner.', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 6748b526d..afbd6a301 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -2822,6 +2822,27 @@ const message = { autoStartHelper: 'Supervisor 起動後にサービスを自動的に起動するかどうか', }, }, + disk: { + management: 'ディスク管理', + partition: 'パーティション', + unmount: 'アンマウント', + unmountHelper: 'パーティション {0} をアンマウントしますか?', + mount: 'マウント', + partitionAlert: + 'ディスクのパーティション分割にはディスクのフォーマットが必要で、既存のデータは削除されます。事前にデータを保存またはスナップショットを取ってください。', + mountPoint: 'マウントディレクトリ', + systemDisk: 'システムディスク', + unpartitionedDisk: '未パーティションディスク', + handlePartition: '今すぐパーティション', + filesystem: 'ファイルシステム', + unmounted: 'アンマウント', + cannotOperate: '操作不可', + systemDiskHelper: 'ヒント: 現在のディスクはシステムディスクです。操作できません。', + autoMount: '自動マウント', + model: 'デバイスモデル', + diskType: 'ディスクタイプ', + serial: 'シリアルナンバー', + }, xpack: { expiresTrialAlert: 'ご注意: あなたのProトライアルは{0}日後に終了し、すべてのPro機能が使用できなくなります。適時に更新またはフルバージョンにアップグレードしてください。', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index a7b82a64f..342e80ccd 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -2773,6 +2773,27 @@ const message = { autoStartHelper: 'Supervisor 시작 후 서비스를 자동으로 시작할지 여부', }, }, + disk: { + management: '디스크 관리', + partition: '파티션', + unmount: '마운트 해제', + unmountHelper: '파티션 {0} 을(를) 마운트 해제하시겠습니까?', + mount: '마운트', + partitionAlert: + '디스크 파티션 작업은 디스크 포맷이 필요하며, 기존 데이터는 삭제됩니다. 데이터를 미리 저장하거나 스냅샷을 찍어주세요.', + mountPoint: '마운트 디렉토리', + systemDisk: '시스템 디스크', + unpartitionedDisk: '미파티션 디스크', + handlePartition: '지금 파티션', + filesystem: '파일 시스템', + unmounted: '마운트 해제됨', + cannotOperate: '작업 불가', + systemDiskHelper: '힌트: 현재 디스크는 시스템 디스크입니다. 작업할 수 없습니다.', + autoMount: '자동 마운트', + model: '장치 모델', + diskType: '디스크 유형', + serial: '시리얼 번호', + }, xpack: { expiresTrialAlert: '친절한 알림: 귀하의 Pro 체험판이 {0}일 후 만료되며, 모든 Pro 기능에 더 이상 접근할 수 없습니다. 제때 갱신하거나 전체 버전으로 업그레이드하시기 바랍니다.', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 2c13aa827..7cdb4b8d0 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -2886,6 +2886,19 @@ const message = { autoStartHelper: 'Sama ada untuk memulakan perkhidmatan secara automatik selepas Supervisor mula', }, }, + disk: { + systemDisk: 'Cakera Sistem', + unpartitionedDisk: 'Cakera Tidak Dibahagikan', + handlePartition: 'Bahagikan Sekarang', + filesystem: 'Sistem Fail', + unmounted: 'Tidak Dikaitkan', + cannotOperate: 'Tidak Boleh Beroperasi', + systemDiskHelper: 'Petunjuk: Cakera semasa adalah cakera sistem, tidak boleh dioperasikan.', + autoMount: 'Pemasangan Automatik', + model: 'Model Peranti', + diskType: 'Jenis Cakera', + serial: 'Nombor Siri', + }, xpack: { expiresTrialAlert: 'Peringatan mesra: Percubaan Pro anda akan tamat dalam {0} hari, dan semua ciri Pro tidak lagi dapat diakses. Sila perbaharui atau naik taraf ke versi penuh tepat pada masanya.', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index cf3dfcc4f..0dcfae212 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -2889,6 +2889,27 @@ const message = { autoStartHelper: 'Se o serviço deve ser iniciado automaticamente após o Supervisor iniciar', }, }, + disk: { + management: 'Gerenciamento de Disco', + partition: 'Partição', + unmount: 'Desmontar', + unmountHelper: 'Deseja desmontar a partição {0}?', + mount: 'Montar', + partitionAlert: + 'O particionamento de disco requer formatação do disco, e os dados existentes serão excluídos. Salve ou tire snapshots dos dados com antecedência.', + mountPoint: 'Diretório de Montagem', + systemDisk: 'Disco do Sistema', + unpartitionedDisk: 'Disco Não Particionado', + handlePartition: 'Particionar Agora', + filesystem: 'Sistema de Arquivos', + unmounted: 'Desmontado', + cannotOperate: 'Não Pode Operar', + systemDiskHelper: 'Dica: O disco atual é o disco do sistema, não pode ser operado.', + autoMount: 'Montagem Automática', + model: 'Modelo do Dispositivo', + diskType: 'Tipo de Disco', + serial: 'Número de Série', + }, xpack: { expiresTrialAlert: 'Lembrete: Sua versão de avaliação profissional expirará em {0} dias. Após isso, todas as funcionalidades da versão profissional não estarão mais disponíveis. Por favor, renove ou faça upgrade para a versão oficial a tempo.', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index f39e32eb8..ab57b9479 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -2881,6 +2881,27 @@ const message = { autoStartHelper: 'Автоматически запускать сервис после запуска Supervisor', }, }, + disk: { + management: 'Управление дисками', + partition: 'Раздел', + unmount: 'Отмонтировать', + unmountHelper: 'Вы хотите отмонтировать раздел {0}?', + mount: 'Подключить', + partitionAlert: + 'Разделение диска требует форматирования диска, и существующие данные будут удалены. Пожалуйста, сохраните или сделайте снимки данных заранее.', + mountPoint: 'Точка монтирования', + systemDisk: 'Системный диск', + unpartitionedDisk: 'Неразделенный диск', + handlePartition: 'Разделить сейчас', + filesystem: 'Файловая система', + unmounted: 'Отмонтирован', + cannotOperate: 'Невозможно выполнить операцию', + systemDiskHelper: 'Подсказка: Текущий диск является системным диском, операции невозможны.', + autoMount: 'Автоматическое монтирование', + model: 'Модель устройства', + diskType: 'Тип диска', + serial: 'Серийный номер', + }, xpack: { expiresTrialAlert: 'Дружеское напоминание: ваша пробная версия Pro истечет через {0} дней, и все функции Pro станут недоступны. Пожалуйста, своевременно продлите или обновите до полной версии.', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 3aaed55a4..1b1dc8301 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -2964,6 +2964,27 @@ const message = { autoStartHelper: 'Supervisor başlatıldıktan sonra servis otomatik olarak başlatılsın mı?', }, }, + disk: { + management: 'Disk Yönetimi', + partition: 'Bölüm', + unmount: 'Bağını Kaldır', + unmountHelper: "Bölüm {0}'ın bağını kaldırmak istiyor musunuz?", + mount: 'Bağla', + partitionAlert: + 'Disk bölümleme, diske biçimlendirme gerektirir ve mevcut veriler silinir. Lütfen verilerinizi önceden kaydedin veya anlık görüntü alın.', + mountPoint: 'Bağlama Noktası', + systemDisk: 'Sistem Diski', + unpartitionedDisk: 'Bölümlendirilmemiş Disk', + handlePartition: 'Şimdi Bölümle', + filesystem: 'Dosya Sistemi', + unmounted: 'Bağlı Değil', + cannotOperate: 'Operasyon Yapılamıyor', + systemDiskHelper: 'İpucu: Mevcut disk sistem diskidir, işlem yapılamaz.', + autoMount: 'Otomatik Bağlama', + model: 'Cihaz Modeli', + diskType: 'Disk Türü', + serial: 'Seri Numarası', + }, xpack: { expiresTrialAlert: 'Nazik hatırlatma: Pro deneme sürümünüz {0} gün içinde sona erecek ve tüm Pro özellikleri kullanılamaz hale gelecektir. Lütfen zamanında yenileyin veya tam sürüme yükseltin.', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 35b837b35..ad78b4142 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -2727,6 +2727,26 @@ const message = { autoStartHelper: 'Supervisor 啟動後是否自動啟動服務', }, }, + disk: { + management: '磁盤管理', + partition: '分區', + unmount: '取消掛載', + unmountHelper: '是否取消掛載分區 {0}?', + mount: '掛載', + partitionAlert: '進行磁盤分區時需要格式化磁盤,原有數據將被刪除,請提前保存或快照數據', + mountPoint: '掛載目錄', + systemDisk: '系統磁盤', + unpartitionedDisk: '未分區磁盤', + handlePartition: '立即分區', + filesystem: '文件系統', + unmounted: '未掛載', + cannotOperate: '無法操作', + systemDiskHelper: '提示:當前磁盤為系統盤,無法進行操作', + autoMount: '自動掛載', + model: '設備型號', + diskType: '磁盤類型', + serial: '序列號', + }, xpack: { expiresTrialAlert: '溫馨提醒:您的專業版試用將在 {0} 天後到期,屆時所有專業版功能將無法繼續使用,請及時續費或升級到正式版本。', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index a5d926617..840684bdb 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -370,6 +370,7 @@ const message = { tamper: '防篡改', app: '应用', msgCenter: '任务中心', + disk: '磁盘管理', }, home: { recommend: '推荐', @@ -2718,6 +2719,26 @@ const message = { autoStartHelper: 'Supervisor 启动后是否自动启动服务', }, }, + disk: { + management: '磁盘管理', + partition: '分区', + unmount: '取消挂载', + unmountHelper: '是否取消挂载分区 {0}?', + mount: '挂载', + partitionAlert: '进行磁盘分区时需要格式化磁盘,原有数据将被删除,请提前保存或快照数据', + mountPoint: '挂载目录', + systemDisk: '系统磁盘', + unpartitionedDisk: '未分区磁盘', + handlePartition: '立即分区', + filesystem: '文件系统', + unmounted: '未挂载', + cannotOperate: '无法操作', + systemDiskHelper: '提示:当前磁盘为系统盘,无法进行操作', + autoMount: '自动挂载', + model: '设备型号', + diskType: '磁盘类型', + serial: '序列号', + }, xpack: { expiresAlert: '温馨提醒:专业版试用将于 [{0}] 天后到期,届时将停止使用所有专业版功能。', name: '专业版', diff --git a/frontend/src/routers/modules/host.ts b/frontend/src/routers/modules/host.ts index 4b6d90760..2dfb2028e 100644 --- a/frontend/src/routers/modules/host.ts +++ b/frontend/src/routers/modules/host.ts @@ -137,6 +137,16 @@ const hostRouter = { requiresAuth: false, }, }, + { + path: '/hosts/disk', + name: 'Disk', + props: true, + component: () => import('@/views/host/disk-management/disk/index.vue'), + meta: { + title: 'menu.disk', + requiresAuth: false, + }, + }, ], }; diff --git a/frontend/src/views/host/disk-management/components/disk-card.vue b/frontend/src/views/host/disk-management/components/disk-card.vue new file mode 100644 index 000000000..b4fb95533 --- /dev/null +++ b/frontend/src/views/host/disk-management/components/disk-card.vue @@ -0,0 +1,133 @@ + + + diff --git a/frontend/src/views/host/disk-management/disk/index.vue b/frontend/src/views/host/disk-management/disk/index.vue new file mode 100644 index 000000000..52e3d8887 --- /dev/null +++ b/frontend/src/views/host/disk-management/disk/index.vue @@ -0,0 +1,59 @@ + + diff --git a/frontend/src/views/host/disk-management/index.vue b/frontend/src/views/host/disk-management/index.vue new file mode 100644 index 000000000..07e428e2b --- /dev/null +++ b/frontend/src/views/host/disk-management/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/views/host/disk-management/partition/index.vue b/frontend/src/views/host/disk-management/partition/index.vue new file mode 100644 index 000000000..288430320 --- /dev/null +++ b/frontend/src/views/host/disk-management/partition/index.vue @@ -0,0 +1,118 @@ + + +