mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 09:02:05 -05:00
Merge branch 'main' into tooltips
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
## 2.2.1 [b5e48a0](https://github.com/sipeed/NanoKVM/commit/b5e48a07e82df3aedd60442342ae50b95684a697) (2025-02-21)
|
||||
|
||||
* fix: mounted image were not being detected correctly
|
||||
* perf: add support for CD-ROM mode when mounting image (thanks to [@scpcom](https://github.com/scpcom))
|
||||
* perf: add a loading state during login
|
||||
* perf: add changelog link in settings
|
||||
* perf: update translation and cleanup the code (thanks to [@ChokunPlayZ](https://github.com/ChokunPlayZ) [@Stoufiler](https://github.com/Stoufiler) [@polyzium](https://github.com/polyzium) [@Jonher937](https://github.com/Jonher937) [@S33G](https://github.com/S33G))
|
||||
|
||||
## 2.2.0 [0dbf8c0](https://github.com/sipeed/NanoKVM/commit/0dbf8c007f2d0183d0f0601c3da6d3c3fccd8b31) (2025-02-17)
|
||||
|
||||
NanoKVM [Image v1.4.0](https://github.com/sipeed/NanoKVM/releases/tag/v1.4.0) has been released!
|
||||
|
||||
@@ -5,9 +5,14 @@ type GetImagesRsp struct {
|
||||
}
|
||||
|
||||
type MountImageReq struct {
|
||||
File string `json:"file" validate:"omitempty"`
|
||||
File string `json:"file" validate:"omitempty"`
|
||||
Cdrom bool `json:"cdrom" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type GetMountedImageRsp struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
type GetCdRomRsp struct {
|
||||
Cdrom int64 `json:"cdrom"`
|
||||
}
|
||||
|
||||
@@ -14,4 +14,5 @@ func storageRouter(r *gin.Engine) {
|
||||
api.GET("/storage/image", service.GetImages) // get image list
|
||||
api.GET("/storage/image/mounted", service.GetMountedImage) // get mounted image
|
||||
api.POST("/storage/image/mount", service.MountImage) // mount image
|
||||
api.GET("/storage/cdrom", service.GetCdRom) // get CD-ROM flag
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -18,9 +19,7 @@ const (
|
||||
imageNone = "/dev/mmcblk0p3"
|
||||
cdromFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/cdrom"
|
||||
mountDevice = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/file"
|
||||
removableFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/removable"
|
||||
roFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/ro"
|
||||
roDisk = "/boot/usb.disk0.ro"
|
||||
)
|
||||
|
||||
func (s *Service) GetImages(c *gin.Context) {
|
||||
@@ -61,55 +60,43 @@ func (s *Service) MountImage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// cdrom and ro flag
|
||||
// set to 0 when unmount image
|
||||
// set to 1 when mount image and the CD-ROM is enabled
|
||||
if req.File == "" || req.Cdrom {
|
||||
flag := "0"
|
||||
if req.File != "" && req.Cdrom {
|
||||
flag = "1"
|
||||
}
|
||||
|
||||
// unmount
|
||||
if err := os.WriteFile(mountDevice, []byte("\n"), 0o666); err != nil {
|
||||
log.Errorf("unmount file failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "unmount image failed")
|
||||
return
|
||||
}
|
||||
|
||||
// ro flag
|
||||
if err := os.WriteFile(roFlag, []byte(flag), 0o666); err != nil {
|
||||
log.Errorf("set ro flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set ro flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// cdrom flag
|
||||
if err := os.WriteFile(cdromFlag, []byte(flag), 0o666); err != nil {
|
||||
log.Errorf("set cdrom flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set cdrom flag failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// mount
|
||||
image := req.File
|
||||
if image == "" {
|
||||
image = imageNone
|
||||
}
|
||||
|
||||
imageRemovable := "1"
|
||||
|
||||
imageRo := "0"
|
||||
isImageRo, _ := isFlagExist(roDisk)
|
||||
if isImageRo {
|
||||
imageRo = "1"
|
||||
}
|
||||
|
||||
imageLow := strings.ToLower(image)
|
||||
imageCdrom := "0"
|
||||
if strings.HasSuffix(imageLow, ".iso") {
|
||||
imageRo = "1"
|
||||
imageCdrom = "1"
|
||||
}
|
||||
|
||||
// unmount
|
||||
if err := os.WriteFile(mountDevice, []byte("\n"), 0o666); err != nil {
|
||||
log.Errorf("unmount file failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "unmount image failed")
|
||||
return
|
||||
}
|
||||
|
||||
// removable flag
|
||||
if err := os.WriteFile(removableFlag, []byte(imageRemovable), 0o666); err != nil {
|
||||
log.Errorf("set removable flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set removable flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// ro flag
|
||||
if err := os.WriteFile(roFlag, []byte(imageRo), 0o666); err != nil {
|
||||
log.Errorf("set ro flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set ro flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// cdrom flag
|
||||
if err := os.WriteFile(cdromFlag, []byte(imageCdrom), 0o666); err != nil {
|
||||
log.Errorf("set cdrom flag failed: %s", err)
|
||||
rsp.ErrRsp(c, -2, "set cdrom flag failed")
|
||||
return
|
||||
}
|
||||
|
||||
// mount
|
||||
if err := os.WriteFile(mountDevice, []byte(image), 0o666); err != nil {
|
||||
log.Errorf("mount file %s failed: %s", image, err)
|
||||
rsp.ErrRsp(c, -2, "mount image failed")
|
||||
@@ -128,6 +115,7 @@ func (s *Service) MountImage(c *gin.Context) {
|
||||
rsp.ErrRsp(c, -2, "execute command failed")
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
rsp.OkRsp(c)
|
||||
@@ -155,17 +143,25 @@ func (s *Service) GetMountedImage(c *gin.Context) {
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
func isFlagExist(flag string) (bool, error) {
|
||||
_, err := os.Stat(flag)
|
||||
func (s *Service) GetCdRom(c *gin.Context) {
|
||||
var rsp proto.Response
|
||||
|
||||
if err == nil {
|
||||
return true, nil
|
||||
content, err := os.ReadFile(cdromFlag)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -1, "read failed")
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
flag := strings.ReplaceAll(string(content), "\n", "")
|
||||
flatInt, err := strconv.ParseInt(flag, 10, 64)
|
||||
if err != nil {
|
||||
rsp.ErrRsp(c, -2, "parse failed")
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("check file %s err: %s", flag, err)
|
||||
return false, err
|
||||
data := &proto.GetCdRomRsp{
|
||||
Cdrom: flatInt,
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ var imageVersionMap = map[string]string{
|
||||
"2024-07-23-20-18-587710.img": "v1.1.0",
|
||||
"2024-08-08-19-44-bef2ca.img": "v1.2.0",
|
||||
"2024-11-13-09-59-9c961a.img": "v1.3.0",
|
||||
"2025-02-17-16-59-3649fe.img": "v1.4.0",
|
||||
}
|
||||
|
||||
func (s *Service) GetInfo(c *gin.Context) {
|
||||
|
||||
@@ -11,9 +11,15 @@ export function getMountedImage() {
|
||||
}
|
||||
|
||||
// mount/unmount image
|
||||
export function mountImage(file?: string) {
|
||||
export function mountImage(file?: string, cdrom?: boolean) {
|
||||
const data = {
|
||||
file: file ? file : ''
|
||||
file: file ? file : '',
|
||||
cdrom: cdrom
|
||||
};
|
||||
return http.post('/api/storage/image/mount', data);
|
||||
}
|
||||
|
||||
// get CD-ROM flag
|
||||
export function getCdRom() {
|
||||
return http.get('/api/storage/cdrom');
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ const en = {
|
||||
title: 'Images',
|
||||
loading: 'Loading...',
|
||||
empty: 'Nothing Found',
|
||||
cdrom: 'Mount the image in CD-ROM mode',
|
||||
mountFailed: 'Mount Failed',
|
||||
mountDesc:
|
||||
"In some systems, it's necessary to eject the virtual disk on the remote host before mounting the image.",
|
||||
|
||||
@@ -87,6 +87,7 @@ const zh = {
|
||||
title: '镜像',
|
||||
loading: '加载中',
|
||||
empty: '无镜像文件',
|
||||
cdrom: '以 CD-ROM 模式挂载镜像',
|
||||
mountFailed: '挂载失败',
|
||||
mountDesc: '在某些系统中,需要在远程主机中弹出虚拟硬盘后再挂载镜像。',
|
||||
tips: {
|
||||
|
||||
@@ -14,6 +14,8 @@ import { Tips } from './tips.tsx';
|
||||
export const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isLoading, setIsloading] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -29,6 +31,9 @@ export const Login = () => {
|
||||
}, [msg]);
|
||||
|
||||
function login(values: any) {
|
||||
if (isLoading) return;
|
||||
setIsloading(true);
|
||||
|
||||
const username = values.username;
|
||||
const password = encrypt(values.password);
|
||||
|
||||
@@ -48,6 +53,9 @@ export const Login = () => {
|
||||
})
|
||||
.catch(() => {
|
||||
setMsg(t('auth.error'));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsloading(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,7 +92,7 @@ export const Login = () => {
|
||||
<div className="text-red-500">{msg}</div>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" className="w-full">
|
||||
<Button type="primary" htmlType="submit" className="w-full" loading={isLoading}>
|
||||
{t('auth.loginButtonText')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
@@ -14,10 +14,11 @@ import * as api from '@/api/storage.ts';
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
|
||||
type ImagesProps = {
|
||||
cdrom: boolean;
|
||||
setIsMounted: (isMounted: boolean) => void;
|
||||
};
|
||||
|
||||
export const Images = ({ setIsMounted }: ImagesProps) => {
|
||||
export const Images = ({ cdrom, setIsMounted }: ImagesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [notify, contextHolder] = notification.useNotification();
|
||||
|
||||
@@ -77,7 +78,7 @@ export const Images = ({ setIsMounted }: ImagesProps) => {
|
||||
const filename = mountedImage === image ? '' : image;
|
||||
|
||||
api
|
||||
.mountImage(filename)
|
||||
.mountImage(filename, cdrom)
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
openNotification();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Divider, Popover, Tooltip } from 'antd';
|
||||
import { Divider, Popover, Switch, Tooltip } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { DiscIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import { getMountedImage } from '@/api/storage.ts';
|
||||
import { getCdRom, getMountedImage } from '@/api/storage.ts';
|
||||
|
||||
import { Images } from './images.tsx';
|
||||
import { Tips } from './tips.tsx';
|
||||
@@ -16,6 +16,7 @@ export const Image = () => {
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [cdrom, setCdrom] = useState(false);
|
||||
|
||||
const tooltip = t('image.title');
|
||||
const [tooltipValue, setTooltipValue] = useState(tooltip);
|
||||
@@ -25,18 +26,33 @@ export const Image = () => {
|
||||
if (rsp.code !== 0) return;
|
||||
setIsMounted(!!rsp.data?.file);
|
||||
});
|
||||
|
||||
getCdRom().then((rsp) => {
|
||||
if (rsp.code !== 0) return;
|
||||
setCdrom(rsp.data?.cdrom === 1);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const content = (
|
||||
<div className="min-w-[300px]">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-base font-bold text-neutral-300">{t('image.title')}</span>
|
||||
<Tips />
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-base font-bold text-neutral-300">{t('image.title')}</span>
|
||||
<Tips />
|
||||
</div>
|
||||
|
||||
<Tooltip title={t('image.cdrom')} placement="bottom">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-neutral-400">CD-ROM</span>
|
||||
|
||||
<Switch size="small" checked={cdrom} onChange={(checked) => setCdrom(checked)}></Switch>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '10px 0 15px 0' }} />
|
||||
|
||||
{isPopoverOpen && <Images setIsMounted={setIsMounted} />}
|
||||
{isPopoverOpen && <Images cdrom={cdrom} setIsMounted={setIsMounted} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -51,13 +51,13 @@ export const Tips = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer items-center space-x-1 text-neutral-400"
|
||||
className="flex cursor-pointer items-center space-x-1 text-neutral-400 hover:text-blue-400"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<CircleHelpIcon size={16} />
|
||||
<span className="text-sm text-neutral-500 hover:text-neutral-400">
|
||||
{t('image.tips.title')}
|
||||
</span>
|
||||
{/*<span className="text-sm text-neutral-500 hover:text-neutral-400">*/}
|
||||
{/* {t('image.tips.title')}*/}
|
||||
{/*</span>*/}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -68,7 +68,7 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
|
||||
setErrMsg('');
|
||||
|
||||
window.location.reload();
|
||||
}, 10000);
|
||||
}, 12000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,6 +114,17 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
|
||||
)}
|
||||
|
||||
{status === 'failed' && <Result subTitle={errMsg} />}
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
href="https://github.com/sipeed/NanoKVM/blob/main/CHANGELOG.md"
|
||||
target="_blank"
|
||||
>
|
||||
CHANGELOG
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user