Add the logic which will call disk image conversion from disk image route
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-05-29 11:37:11 +02:00
parent e7ac0198ab
commit e017fe96d5
10 changed files with 279 additions and 13 deletions

View File

@ -1,16 +1,17 @@
import { APIClient } from "./ApiClient";
export type DiskImageFormat =
| { format: "Raw"; is_sparse: boolean }
| { format: "QCow2"; virtual_size?: number }
| { format: "CompressedQCow2" }
| { format: "CompressedRaw" };
export type DiskImage = {
file_size: number;
file_name: string;
name: string;
created: number;
} & (
| { format: "Raw"; is_sparse: boolean }
| { format: "QCow2"; virtual_size: number }
| { format: "CompressedQCow2" }
| { format: "CompressedRaw" }
);
} & DiskImageFormat;
export class DiskImageApi {
/**
@ -61,6 +62,21 @@ export class DiskImageApi {
).data;
}
/**
* Convert disk image file
*/
static async Convert(
file: DiskImage,
dest_file_name: string,
dest_format: DiskImageFormat
): Promise<void> {
await APIClient.exec({
method: "POST",
uri: `/disk_images/${file.file_name}/convert`,
jsonData: { ...dest_format, dest_file_name },
});
}
/**
* Delete disk image file
*/

View File

@ -22,6 +22,7 @@ export interface ServerConstraints {
memory_size: LenConstraint;
disk_name_size: LenConstraint;
disk_size: LenConstraint;
disk_image_name_size: LenConstraint;
net_name_size: LenConstraint;
net_title_size: LenConstraint;
net_nat_comment_size: LenConstraint;

View File

@ -0,0 +1,106 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material";
import { DiskImage, DiskImageApi, DiskImageFormat } from "../api/DiskImageApi";
import React from "react";
import { FileDiskImageWidget } from "../widgets/FileDiskImageWidget";
import { FileInput } from "../widgets/forms/FileInput";
import { TextInput } from "../widgets/forms/TextInput";
import { ServerApi } from "../api/ServerApi";
import { SelectInput } from "../widgets/forms/SelectInput";
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
import { useAlert } from "../hooks/providers/AlertDialogProvider";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
export function ConvertDiskImageDialog(p: {
image: DiskImage;
onCancel: () => void;
onFinished: () => void;
}): React.ReactElement {
const alert = useAlert();
const snackbar = useSnackbar();
const loadingMessage = useLoadingMessage();
const [format, setFormat] = React.useState<DiskImageFormat>({
format: "QCow2",
});
const [filename, setFilename] = React.useState(p.image.file_name + ".qcow2");
const handleFormatChange = async (value?: string) => {
setFormat({ format: value ?? ("QCow2" as any) });
if (value === "QCow2") setFilename(`${p.image.file_name}.qcow2`);
if (value === "CompressedQCow2")
setFilename(`${p.image.file_name}.qcow2.gz`);
if (value === "Raw") setFilename(`${p.image.file_name}.raw`);
if (value === "CompressedRaw") setFilename(`${p.image.file_name}.raw.gz`);
};
const handleSubmit = async () => {
try {
loadingMessage.show("Converting image...");
// Perform the conversion
await DiskImageApi.Convert(p.image, filename, format);
p.onFinished();
snackbar("Conversion successful!");
} catch (e) {
console.error("Failed to convert image!", e);
alert(`Failed to convert image! ${e}`);
} finally {
loadingMessage.hide();
}
};
return (
<Dialog open onClose={p.onCancel}>
<DialogTitle>Convert disk image</DialogTitle>
<DialogContent>
<DialogContentText>
Select the destination format for this image:
</DialogContentText>
<FileDiskImageWidget image={p.image} />
{/* New image format */}
<SelectInput
editable
label="Target format"
value={format.format}
onValueChange={handleFormatChange}
options={[
{ value: "QCow2" },
{ value: "Raw" },
{ value: "CompressedRaw" },
{ value: "CompressedQCow2" },
]}
/>
{/* New image name */}
<TextInput
editable
label="New image name"
value={filename}
onValueChange={(s) => setFilename(s ?? "")}
size={ServerApi.Config.constraints.disk_image_name_size}
helperText="The image name shall contain the proper file extension"
/>
</DialogContent>
<DialogActions>
<Button onClick={p.onCancel}>Cancel</Button>
<Button onClick={handleSubmit} autoFocus>
Convert image
</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -1,6 +1,7 @@
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import RefreshIcon from "@mui/icons-material/Refresh";
import LoopIcon from "@mui/icons-material/Loop";
import {
Alert,
Button,
@ -25,6 +26,7 @@ import { FileInput } from "../widgets/forms/FileInput";
import { VirtWebPaper } from "../widgets/VirtWebPaper";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { downloadBlob } from "../utils/FilesUtils";
import { ConvertDiskImageDialog } from "../dialogs/ConvertDiskImageDialog";
export function DiskImagesRoute(): React.ReactElement {
const [list, setList] = React.useState<DiskImage[] | undefined>();
@ -162,8 +164,16 @@ function DiskImageList(p: {
const confirm = useConfirm();
const loadingMessage = useLoadingMessage();
const [currConversion, setCurrConversion] = React.useState<
DiskImage | undefined
>();
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
// Convert disk image file
const convertDiskImage = async (entry: DiskImage) => {
setCurrConversion(entry);
};
// Download disk image file
const downloadDiskImage = async (entry: DiskImage) => {
setDlProgress(0);
@ -236,11 +246,16 @@ function DiskImageList(p: {
{
field: "actions",
headerName: "",
width: 120,
width: 140,
renderCell(params) {
return (
<>
<Tooltip title="Download image">
<Tooltip title="Convert disk image">
<IconButton onClick={() => convertDiskImage(params.row)}>
<LoopIcon />
</IconButton>
</Tooltip>
<Tooltip title="Download disk image">
<IconButton onClick={() => downloadDiskImage(params.row)}>
<DownloadIcon />
</IconButton>
@ -281,6 +296,20 @@ function DiskImageList(p: {
</div>
</Alert>
)}
{/* Disk image conversion dialog */}
{currConversion && (
<ConvertDiskImageDialog
image={currConversion}
onCancel={() => setCurrConversion(undefined)}
onFinished={() => {
setCurrConversion(undefined);
p.onReload();
}}
/>
)}
{/* The table itself */}
<DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} />
</>
);

View File

@ -0,0 +1,23 @@
import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material";
import { DiskImage } from "../api/DiskImageApi";
import { mdiHarddisk } from "@mdi/js";
import { filesize } from "filesize";
import Icon from "@mdi/react";
export function FileDiskImageWidget(p: {
image: DiskImage;
}): React.ReactElement {
return (
<ListItem>
<ListItemAvatar>
<Avatar>
<Icon path={mdiHarddisk} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={p.image.file_name}
secondary={`${p.image.format} - ${filesize(p.image.file_size)}`}
/>
</ListItem>
);
}