2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 07:57:21 +08:00

feat: 添加磁盘管理工具到工具箱 (#1195)

* Initial plan

* 实现磁盘管理工具的后端和前端基础功能

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* 完成磁盘管理工具功能实现并验证构建成功

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* 添加输入验证防止命令注入攻击

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* 移除命令注入验证并修复评审意见

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* feat: merge main

* feat: merge main

* feat: 分区优化

* feat: fstab管理

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
Copilot
2026-01-09 03:22:35 +08:00
committed by GitHub
parent 24312f5993
commit 54b3b60efd
10 changed files with 1748 additions and 20 deletions

View File

@@ -0,0 +1,76 @@
package request
// ToolboxDiskDevice 磁盘设备请求
type ToolboxDiskDevice struct {
Device string `form:"device" json:"device" validate:"required"`
}
// ToolboxDiskMount 挂载请求
type ToolboxDiskMount struct {
Device string `form:"device" json:"device" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
WriteFstab bool `form:"write_fstab" json:"write_fstab"`
MountOption string `form:"mount_option" json:"mount_option"`
}
// ToolboxDiskUmount 卸载请求
type ToolboxDiskUmount struct {
Path string `form:"path" json:"path" validate:"required"`
}
// ToolboxDiskFormat 格式化请求
type ToolboxDiskFormat struct {
Device string `form:"device" json:"device" validate:"required"`
FsType string `form:"fs_type" json:"fs_type" validate:"required|in:ext4,ext3,xfs,btrfs"`
}
// ToolboxDiskVG 卷组请求
type ToolboxDiskVG struct {
Name string `form:"name" json:"name" validate:"required"`
Devices []string `form:"devices" json:"devices" validate:"required"`
}
// ToolboxDiskLV 逻辑卷请求
type ToolboxDiskLV struct {
Name string `form:"name" json:"name" validate:"required"`
VGName string `form:"vg_name" json:"vg_name" validate:"required"`
Size int `form:"size" json:"size" validate:"required|min:1"`
}
// ToolboxDiskVGName 卷组名称请求
type ToolboxDiskVGName struct {
Name string `form:"name" json:"name" validate:"required"`
}
// ToolboxDiskLVPath 逻辑卷路径请求
type ToolboxDiskLVPath struct {
Path string `form:"path" json:"path" validate:"required"`
}
// ToolboxDiskExtendLV 扩容逻辑卷请求
type ToolboxDiskExtendLV struct {
Path string `form:"path" json:"path" validate:"required"`
Size int `form:"size" json:"size" validate:"required|min:1"`
Resize bool `form:"resize" json:"resize"`
}
// ToolboxDiskInit 初始化磁盘请求
type ToolboxDiskInit struct {
Device string `form:"device" json:"device" validate:"required"`
FsType string `form:"fs_type" json:"fs_type" validate:"required|in:ext4,ext3,xfs,btrfs"`
}
// ToolboxDiskFstabEntry fstab 条目结构
type ToolboxDiskFstabEntry struct {
Device string `json:"device"` // 设备UUID=xxx 或 /dev/xxx
MountPoint string `json:"mount_point"` // 挂载点
FsType string `json:"fs_type"` // 文件系统类型
Options string `json:"options"` // 挂载选项
Dump string `json:"dump"` // 备份标志
Pass string `json:"pass"` // 检查顺序
}
// ToolboxDiskFstabDelete 删除 fstab 条目请求
type ToolboxDiskFstabDelete struct {
MountPoint string `form:"mount_point" json:"mount_point" validate:"required"`
}

View File

@@ -49,6 +49,7 @@ type Http struct {
toolboxSystem *service.ToolboxSystemService
toolboxBenchmark *service.ToolboxBenchmarkService
toolboxSSH *service.ToolboxSSHService
toolboxDisk *service.ToolboxDiskService
webhook *service.WebHookService
apps *apploader.Loader
}
@@ -87,6 +88,7 @@ func NewHttp(
toolboxSystem *service.ToolboxSystemService,
toolboxBenchmark *service.ToolboxBenchmarkService,
toolboxSSH *service.ToolboxSSHService,
toolboxDisk *service.ToolboxDiskService,
webhook *service.WebHookService,
apps *apploader.Loader,
) *Http {
@@ -124,6 +126,7 @@ func NewHttp(
toolboxSystem: toolboxSystem,
toolboxBenchmark: toolboxBenchmark,
toolboxSSH: toolboxSSH,
toolboxDisk: toolboxDisk,
webhook: webhook,
apps: apps,
}
@@ -463,6 +466,25 @@ func (route *Http) Register(r *chi.Mux) {
r.Post("/root_key", route.toolboxSSH.GenerateRootKey)
})
r.Route("/toolbox_disk", func(r chi.Router) {
r.Get("/list", route.toolboxDisk.List)
r.Post("/partitions", route.toolboxDisk.GetPartitions)
r.Post("/mount", route.toolboxDisk.Mount)
r.Post("/umount", route.toolboxDisk.Umount)
r.Post("/format", route.toolboxDisk.Format)
r.Post("/init", route.toolboxDisk.Init)
r.Get("/fstab", route.toolboxDisk.GetFstab)
r.Delete("/fstab", route.toolboxDisk.DeleteFstab)
r.Get("/lvm", route.toolboxDisk.GetLVMInfo)
r.Post("/lvm/pv", route.toolboxDisk.CreatePV)
r.Delete("/lvm/pv", route.toolboxDisk.RemovePV)
r.Post("/lvm/vg", route.toolboxDisk.CreateVG)
r.Delete("/lvm/vg", route.toolboxDisk.RemoveVG)
r.Post("/lvm/lv", route.toolboxDisk.CreateLV)
r.Delete("/lvm/lv", route.toolboxDisk.RemoveLV)
r.Post("/lvm/lv/extend", route.toolboxDisk.ExtendLV)
})
r.Route("/webhook", func(r chi.Router) {
r.Get("/", route.webhook.List)
r.Post("/", route.webhook.Create)

View File

@@ -38,5 +38,6 @@ var ProviderSet = wire.NewSet(
NewToolboxSystemService,
NewToolboxBenchmarkService,
NewToolboxSSHService,
NewToolboxDiskService,
NewWsService,
)

View File

@@ -0,0 +1,567 @@
package service
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"github.com/leonelquinteros/gotext"
"github.com/libtnb/chix"
"github.com/acepanel/panel/internal/http/request"
"github.com/acepanel/panel/pkg/shell"
)
type ToolboxDiskService struct {
t *gotext.Locale
}
func NewToolboxDiskService(t *gotext.Locale) *ToolboxDiskService {
return &ToolboxDiskService{
t: t,
}
}
// List 获取磁盘列表
func (s *ToolboxDiskService) List(w http.ResponseWriter, r *http.Request) {
// 获取磁盘基本信息
lsblkOutput, err := shell.Execf("lsblk -J -b -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,UUID,LABEL,MODEL")
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to get disk list: %v", err))
return
}
// 解析 lsblk JSON
var lsblkData struct {
BlockDevices []any `json:"blockdevices"`
}
if err = json.Unmarshal([]byte(lsblkOutput), &lsblkData); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to parse disk list: %v", err))
return
}
// 获取磁盘使用情况
dfOutput, _ := shell.Execf("df -B1 --output=source,size,used,avail,pcent,target 2>/dev/null | tail -n +2")
// 解析 df 输出为 map
dfMap := make(map[string]map[string]string)
lines := strings.Split(strings.TrimSpace(dfOutput), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) >= 6 {
mountpoint := fields[5]
dfMap[mountpoint] = map[string]string{
"size": fields[1],
"used": fields[2],
"avail": fields[3],
"percent": strings.TrimSuffix(fields[4], "%"),
}
}
}
Success(w, chix.M{
"disks": lsblkData.BlockDevices,
"df": dfMap,
})
}
// GetPartitions 获取分区列表
func (s *ToolboxDiskService) GetPartitions(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskDevice](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
output, err := shell.Execf("lsblk -J -b -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,UUID,LABEL '/dev/%s'", req.Device)
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to get partitions: %v", err))
return
}
Success(w, output)
}
// Mount 挂载分区
func (s *ToolboxDiskService) Mount(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskMount](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("test -d '%s' || mkdir -p '%s'", req.Path, req.Path); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to create mount point: %v", err))
return
}
if _, err = shell.Execf("mount '/dev/%s' '%s'", req.Device, req.Path); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to mount partition: %v", err))
return
}
// 如果需要写入 fstab
if req.WriteFstab {
// 获取分区的 UUID
uuid, err := shell.Execf("blkid -s UUID -o value '/dev/%s'", req.Device)
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to get partition UUID: %v", err))
return
}
uuid = strings.TrimSpace(uuid)
if uuid == "" {
Error(w, http.StatusInternalServerError, s.t.Get("partition has no UUID"))
return
}
// 获取文件系统类型
fsType, err := shell.Execf("blkid -s TYPE -o value '/dev/%s'", req.Device)
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to get filesystem type: %v", err))
return
}
fsType = strings.TrimSpace(fsType)
if fsType == "" {
fsType = "auto"
}
// 挂载选项
mountOption := req.MountOption
if mountOption == "" {
mountOption = "defaults"
}
// 检查 fstab 中是否已存在该挂载点
existCheck, _ := shell.Execf("grep -E '^[^#].*\\s+%s\\s+' /etc/fstab", req.Path)
if strings.TrimSpace(existCheck) != "" {
Error(w, http.StatusBadRequest, s.t.Get("mount point %s already exists in fstab", req.Path))
return
}
// 写入 fstab
fstabEntry := fmt.Sprintf("UUID=%s %s %s %s 0 2", uuid, req.Path, fsType, mountOption)
if _, err = shell.Execf("echo '%s' >> /etc/fstab", fstabEntry); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to write fstab: %v", err))
return
}
}
Success(w, nil)
}
// Umount 卸载分区
func (s *ToolboxDiskService) Umount(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskUmount](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("umount '%s'", req.Path); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to umount partition: %v", err))
return
}
Success(w, nil)
}
// Format 格式化分区
func (s *ToolboxDiskService) Format(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskFormat](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
var formatCmd string
switch req.FsType {
case "ext4":
formatCmd = fmt.Sprintf("mkfs.ext4 -F '/dev/%s'", req.Device)
case "ext3":
formatCmd = fmt.Sprintf("mkfs.ext3 -F '/dev/%s'", req.Device)
case "xfs":
formatCmd = fmt.Sprintf("mkfs.xfs -f '/dev/%s'", req.Device)
case "btrfs":
formatCmd = fmt.Sprintf("mkfs.btrfs -f '/dev/%s'", req.Device)
default:
Error(w, http.StatusUnprocessableEntity, s.t.Get("unsupported filesystem type: %s", req.FsType))
return
}
if _, err = shell.Execf(formatCmd); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to format partition: %v", err))
return
}
Success(w, nil)
}
// Init 初始化磁盘(删除所有分区,创建单个分区并格式化)
func (s *ToolboxDiskService) Init(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskInit](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
device := "/dev/" + req.Device
// 检查设备是否存在
if _, err = shell.Execf("test -b '%s'", device); err != nil {
Error(w, http.StatusBadRequest, s.t.Get("device not found: %s", device))
return
}
// 检查是否为系统盘(检查是否有分区挂载在 /
mountInfo, _ := shell.Execf("lsblk -no MOUNTPOINT '%s' 2>/dev/null", device)
if strings.Contains(mountInfo, "/\n") || strings.TrimSpace(mountInfo) == "/" {
Error(w, http.StatusBadRequest, s.t.Get("cannot initialize system disk"))
return
}
// 卸载该磁盘的所有分区
_, _ = shell.Execf("umount '%s'* 2>/dev/null || true", device)
// 先清除分区表
if _, err = shell.Execf("wipefs -a '%s'", device); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to wipe disk: %v", err))
return
}
// sfdisk 创建 GPT 分区表和单个分区
if _, err = shell.Execf("echo 'type=linux' | sfdisk '%s'", device); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to create partition: %v", err))
return
}
// 等待内核更新分区表
_, _ = shell.Execf("partprobe '%s' 2>/dev/null || true", device)
_, _ = shell.Execf("sleep 1")
// 确定新分区的设备名device + "1",如 sdb1 或 nvme0n1p1
var partDevice string
if strings.Contains(req.Device, "nvme") || strings.Contains(req.Device, "loop") {
partDevice = device + "p1"
} else {
partDevice = device + "1"
}
// 格式化新分区
var formatCmd string
switch req.FsType {
case "ext4":
formatCmd = fmt.Sprintf("mkfs.ext4 -F '%s'", partDevice)
case "ext3":
formatCmd = fmt.Sprintf("mkfs.ext3 -F '%s'", partDevice)
case "xfs":
formatCmd = fmt.Sprintf("mkfs.xfs -f '%s'", partDevice)
case "btrfs":
formatCmd = fmt.Sprintf("mkfs.btrfs -f '%s'", partDevice)
default:
Error(w, http.StatusUnprocessableEntity, s.t.Get("unsupported filesystem type: %s", req.FsType))
return
}
if _, err = shell.Execf(formatCmd); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to format partition: %v", err))
return
}
Success(w, nil)
}
// GetLVMInfo 获取LVM信息
func (s *ToolboxDiskService) GetLVMInfo(w http.ResponseWriter, r *http.Request) {
// 获取物理卷信息
pvOutput, _ := shell.Execf("pvdisplay -C --noheadings --separator '|' -o pv_name,vg_name,pv_size,pv_free 2>/dev/null || echo ''")
// 获取卷组信息
vgOutput, _ := shell.Execf("vgdisplay -C --noheadings --separator '|' -o vg_name,pv_count,lv_count,vg_size,vg_free 2>/dev/null || echo ''")
// 获取逻辑卷信息
lvOutput, _ := shell.Execf("lvdisplay -C --noheadings --separator '|' -o lv_name,vg_name,lv_size,lv_path 2>/dev/null || echo ''")
pvs := s.parseLVMOutput(pvOutput)
vgs := s.parseLVMOutput(vgOutput)
lvs := s.parseLVMOutput(lvOutput)
Success(w, chix.M{
"pvs": pvs,
"vgs": vgs,
"lvs": lvs,
})
}
// CreatePV 创建物理卷
func (s *ToolboxDiskService) CreatePV(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskDevice](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("pvcreate '/dev/%s'", req.Device); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to create physical volume: %v", err))
return
}
Success(w, nil)
}
// CreateVG 创建卷组
func (s *ToolboxDiskService) CreateVG(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskVG](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
// 构建设备列表,每个设备单独引用
var deviceArgs []string
for _, dev := range req.Devices {
deviceArgs = append(deviceArgs, fmt.Sprintf("'%s'", dev))
}
if _, err = shell.Execf("vgcreate '%s' %s", req.Name, strings.Join(deviceArgs, " ")); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to create volume group: %v", err))
return
}
Success(w, nil)
}
// CreateLV 创建逻辑卷
func (s *ToolboxDiskService) CreateLV(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskLV](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
// 验证逻辑卷大小(必须为正数)
if req.Size <= 0 {
Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid logical volume size"))
return
}
// 创建逻辑卷
if _, err = shell.Execf("lvcreate -L '%dG' -n '%s' '%s'", req.Size, req.Name, req.VGName); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to create logical volume: %v", err))
return
}
Success(w, nil)
}
// RemovePV 删除物理卷
func (s *ToolboxDiskService) RemovePV(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskDevice](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("pvremove '%s'", req.Device); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to remove physical volume: %v", err))
return
}
Success(w, nil)
}
// RemoveVG 删除卷组
func (s *ToolboxDiskService) RemoveVG(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskVGName](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("vgremove -f '%s'", req.Name); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to remove volume group: %v", err))
return
}
Success(w, nil)
}
// RemoveLV 删除逻辑卷
func (s *ToolboxDiskService) RemoveLV(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskLVPath](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("lvremove -f '%s'", req.Path); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to remove logical volume: %v", err))
return
}
Success(w, nil)
}
// ExtendLV 扩容逻辑卷
func (s *ToolboxDiskService) ExtendLV(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskExtendLV](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
// 验证扩容大小为正整数
if req.Size <= 0 {
Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid size"))
return
}
// 扩容逻辑卷
if _, err = shell.Execf("lvextend -L +%dG '%s'", req.Size, req.Path); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to extend logical volume: %v", err))
return
}
// 扩展文件系统
if req.Resize {
// 检测文件系统类型并扩展
fsType, _ := shell.Execf("blkid -o value -s TYPE '%s'", req.Path)
fsType = strings.TrimSpace(fsType)
switch fsType {
case "ext4", "ext3":
if _, err = shell.Execf("resize2fs '%s'", req.Path); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to resize filesystem: %v", err))
return
}
case "xfs":
// XFS需要挂载后才能扩展
mountPoint, _ := shell.Execf("findmnt -n -o TARGET '%s'", req.Path)
mountPoint = strings.TrimSpace(mountPoint)
if mountPoint != "" {
if _, err = shell.Execf("xfs_growfs '%s'", mountPoint); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to resize filesystem: %v", err))
return
}
} else {
// XFS未挂载时返回错误信息
Error(w, http.StatusInternalServerError, s.t.Get("xfs filesystem is not mounted, logical volume has been extended but filesystem was not resized"))
return
}
case "btrfs":
// btrfs需要挂载后才能扩展
mountPoint, _ := shell.Execf("findmnt -n -o TARGET '%s'", req.Path)
mountPoint = strings.TrimSpace(mountPoint)
if mountPoint != "" {
// 扩展到当前可用的最大空间
if _, err = shell.Execf("btrfs filesystem resize max '%s'", mountPoint); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to resize filesystem: %v", err))
return
}
} else {
// btrfs未挂载时返回错误信息
Error(w, http.StatusInternalServerError, s.t.Get("btrfs filesystem is not mounted, logical volume has been extended but filesystem was not resized"))
return
}
}
}
Success(w, nil)
}
// parseLVMOutput 解析LVM命令输出
// 将LVM命令的表格输出解析为map数组每行数据的字段以field_0, field_1...命名
var spaceRegex = regexp.MustCompile(`\s+`)
func (s *ToolboxDiskService) parseLVMOutput(output string) []map[string]string {
lines := strings.Split(strings.TrimSpace(output), "\n")
var result []map[string]string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
line = spaceRegex.ReplaceAllString(line, " ")
fields := strings.Split(line, "|")
item := make(map[string]string)
for i, field := range fields {
item[fmt.Sprintf("field_%d", i)] = strings.TrimSpace(field)
}
result = append(result, item)
}
return result
}
// GetFstab 获取 fstab 列表
func (s *ToolboxDiskService) GetFstab(w http.ResponseWriter, r *http.Request) {
content, err := shell.Execf("cat /etc/fstab")
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to read fstab: %v", err))
return
}
var entries []request.ToolboxDiskFstabEntry
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// 跳过空行和注释
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
if len(fields) >= 4 {
entry := request.ToolboxDiskFstabEntry{
Device: fields[0],
MountPoint: fields[1],
FsType: fields[2],
Options: fields[3],
}
if len(fields) >= 5 {
entry.Dump = fields[4]
}
if len(fields) >= 6 {
entry.Pass = fields[5]
}
entries = append(entries, entry)
}
}
Success(w, entries)
}
// DeleteFstab 删除 fstab 条目
func (s *ToolboxDiskService) DeleteFstab(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxDiskFstabDelete](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
// 不允许删除根目录挂载
if req.MountPoint == "/" {
Error(w, http.StatusBadRequest, s.t.Get("cannot delete root mount point"))
return
}
if _, err = shell.Execf(`sed -i 's@^[^#].*\s%s\s.*$@@g' /etc/fstab`, req.MountPoint); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to delete fstab entry: %v", err))
return
}
if _, err = shell.Execf("mount -a"); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to remount filesystems: %v", err))
return
}
Success(w, nil)
}