fix: mounted image were not being detected correctly

perf: add support for CD-ROM mode when mounting image
perf: add a loading state during login
perf: add changelog link in settings
This commit is contained in:
wj-xiao
2025-02-21 14:52:48 +08:00
parent 9c0c50140b
commit b5e48a07e8
10 changed files with 116 additions and 71 deletions

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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');
}

View File

@@ -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>

View File

@@ -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();

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import { Divider, Popover } 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,24 +16,40 @@ export const Image = () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [cdrom, setCdrom] = useState(false);
useEffect(() => {
getMountedImage().then((rsp) => {
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="是否以 CD-ROM 模式挂载镜像" 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>
);

View File

@@ -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

View File

@@ -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>
</>
);
};