diff --git a/backend/app/api/v1/device.go b/backend/app/api/v1/device.go index 2769d73ca086..8945b8e06f85 100644 --- a/backend/app/api/v1/device.go +++ b/backend/app/api/v1/device.go @@ -73,7 +73,7 @@ func (b *BaseApi) LoadDeviceConf(c *gin.Context) { // @Success 200 // @Security ApiKeyAuth // @Router /toolbox/device/update/byconf [post] -func (b *BaseApi) UpdateDevicByFile(c *gin.Context) { +func (b *BaseApi) UpdateDeviceByFile(c *gin.Context) { var req dto.UpdateByNameAndFile if err := helper.CheckBindAndValidate(&req, c); err != nil { return @@ -138,7 +138,7 @@ func (b *BaseApi) UpdateDeviceHost(c *gin.Context) { // @Success 200 // @Security ApiKeyAuth // @Router /toolbox/device/update/passwd [post] -func (b *BaseApi) UpdateDevicPasswd(c *gin.Context) { +func (b *BaseApi) UpdateDevicePasswd(c *gin.Context) { var req dto.ChangePasswd if err := helper.CheckBindAndValidate(&req, c); err != nil { return @@ -159,6 +159,28 @@ func (b *BaseApi) UpdateDevicPasswd(c *gin.Context) { helper.SuccessWithData(c, nil) } +// @Tags Device +// @Summary Update device swap +// @Description 修改系统 Swap +// @Accept json +// @Param request body dto.SwapHelper true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /toolbox/device/update/swap [post] +// @x-panel-log {"bodyKeys":["operate","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机 swap [path]","formatEN":"[operate] device swap [path]"} +func (b *BaseApi) UpdateDeviceSwap(c *gin.Context) { + var req dto.SwapHelper + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := deviceService.UpdateSwap(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, nil) +} + // @Tags Device // @Summary Check device DNS conf // @Description 检查系统 DNS 配置可用性 diff --git a/backend/app/dto/device.go b/backend/app/dto/device.go index 3cbc4f84362a..7aa283bd9d4a 100644 --- a/backend/app/dto/device.go +++ b/backend/app/dto/device.go @@ -8,6 +8,12 @@ type DeviceBaseInfo struct { LocalTime string `json:"localTime"` Ntp string `json:"ntp"` User string `json:"user"` + + SwapMemoryTotal uint64 `json:"swapMemoryTotal"` + SwapMemoryAvailable uint64 `json:"swapMemoryAvailable"` + SwapMemoryUsed uint64 `json:"swapMemoryUsed"` + + SwapDetails []SwapHelper `json:"swapDetails"` } type HostHelper struct { @@ -15,6 +21,14 @@ type HostHelper struct { Host string `json:"host"` } +type SwapHelper struct { + Path string `json:"path" validate:"required"` + Size uint64 `json:"size"` + Used string `json:"used"` + + IsNew bool `json:"isNew"` +} + type TimeZoneOptions struct { From string `json:"from"` Zones []string `json:"zones"` diff --git a/backend/app/service/device.go b/backend/app/service/device.go index 5f779e2be46f..0d4b00c73f1e 100644 --- a/backend/app/service/device.go +++ b/backend/app/service/device.go @@ -6,6 +6,8 @@ import ( "fmt" "net" "os" + "path" + "strconv" "strings" "time" @@ -14,10 +16,12 @@ import ( "github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/ntp" + "github.com/shirou/gopsutil/v3/mem" ) const defaultDNSPath = "/etc/resolv.conf" const defaultHostPath = "/etc/hosts" +const defaultFstab = "/etc/fstab" type DeviceService struct{} @@ -26,6 +30,7 @@ type IDeviceService interface { Update(key, value string) error UpdateHosts(req []dto.HostHelper) error UpdatePasswd(req dto.ChangePasswd) error + UpdateSwap(req dto.SwapHelper) error UpdateByConf(req dto.UpdateByNameAndFile) error LoadTimeZone() ([]string, error) CheckDNS(key, value string) (bool, error) @@ -47,6 +52,14 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) { ntp, _ := settingRepo.Get(settingRepo.WithByKey("NtpSite")) baseInfo.Ntp = ntp.Value + swapInfo, _ := mem.SwapMemory() + baseInfo.SwapMemoryTotal = swapInfo.Total + baseInfo.SwapMemoryAvailable = swapInfo.Free + baseInfo.SwapMemoryUsed = swapInfo.Used + if baseInfo.SwapMemoryTotal != 0 { + baseInfo.SwapDetails = loadSwap() + } + return baseInfo, nil } @@ -104,11 +117,20 @@ func (u *DeviceService) Update(key, value string) error { if err != nil { return errors.New(std) } - case "LocalTime": - if err := settingRepo.Update("NtpSite", value); err != nil { - return err + case "Ntp", "LocalTime": + ntpValue := value + if key == "LocalTime" { + ntpItem, err := settingRepo.Get(settingRepo.WithByKey("NtpSite")) + if err != nil { + return err + } + ntpValue = ntpItem.Value + } else { + if err := settingRepo.Update("NtpSite", ntpValue); err != nil { + return err + } } - ntime, err := ntp.GetRemoteTime(value) + ntime, err := ntp.GetRemoteTime(ntpValue) if err != nil { return err } @@ -168,6 +190,35 @@ func (u *DeviceService) UpdatePasswd(req dto.ChangePasswd) error { return nil } +func (u *DeviceService) UpdateSwap(req dto.SwapHelper) error { + if !req.IsNew { + std, err := cmd.Execf("%s swapoff %s", cmd.SudoHandleCmd(), req.Path) + if err != nil { + return fmt.Errorf("handle swapoff %s failed, err: %s", req.Path, std) + } + } + if req.Size == 0 { + if req.Path == path.Join(global.CONF.System.BaseDir, ".1panel_swap") { + _ = os.Remove(path.Join(global.CONF.System.BaseDir, ".1panel_swap")) + } + return operateSwapWithFile(true, req) + } + std1, err := cmd.Execf("%s dd if=/dev/zero of=%s bs=1024 count=%d", cmd.SudoHandleCmd(), req.Path, req.Size) + if err != nil { + return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std1) + } + std2, err := cmd.Execf("%s mkswap -f %s", cmd.SudoHandleCmd(), req.Path) + if err != nil { + return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std2) + } + std3, err := cmd.Execf("%s swapon %s", cmd.SudoHandleCmd(), req.Path) + if err != nil { + _, _ = cmd.Execf("%s swapoff %s", cmd.SudoHandleCmd(), req.Path) + return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std3) + } + return operateSwapWithFile(false, req) +} + func (u *DeviceService) LoadConf(name string) (string, error) { pathItem := "" switch name { @@ -291,3 +342,55 @@ func loadUser() string { } return strings.ReplaceAll(std, "\n", "") } + +func loadSwap() []dto.SwapHelper { + var data []dto.SwapHelper + std, err := cmd.Execf("%s swapon --show --summary", cmd.SudoHandleCmd()) + if err != nil { + return data + } + lines := strings.Split(std, "\n") + for index, line := range lines { + if index == 0 { + continue + } + parts := strings.Fields(line) + if len(parts) < 5 { + continue + } + sizeItem, _ := strconv.Atoi(parts[2]) + data = append(data, dto.SwapHelper{Path: parts[0], Size: uint64(sizeItem), Used: parts[3]}) + } + return data +} + +func operateSwapWithFile(delete bool, req dto.SwapHelper) error { + conf, err := os.ReadFile(defaultFstab) + if err != nil { + return fmt.Errorf("read file %s failed, err: %v", defaultFstab, err) + } + lines := strings.Split(string(conf), "\n") + newFile := "" + for _, line := range lines { + if len(line) == 0 { + continue + } + parts := strings.Fields(line) + if len(parts) == 6 && parts[0] == req.Path { + continue + } + newFile += line + "\n" + } + if !delete { + newFile += fmt.Sprintf("%s swap swap defaults 0 0\n", req.Path) + } + file, err := os.OpenFile(defaultFstab, os.O_WRONLY|os.O_TRUNC, 0640) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(newFile) + write.Flush() + return nil +} diff --git a/backend/router/ro_toolbox.go b/backend/router/ro_toolbox.go index 64a58a63d548..fb86e8ca0236 100644 --- a/backend/router/ro_toolbox.go +++ b/backend/router/ro_toolbox.go @@ -20,8 +20,9 @@ func (s *ToolboxRouter) InitToolboxRouter(Router *gin.RouterGroup) { toolboxRouter.GET("/device/zone/options", baseApi.LoadTimeOption) toolboxRouter.POST("/device/update/conf", baseApi.UpdateDeviceConf) toolboxRouter.POST("/device/update/host", baseApi.UpdateDeviceHost) - toolboxRouter.POST("/device/update/passwd", baseApi.UpdateDevicPasswd) - toolboxRouter.POST("/device/update/byconf", baseApi.UpdateDevicByFile) + toolboxRouter.POST("/device/update/passwd", baseApi.UpdateDevicePasswd) + toolboxRouter.POST("/device/update/swap", baseApi.UpdateDeviceSwap) + toolboxRouter.POST("/device/update/byconf", baseApi.UpdateDeviceByFile) toolboxRouter.POST("/device/check/dns", baseApi.CheckDNS) toolboxRouter.POST("/device/conf", baseApi.LoadDeviceConf) diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 2e53c8099847..063f2edeacfb 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -10331,6 +10331,49 @@ const docTemplate = `{ } } }, + "/toolbox/device/update/swap": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 Swap", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device swap", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SwapHelper" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate", + "path" + ], + "formatEN": "[operate] device swap [path]", + "formatZH": "[operate] 主机 swap [path]", + "paramKeys": [] + } + } + }, "/toolbox/device/zone/options": { "get": { "security": [ @@ -13748,6 +13791,9 @@ const docTemplate = `{ "openStdin": { "type": "boolean" }, + "privileged": { + "type": "boolean" + }, "publishAllPorts": { "type": "boolean" }, @@ -14519,6 +14565,21 @@ const docTemplate = `{ "ntp": { "type": "string" }, + "swapDetails": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SwapHelper" + } + }, + "swapMemoryAvailable": { + "type": "integer" + }, + "swapMemoryTotal": { + "type": "integer" + }, + "swapMemoryUsed": { + "type": "integer" + }, "timeZone": { "type": "string" }, @@ -16653,6 +16714,32 @@ const docTemplate = `{ } } }, + "dto.SwapHelper": { + "type": "object", + "required": [ + "operate", + "path" + ], + "properties": { + "operate": { + "type": "string", + "enum": [ + "create", + "delete", + "update" + ] + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "used": { + "type": "string" + } + } + }, "dto.TreeChild": { "type": "object", "properties": { diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 73e057a8a02b..41fa03ad92e6 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -10324,6 +10324,49 @@ } } }, + "/toolbox/device/update/swap": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 Swap", + "consumes": [ + "application/json" + ], + "tags": [ + "Device" + ], + "summary": "Update device swap", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SwapHelper" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "operate", + "path" + ], + "formatEN": "[operate] device swap [path]", + "formatZH": "[operate] 主机 swap [path]", + "paramKeys": [] + } + } + }, "/toolbox/device/zone/options": { "get": { "security": [ @@ -13741,6 +13784,9 @@ "openStdin": { "type": "boolean" }, + "privileged": { + "type": "boolean" + }, "publishAllPorts": { "type": "boolean" }, @@ -14512,6 +14558,21 @@ "ntp": { "type": "string" }, + "swapDetails": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SwapHelper" + } + }, + "swapMemoryAvailable": { + "type": "integer" + }, + "swapMemoryTotal": { + "type": "integer" + }, + "swapMemoryUsed": { + "type": "integer" + }, "timeZone": { "type": "string" }, @@ -16646,6 +16707,32 @@ } } }, + "dto.SwapHelper": { + "type": "object", + "required": [ + "operate", + "path" + ], + "properties": { + "operate": { + "type": "string", + "enum": [ + "create", + "delete", + "update" + ] + }, + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "used": { + "type": "string" + } + } + }, "dto.TreeChild": { "type": "object", "properties": { diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index 0b2304c57549..cd19f432ffe5 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -433,6 +433,8 @@ definitions: type: string openStdin: type: boolean + privileged: + type: boolean publishAllPorts: type: boolean restartPolicy: @@ -954,6 +956,16 @@ definitions: type: string ntp: type: string + swapDetails: + items: + $ref: '#/definitions/dto.SwapHelper' + type: array + swapMemoryAvailable: + type: integer + swapMemoryTotal: + type: integer + swapMemoryUsed: + type: integer timeZone: type: string user: @@ -2395,6 +2407,24 @@ definitions: required: - id type: object + dto.SwapHelper: + properties: + operate: + enum: + - create + - delete + - update + type: string + path: + type: string + size: + type: integer + used: + type: string + required: + - operate + - path + type: object dto.TreeChild: properties: id: @@ -11107,6 +11137,34 @@ paths: summary: Update device passwd tags: - Device + /toolbox/device/update/swap: + post: + consumes: + - application/json + description: 修改系统 Swap + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SwapHelper' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update device swap + tags: + - Device + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - operate + - path + formatEN: '[operate] device swap [path]' + formatZH: '[operate] 主机 swap [path]' + paramKeys: [] /toolbox/device/zone/options: get: consumes: diff --git a/frontend/src/api/interface/toolbox.ts b/frontend/src/api/interface/toolbox.ts index cb793d35e797..d27497445b43 100644 --- a/frontend/src/api/interface/toolbox.ts +++ b/frontend/src/api/interface/toolbox.ts @@ -7,6 +7,19 @@ export namespace Toolbox { user: string; timeZone: string; localTime: string; + + swapMemoryTotal: number; + swapMemoryAvailable: number; + swapMemoryUsed: number; + + swapDetails: Array; + } + export interface SwapHelper { + path: string; + size: number; + used: string; + + isNew: boolean; } export interface HostHelper { ip: string; diff --git a/frontend/src/api/modules/toolbox.ts b/frontend/src/api/modules/toolbox.ts index ced950fd76e5..9c9f826398d8 100644 --- a/frontend/src/api/modules/toolbox.ts +++ b/frontend/src/api/modules/toolbox.ts @@ -2,6 +2,7 @@ import http from '@/api'; import { UpdateByFile } from '../interface'; import { Toolbox } from '../interface/toolbox'; import { Base64 } from 'js-base64'; +import { TimeoutEnum } from '@/enums/http-enum'; // device export const getDeviceBase = () => { @@ -19,6 +20,9 @@ export const updateDeviceHost = (param: Array) => { export const updateDevicePasswd = (user: string, passwd: string) => { return http.post(`/toolbox/device/update/passwd`, { user: user, passwd: Base64.encode(passwd) }); }; +export const updateDeviceSwap = (params: Toolbox.SwapHelper) => { + return http.post(`/toolbox/device/update/swap`, params, TimeoutEnum.T_5M); +}; export const updateDeviceByConf = (name: string, file: string) => { return http.post(`/toolbox/device/update/byconf`, { name: name, file: file }); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 111dd2129585..865dc7890ac2 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -884,13 +884,30 @@ const message = { emptyTerminal: 'No terminal is currently connected', }, toolbox: { + swap: { + swap: 'Swap Partition', + swapHelper1: + 'The size of the swap should be 1 to 2 times the physical memory, adjustable based on specific requirements;', + swapHelper2: + 'Before creating a swap file, ensure that the system disk has sufficient available space, as the swap file size will occupy the corresponding disk space;', + swapHelper3: + 'Swap can help alleviate memory pressure, but it is only an alternative. Excessive reliance on swap may lead to a decrease in system performance. It is recommended to prioritize increasing memory or optimizing application memory usage;', + swapHelper4: 'It is advisable to regularly monitor the usage of swap to ensure normal system operation.', + swapDeleteHelper: + 'This operation will remove the Swap partition {0}. For system security reasons, the corresponding file will not be automatically deleted. If deletion is required, please proceed manually!', + saveHelper: 'Please save the current settings first!', + saveSwap: + 'Saving the current configuration will adjust the Swap partition {0} size to {1}. Do you want to continue?', + saveSwapHelper: 'The minimum partition size is 40 KB. Please modify and try again!', + swapOff: 'The minimum partition size is 40 KB. Setting it to 0 will disable the Swap partition.', + }, device: { dnsHelper: 'Server Address Domain Resolution', hostsHelper: 'Hostname Resolution', hosts: 'Domain', toolbox: 'Toolbox', hostname: 'Hostname', - passwd: 'Host Password', + passwd: 'System Password', passwdHelper: 'Input characters cannot include $ and &', timeZone: 'System Time Zone', localTime: 'Server Time', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index b5a800bbc350..20c452c127f3 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -845,13 +845,26 @@ const message = { emptyTerminal: '暫無終端連接', }, toolbox: { + swap: { + swap: 'Swap', + swapHelper1: 'Swap 的大小應該是物理內存的 1 到 2 倍,可根據具體情況進行調整;', + swapHelper2: '在創建 Swap 文件之前,請確保系統硬盤有足夠的可用空間,Swap 文件的大小將佔用相應的磁盤空間;', + swapHelper3: + 'Swap 可以幫助緩解內存壓力,但僅是一個備選項,過多依賴可能導致系統性能下降,建議優先考慮增加內存或者優化應用程序內存使用;', + swapHelper4: '建議定期監控 Swap 的使用情況,以確保系統正常運行。', + swapDeleteHelper: '此操作將移除 Swap 分區 {0},出於系統安全考慮,不會自動刪除該文件,如需刪除請手動操作!', + saveHelper: '請先保存當前設置!', + saveSwap: '儲存當前配置將調整 Swap 分區 {0} 大小到 {1},是否繼續?', + saveSwapHelper: '分區大小最小值為 40 KB,請修改後重試!', + swapOff: '分區大小最小值為 40 KB,設置為 0 則關閉 Swap 分區。', + }, device: { dnsHelper: '伺服器地址域名解析', hostsHelper: '主機名解析', hosts: '域名', toolbox: '工具箱', hostname: '主機名', - passwd: '主機密碼', + passwd: '系統密碼', passwdHelper: '輸入的字符不能包含 $ 和 &', timeZone: '系統時區', localTime: '伺服器時間', @@ -863,6 +876,7 @@ const message = { ntpALi: '阿里', ntpGoogle: '谷歌', syncSite: 'NTP 伺服器', + syncSiteHelper: '該操作將使用 {0} 作為源進行系統時間同步,是否繼續?', hostnameHelper: '主機名修改依賴於 hostnamectl 命令,如未安裝可能導致修改失敗', userHelper: '用戶名依賴於 whoami 命令獲取,如未安裝可能導致獲取失敗。', passwordHelper: '密碼修改依賴於 chpasswd 命令,如未安裝可能導致修改失敗', @@ -1090,7 +1104,6 @@ const message = { systemIP: '服務器地址', systemIPWarning: '當前未設置服務器地址,請先在面板設置中設置!', defaultNetwork: '默認網卡', - syncSiteHelper: '該操作將使用 {0} 作為源進行系統時間同步,是否繼續?', changePassword: '密碼修改', oldPassword: '原密碼', newPassword: '新密碼', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index eb94800ffcb6..908500d4390f 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -846,13 +846,26 @@ const message = { emptyTerminal: '暂无终端连接', }, toolbox: { + swap: { + swap: 'Swap 分区', + swapHelper1: 'Swap 的大小应该是物理内存的 1 到 2 倍,可根据具体情况进行调整;', + swapHelper2: '在创建 Swap 文件之前,请确保系统硬盘有足够的可用空间,Swap 文件的大小将占用相应的磁盘空间;', + swapHelper3: + 'Swap 可以帮助缓解内存压力,但仅是一个备选项,过多依赖可能导致系统性能下降,建议优先考虑增加内存或者优化应用程序内存使用;', + swapHelper4: '建议定期监控 Swap 的使用情况,以确保系统正常运行。', + swapDeleteHelper: '此操作将移除 Swap 分区 {0},出于系统安全考虑,不会自动删除该文件,如需删除请手动操作!', + saveHelper: '请先保存当前设置!', + saveSwap: '保存当前配置将调整 Swap 分区 {0} 大小到 {1},是否继续?', + saveSwapHelper: '分区大小最小值为 40 KB,请修改后重试!', + swapOff: '分区大小最小值为 40 KB,设置成 0 则关闭 Swap 分区。', + }, device: { dnsHelper: '服务器地址域名解析', hostsHelper: '主机名解析', hosts: '域名', toolbox: '工具箱', hostname: '主机名', - passwd: '主机密码', + passwd: '系统密码', passwdHelper: '输入字符不能包含 $ 和 &', timeZone: '系统时区', localTime: '服务器时间', @@ -864,6 +877,7 @@ const message = { ntpALi: '阿里', ntpGoogle: '谷歌', syncSite: 'NTP 服务器', + syncSiteHelper: '该操作将使用 {0} 作为源进行系统时间同步,是否继续?', hostnameHelper: '主机名修改依赖于 hostnamectl 命令,如未安装可能导致修改失败', userHelper: '用户名依赖于 whoami 命令获取,如未安装可能导致获取失败。', passwordHelper: '密码修改依赖于 chpasswd 命令,如未安装可能导致修改失败', @@ -1091,7 +1105,6 @@ const message = { systemIP: '服务器地址', systemIPWarning: '当前未设置服务器地址,请先在面板设置中设置!', defaultNetwork: '默认网卡', - syncSiteHelper: '该操作将使用 {0} 作为源进行系统时间同步,是否继续?', changePassword: '密码修改', oldPassword: '原密码', newPassword: '新密码', diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index 5053d3cacf0a..48190ba8c9ae 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -134,10 +134,25 @@ export function loadZero(i: number) { export function computeSize(size: number): string { const num = 1024.0; if (size < num) return size + ' B'; - if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + ' KB'; - if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + ' MB'; - if (size < Math.pow(num, 4)) return (size / Math.pow(num, 3)).toFixed(2) + ' GB'; - return (size / Math.pow(num, 4)).toFixed(2) + ' TB'; + if (size < Math.pow(num, 2)) return formattedNumber((size / num).toFixed(2)) + ' KB'; + if (size < Math.pow(num, 3)) return formattedNumber((size / Math.pow(num, 2)).toFixed(2)) + ' MB'; + if (size < Math.pow(num, 4)) return formattedNumber((size / Math.pow(num, 3)).toFixed(2)) + ' GB'; + return formattedNumber((size / Math.pow(num, 4)).toFixed(2)) + ' TB'; +} + +export function splitSize(size: number): any { + const num = 1024.0; + if (size < num) return { size: Number(size), unit: 'B' }; + if (size < Math.pow(num, 2)) return { size: formattedNumber((size / num).toFixed(2)), unit: 'KB' }; + if (size < Math.pow(num, 3)) + return { size: formattedNumber((size / Number(Math.pow(num, 2).toFixed(2))).toFixed(2)), unit: 'MB' }; + if (size < Math.pow(num, 4)) + return { size: formattedNumber((size / Number(Math.pow(num, 3))).toFixed(2)), unit: 'GB' }; + return { size: formattedNumber((size / Number(Math.pow(num, 4))).toFixed(2)), unit: 'TB' }; +} + +export function formattedNumber(num: string) { + return num.endsWith('.00') ? Number(num.slice(0, -3)) : Number(num); } export function computeSizeFromMB(size: number): string { diff --git a/frontend/src/views/container/network/create/index.vue b/frontend/src/views/container/network/create/index.vue index e096aab9cf63..401295612a54 100644 --- a/frontend/src/views/container/network/create/index.vue +++ b/frontend/src/views/container/network/create/index.vue @@ -203,13 +203,51 @@ const rules = reactive({ name: [Rules.requiredInput], driver: [Rules.requiredSelect], subnet: [{ validator: checkCidr, trigger: 'blur' }], - gateway: [Rules.ip], + gateway: [{ validator: checkGateway, trigger: 'blur' }], scope: [{ validator: checkCidr, trigger: 'blur' }], subnetV6: [{ validator: checkFixedCidrV6, trigger: 'blur' }], - gatewayV6: [Rules.ipV6], + gatewayV6: [{ validator: checkGatewayV6, trigger: 'blur' }], scopeV6: [{ validator: checkFixedCidrV6, trigger: 'blur' }], }); +function checkGateway(rule: any, value: any, callback: any) { + if (value === '') { + callback(); + } + const reg = + /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/; + if (!reg.test(value) && value !== '') { + return callback(new Error(i18n.global.t('commons.rule.formatErr'))); + } + callback(); +} + +function checkGatewayV6(rule: any, value: any, callback: any) { + if (value === '') { + callback(); + } else { + const IPv4SegmentFormat = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'; + const IPv4AddressFormat = `(${IPv4SegmentFormat}[.]){3}${IPv4SegmentFormat}`; + const IPv6SegmentFormat = '(?:[0-9a-fA-F]{1,4})'; + const IPv6AddressRegExp = new RegExp( + '^(' + + `(?:${IPv6SegmentFormat}:){7}(?:${IPv6SegmentFormat}|:)|` + + `(?:${IPv6SegmentFormat}:){6}(?:${IPv4AddressFormat}|:${IPv6SegmentFormat}|:)|` + + `(?:${IPv6SegmentFormat}:){5}(?::${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,2}|:)|` + + `(?:${IPv6SegmentFormat}:){4}(?:(:${IPv6SegmentFormat}){0,1}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,3}|:)|` + + `(?:${IPv6SegmentFormat}:){3}(?:(:${IPv6SegmentFormat}){0,2}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,4}|:)|` + + `(?:${IPv6SegmentFormat}:){2}(?:(:${IPv6SegmentFormat}){0,3}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,5}|:)|` + + `(?:${IPv6SegmentFormat}:){1}(?:(:${IPv6SegmentFormat}){0,4}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,6}|:)|` + + `(?::((?::${IPv6SegmentFormat}){0,5}:${IPv4AddressFormat}|(?::${IPv6SegmentFormat}){1,7}|:))` + + ')(%[0-9a-zA-Z-.:]{1,})?$', + ); + if (!IPv6AddressRegExp.test(value) && value !== '') { + return callback(new Error(i18n.global.t('commons.rule.formatErr'))); + } + callback(); + } +} + function checkCidr(rule: any, value: any, callback: any) { if (value === '') { callback(); diff --git a/frontend/src/views/home/status/index.vue b/frontend/src/views/home/status/index.vue index 5f60f90aae46..fe7be721aafc 100644 --- a/frontend/src/views/home/status/index.vue +++ b/frontend/src/views/home/status/index.vue @@ -71,7 +71,7 @@ {{ $t('home.free') }}: {{ formatNumber(currentInfo.swapMemoryAvailable / 1024 / 1024) }} MB - {{ $t('home.percent') }}: {{ formatNumber(100 - currentInfo.swapMemoryUsedPercent * 100) }}% + {{ $t('home.percent') }}: {{ formatNumber(currentInfo.swapMemoryUsedPercent * 100) }}% + + + + +