From 53924dc8efe06ee341cd49101640a5a71e22aaca Mon Sep 17 00:00:00 2001 From: Guoguo Date: Tue, 25 Nov 2025 16:36:40 +0800 Subject: [PATCH] feat: add autostart feature --- server/proto/vm.go | 9 + server/router/vm.go | 5 + server/service/vm/autostart.go | 104 ++++++++++ web/src/api/autostart.ts | 20 ++ web/src/i18n/locales/en.ts | 11 ++ web/src/i18n/locales/zh.ts | 11 ++ .../settings/device/advanced/autostart.tsx | 185 ++++++++++++++++++ .../menu/settings/device/advanced/index.tsx | 4 +- 8 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 server/service/vm/autostart.go create mode 100644 web/src/api/autostart.ts create mode 100644 web/src/pages/desktop/menu/settings/device/advanced/autostart.tsx diff --git a/server/proto/vm.go b/server/proto/vm.go index 21d3844..4f2fe18 100644 --- a/server/proto/vm.go +++ b/server/proto/vm.go @@ -55,6 +55,15 @@ type DeleteScriptReq struct { Name string `validate:"required"` } +// autostart +type GetAutostartRsp struct { + Files []string `json:"files"` +} + +type UploadAutostartReq struct { + Content string `json:"content"` +} + type GetVirtualDeviceRsp struct { Network bool `json:"network"` Disk bool `json:"disk"` diff --git a/server/router/vm.go b/server/router/vm.go index f5a78a4..f41b6b6 100644 --- a/server/router/vm.go +++ b/server/router/vm.go @@ -63,5 +63,10 @@ func vmRouter(r *gin.Engine) { api.POST("/vm/tls", service.SetTls) // enable/disable TLS + api.GET("/vm/autostart", service.GetAutostart) // get autostart list + api.GET("/vm/autostart/:name", service.GetAutostartContent) // get autostart content + api.DELETE("/vm/autostart/:name", service.DeleteAutostart) // delete autostart script + api.POST("/vm/autostart/:name", service.UploadAutostart) // upload autostart script + api.POST("/vm/system/reboot", service.Reboot) // reboot system } diff --git a/server/service/vm/autostart.go b/server/service/vm/autostart.go new file mode 100644 index 0000000..3994f40 --- /dev/null +++ b/server/service/vm/autostart.go @@ -0,0 +1,104 @@ +package vm + +import ( + "NanoKVM-Server/proto" + "fmt" + "os" + "path/filepath" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +const autostartDirectory = "/etc/kvm/autostart" + +func (s *Service) GetAutostart(c *gin.Context) { + var rsp proto.Response + + var files []string + err := filepath.Walk(autostartDirectory, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + files = append(files, info.Name()) + } + + return nil + }) + if err != nil { + rsp.ErrRsp(c, -1, "get autostart directory fail") + return + } + rsp.OkRspWithData(c, &proto.GetAutostartRsp{ + Files: files, + }) + + log.Debugf("get autostart total %d", len(files)) +} + +func (s *Service) UploadAutostart(c *gin.Context) { + var req proto.UploadAutostartReq + var rsp proto.Response + + fileName := c.Param("name") + + if err := proto.ParseFormRequest(c, &req); err != nil { + rsp.ErrRsp(c, -1, "parse form request fail") + return + } + + if _, err := os.Stat(autostartDirectory); err != nil { + _ = os.MkdirAll(autostartDirectory, 0o755) + } + + target := fmt.Sprintf("%s/%s", autostartDirectory, fileName) + + f, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) + if err != nil { + rsp.ErrRsp(c, -1, "create file fail") + return + } + + defer f.Close() + _, err = f.WriteString(req.Content) + if err != nil { + rsp.ErrRsp(c, -1, "write content fail") + return + } + + rsp.OkRspWithData(c, fileName) + + log.Debugf("upload autostart %s success", fileName) +} + +func (s *Service) DeleteAutostart(c *gin.Context) { + var rsp proto.Response + + fileName := c.Param("name") + + file := fmt.Sprintf("%s/%s", autostartDirectory, fileName) + if err := os.Remove(file); err != nil { + log.Errorf("delete autostart file %s fail", fileName) + rsp.ErrRsp(c, -3, "remove file fail") + return + } + + rsp.OkRsp(c) + log.Debugf("delete autostart %s success", fileName) +} + +func (s *Service) GetAutostartContent(c *gin.Context) { + var rsp proto.Response + fileName := c.Param("name") + file := fmt.Sprintf("%s/%s", autostartDirectory, fileName) + content, err := os.ReadFile(file) + if err != nil { + rsp.ErrRsp(c, -1, "read file fail") + return + } + + rsp.OkRspWithData(c, string(content)) + log.Debugf("get autostart content %s success", fileName) +} diff --git a/web/src/api/autostart.ts b/web/src/api/autostart.ts new file mode 100644 index 0000000..151e032 --- /dev/null +++ b/web/src/api/autostart.ts @@ -0,0 +1,20 @@ +import { http } from '@/lib/http.ts'; + +export function getAutostart() { + return http.get('/api/vm/autostart'); +} + +export function uploadAutostart(name: string, content: string) { + return http.post('/api/vm/autostart/' + name, { content }); +} + +export function deleteAutostart(name: string) { + return http.request({ + url: '/api/vm/autostart/' + name, + method: 'delete', + }); +} + +export function getAutostartContent(name: string) { + return http.get('/api/vm/autostart/' + name); +} diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 286b692..3dad7eb 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -277,6 +277,17 @@ const en = { hdmi: { description: 'Enable HDMI/monitor output' }, + autostart: { + title: 'Autostart Scripts Settings', + description: 'Manage scripts that run automatically on system startup', + new: 'New', + deleteConfirm: 'Are you sure you want to delete this file?', + yes: 'Yes', + no: 'No', + scriptName: 'Autostart Script Name', + scriptContent: 'Autostart Script Content', + settings: 'Settings' + }, hidOnly: 'HID-Only Mode', disk: 'Virtual Disk', diskDesc: 'Mount virtual U-disk on the remote host', diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index cbf0c06..31dd049 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -251,6 +251,17 @@ const zh = { hdmi: { description: '启用 HDMI/显示器 输出功能' }, + autostart: { + title: '自动启动脚本设置', + description: '管理能够在 NanoKVM 启动时自动运行的脚本文件', + new: '创建新脚本', + deleteConfirm: '确定要删除该文件吗?', + yes: '是', + no: '否', + scriptName: '自动启动脚本名称', + scriptContent: '自动启动脚本内容', + settings: '设置' + }, hidOnly: 'HID-Only 模式', disk: '虚拟U盘', diskDesc: '在远程主机中挂载虚拟U盘', diff --git a/web/src/pages/desktop/menu/settings/device/advanced/autostart.tsx b/web/src/pages/desktop/menu/settings/device/advanced/autostart.tsx new file mode 100644 index 0000000..c326678 --- /dev/null +++ b/web/src/pages/desktop/menu/settings/device/advanced/autostart.tsx @@ -0,0 +1,185 @@ +import { useEffect, useState } from 'react'; +import { DeleteOutlined, EditOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons'; +import { Button, Input, Modal, Popconfirm, Space, Table } from 'antd'; +import TextArea from 'antd/es/input/TextArea'; +import type { TableProps } from 'antd/es/table'; +import { useSetAtom } from 'jotai'; +import { useTranslation } from 'react-i18next'; + +import * as api from '@/api/autostart.ts'; +import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts'; + +export const Autostart = () => { + interface AutostartItem { + name: string; + } + + const { t } = useTranslation(); + + const setIsKeyboardEnable = useSetAtom(isKeyboardEnableAtom); + + const [isEditAutostartOpen, setIsEditAutostartOpen] = useState(false); + const [isManageAutostartOpen, setIsManageAutostartOpen] = useState(false); + const [isAutostartNameEditable, setIsAutostartNameEditable] = useState(true); + const [autostartItems, setAutostartItems] = useState([]); + const [autostartName, setAutostartName] = useState(''); + const [autostartContent, setAutostartContent] = useState(''); + + const autostartColumns: TableProps['columns'] = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name' + }, + { + title: 'Action', + key: 'action', + render: (_, record) => ( + <> +