diff --git a/server/proto/vm.go b/server/proto/vm.go
index 9581114..9660772 100644
--- a/server/proto/vm.go
+++ b/server/proto/vm.go
@@ -84,6 +84,10 @@ type GetSSHStateRsp struct {
Enabled bool `json:"enabled"`
}
+type GetSwapStateRsp struct {
+ Enabled bool `json:"enabled"`
+}
+
type GetMdnsStateRsp struct {
Enabled bool `json:"enabled"`
}
diff --git a/server/router/vm.go b/server/router/vm.go
index fe34b0b..2a2954b 100644
--- a/server/router/vm.go
+++ b/server/router/vm.go
@@ -40,6 +40,10 @@ func vmRouter(r *gin.Engine) {
api.POST("/vm/ssh/enable", service.EnableSSH) // enable SSH
api.POST("/vm/ssh/disable", service.DisableSSH) // disable SSH
+ api.GET("/vm/swap", service.GetSwapState) // get Swap state
+ api.POST("/vm/swap/enable", service.EnableSwap) // enable Swap
+ api.POST("/vm/swap/disable", service.DisableSwap) // disable Swap
+
api.GET("/vm/mdns", service.GetMdnsState) // get mDNS state
api.POST("/vm/mdns/enable", service.EnableMdns) // enable mDNS
api.POST("/vm/mdns/disable", service.DisableMdns) // disable mDNS
diff --git a/server/service/vm/swap.go b/server/service/vm/swap.go
new file mode 100644
index 0000000..4b6fcd5
--- /dev/null
+++ b/server/service/vm/swap.go
@@ -0,0 +1,136 @@
+package vm
+
+import (
+ "NanoKVM-Server/proto"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+ log "github.com/sirupsen/logrus"
+)
+
+const (
+ SwapFile = "/swapfile"
+ SwapSize = "128M"
+ InittabPath = "/etc/inittab"
+ TempInittab = "/etc/inittab.tmp"
+)
+
+func (s *Service) GetSwapState(c *gin.Context) {
+ var rsp proto.Response
+
+ enabled := isSwapEnabled()
+ rsp.OkRspWithData(c, &proto.GetSwapStateRsp{
+ Enabled: enabled,
+ })
+}
+
+func (s *Service) EnableSwap(c *gin.Context) {
+
+ var rsp proto.Response
+
+ if err := exec.Command("fallocate", "-l", SwapSize, SwapFile).Run(); err != nil {
+ log.Errorf("create swap partition failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ if err := exec.Command("chmod", "600", SwapFile).Run(); err != nil {
+ log.Errorf("chmod failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ if err := exec.Command("mkswap", SwapFile).Run(); err != nil {
+ log.Errorf("formatting failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ if err := exec.Command("swapon", SwapFile).Run(); err != nil {
+ log.Errorf("enable swap partition failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ fstabEntry := fmt.Sprintf("\nsi11::sysinit:/sbin/swapon %s", SwapFile)
+ f, err := os.OpenFile(InittabPath, os.O_APPEND|os.O_WRONLY, 0644)
+
+ if err != nil {
+ log.Errorf("read fstab failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ defer f.Close()
+
+ _, err = f.WriteString(fstabEntry)
+ if err != nil {
+ log.Errorf("write fstab failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ rsp.OkRsp(c)
+ log.Debugf("Swap enabled")
+}
+
+func (s *Service) DisableSwap(c *gin.Context) {
+ var rsp proto.Response
+
+ input, err := os.ReadFile(InittabPath)
+ if err != nil {
+ log.Errorf("read fstab failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ lines := strings.Split(string(input), "\n")
+ output := make([]string, 0)
+ for _, line := range lines {
+ if !strings.Contains(line, SwapFile) {
+ output = append(output, line)
+ }
+ }
+
+ content := strings.Join(output, "\n")
+ content = strings.TrimSuffix(content, "\n")
+ if err := os.WriteFile(TempInittab, []byte(content), 0644); err != nil {
+ log.Errorf("write temp fstab failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ if err := os.Rename(TempInittab, InittabPath); err != nil {
+ log.Errorf("replace fstab failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ if err := exec.Command("swapoff", "-a").Run(); err != nil {
+ log.Errorf("close swap partition failed: %s", err)
+ rsp.ErrRsp(c, -1, "operation failed")
+ return
+ }
+
+ if err := os.Remove(SwapFile); err != nil && !os.IsNotExist(err) {
+ log.Warnf("delete swap file failed: %s (ignored)", err)
+ }
+
+ rsp.OkRsp(c)
+ log.Debugf("Swap disabled")
+}
+
+func isSwapEnabled() bool {
+ _, err := os.Stat(SwapFile)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/web/src/api/vm.ts b/web/src/api/vm.ts
index f3957ac..7a0cf43 100644
--- a/web/src/api/vm.ts
+++ b/web/src/api/vm.ts
@@ -77,6 +77,21 @@ export function disableSSH() {
return http.post('/api/vm/ssh/disable');
}
+// get Swap state
+export function getSwapState() {
+ return http.get('/api/vm/swap');
+}
+
+// enable Swap
+export function enableSwap() {
+ return http.post('/api/vm/swap/enable');
+}
+
+// disable Swap
+export function disableSwap() {
+ return http.post('/api/vm/swap/disable');
+}
+
// get mDNS state
export function getMdnsState() {
return http.get('/api/vm/mdns');
diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts
index 0e6af05..f86d0c5 100644
--- a/web/src/i18n/locales/en.ts
+++ b/web/src/i18n/locales/en.ts
@@ -220,6 +220,10 @@ const en = {
description: 'Enable SSH remote access',
tip: 'Set a strong password before enabling (Account - Change Password)'
},
+ swap: {
+ description: 'Enable the swap partition',
+ tip: 'Enable the default swap partition(128M)'
+ },
mdns: {
description: 'Enable mDNS discovery service',
tip: "Turning it off if it's not needed"
diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts
index 29e095a..a930870 100644
--- a/web/src/i18n/locales/zh.ts
+++ b/web/src/i18n/locales/zh.ts
@@ -204,6 +204,10 @@ const zh = {
description: '启用 SSH 远程访问',
tip: '启用前请务必设置强密码(帐号 - 修改密码)'
},
+ swap: {
+ description: '启用交换分区',
+ tip: '启用默认的交换分区(128M)'
+ },
mdns: {
description: '启用 mDNS 发现服务',
tip: '如果您未使用此功能,建议将其关闭'
diff --git a/web/src/pages/desktop/menu/settings/device/index.tsx b/web/src/pages/desktop/menu/settings/device/index.tsx
index 4045682..a510bd5 100644
--- a/web/src/pages/desktop/menu/settings/device/index.tsx
+++ b/web/src/pages/desktop/menu/settings/device/index.tsx
@@ -13,6 +13,7 @@ import { Reboot } from './reboot.tsx';
import { Ssh } from './ssh.tsx';
import { VirtualDevices } from './virtual-devices.tsx';
import { Wifi } from './wifi.tsx';
+import { Swap } from './swap.tsx';
export const Device = () => {
const { t } = useTranslation();
@@ -37,6 +38,7 @@ export const Device = () => {