1 Commits

Author SHA1 Message Date
3680ceed61 Update Rust crate reqwest to 0.12.19
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-06-09 00:25:15 +00:00
11 changed files with 98 additions and 335 deletions

View File

@ -137,9 +137,6 @@ pub const PROGRAM_COPY: &str = "/bin/cp";
/// Gzip program path
pub const PROGRAM_GZIP: &str = "/usr/bin/gzip";
/// XZ program path
pub const PROGRAM_XZ: &str = "/usr/bin/xz";
/// Bash program
pub const PROGRAM_BASH: &str = "/usr/bin/bash";

View File

@ -28,10 +28,8 @@ pub enum DiskFileFormat {
#[serde(default)]
virtual_size: FileSize,
},
GzCompressedRaw,
GzCompressedQCow2,
XzCompressedRaw,
XzCompressedQCow2,
CompressedRaw,
CompressedQCow2,
}
impl DiskFileFormat {
@ -39,10 +37,8 @@ impl DiskFileFormat {
match self {
DiskFileFormat::Raw { .. } => &["raw", ""],
DiskFileFormat::QCow2 { .. } => &["qcow2"],
DiskFileFormat::GzCompressedRaw => &["raw.gz"],
DiskFileFormat::GzCompressedQCow2 => &["qcow2.gz"],
DiskFileFormat::XzCompressedRaw => &["raw.xz"],
DiskFileFormat::XzCompressedQCow2 => &["qcow2.xz"],
DiskFileFormat::CompressedRaw => &["raw.gz"],
DiskFileFormat::CompressedQCow2 => &["qcow2.gz"],
}
}
}
@ -85,14 +81,9 @@ impl DiskFileInfo {
},
"gz" if name.ends_with(".qcow2") => {
name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string();
DiskFileFormat::GzCompressedQCow2
DiskFileFormat::CompressedQCow2
}
"gz" => DiskFileFormat::GzCompressedRaw,
"xz" if name.ends_with(".qcow2") => {
name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string();
DiskFileFormat::XzCompressedQCow2
}
"xz" => DiskFileFormat::XzCompressedRaw,
"gz" => DiskFileFormat::CompressedRaw,
_ => anyhow::bail!("Unsupported disk extension: {ext}!"),
};
@ -168,8 +159,8 @@ impl DiskFileInfo {
// Prepare the conversion
let mut cmd = match (self.format, dest_format) {
// Decompress QCow2 (GZIP)
(DiskFileFormat::GzCompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
// Decompress QCow2
(DiskFileFormat::CompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--decompress")
@ -179,19 +170,8 @@ impl DiskFileInfo {
cmd
}
// Decompress QCow2 (XZ)
(DiskFileFormat::XzCompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--stdout")
.arg("--keep")
.arg("--decompress")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Compress QCow2 (Gzip)
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::GzCompressedQCow2) => {
// Compress QCow2
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::CompressedQCow2) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--to-stdout")
@ -200,16 +180,6 @@ impl DiskFileInfo {
cmd
}
// Compress QCow2 (Xz)
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::XzCompressedQCow2) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Convert QCow2 to Raw file
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::Raw { is_sparse }) => {
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
@ -274,8 +244,8 @@ impl DiskFileInfo {
cmd
}
// Compress Raw (Gz)
(DiskFileFormat::Raw { .. }, DiskFileFormat::GzCompressedRaw) => {
// Compress Raw
(DiskFileFormat::Raw { .. }, DiskFileFormat::CompressedRaw) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--to-stdout")
@ -284,18 +254,8 @@ impl DiskFileInfo {
cmd
}
// Compress Raw (Xz)
(DiskFileFormat::Raw { .. }, DiskFileFormat::XzCompressedRaw) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw (Gz) to not sparse file
(DiskFileFormat::GzCompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
// Decompress Raw to not sparse file
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--decompress")
@ -304,23 +264,13 @@ impl DiskFileInfo {
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw (Xz) to not sparse file
(DiskFileFormat::XzCompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--keep")
.arg("--decompress")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw (Gz) to sparse file
// Decompress Raw to sparse file
// https://benou.fr/www/ben/decompressing-sparse-files.html
(DiskFileFormat::GzCompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::PROGRAM_BASH);
cmd.arg("-c").arg(format!(
"{} --decompress --to-stdout {} | {} conv=sparse of={}",
"{} -d -c {} | {} conv=sparse of={}",
constants::PROGRAM_GZIP,
self.file_path.display(),
constants::PROGRAM_DD,
@ -329,20 +279,6 @@ impl DiskFileInfo {
cmd
}
// Decompress Raw (XZ) to sparse file
// https://benou.fr/www/ben/decompressing-sparse-files.html
(DiskFileFormat::XzCompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::PROGRAM_BASH);
cmd.arg("-c").arg(format!(
"{} --decompress --to-stdout {} | {} conv=sparse of={}",
constants::PROGRAM_XZ,
self.file_path.display(),
constants::PROGRAM_DD,
temp_file.display()
));
cmd
}
// Dumb copy of file
(a, b) if a == b => {
let mut cmd = Command::new(constants::PROGRAM_COPY);
@ -394,44 +330,6 @@ impl DiskFileInfo {
Ok(())
}
/// Get disk virtual size, if available
pub fn virtual_size(&self) -> Option<FileSize> {
match self.format {
DiskFileFormat::Raw { .. } => Some(self.file_size),
DiskFileFormat::QCow2 { virtual_size } => Some(virtual_size),
_ => None,
}
}
/// Resize disk
pub fn resize(&self, new_size: FileSize) -> anyhow::Result<()> {
if new_size <= self.virtual_size().unwrap_or(new_size) {
anyhow::bail!("Shrinking disk image file is not supported!");
}
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
cmd.arg("resize")
.arg("-f")
.arg(match self.format {
DiskFileFormat::QCow2 { .. } => "qcow2",
DiskFileFormat::Raw { .. } => "raw",
f => anyhow::bail!("Unsupported disk format for resize: {f:?}"),
})
.arg(&self.file_path)
.arg(new_size.as_bytes().to_string());
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"{} info failed, status: {}, stderr: {}",
constants::PROGRAM_QEMU_IMAGE,
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
}
#[derive(serde::Deserialize)]

View File

@ -44,9 +44,6 @@ pub struct VMFileDisk {
/// When creating a new disk, specify the disk image template to use
#[serde(skip_serializing_if = "Option::is_none")]
pub from_image: Option<String>,
/// Set this variable to true to resize disk image
#[serde(skip_serializing_if = "Option::is_none")]
pub resize: Option<bool>,
/// Set this variable to true to delete the disk
pub delete: bool,
}
@ -81,7 +78,6 @@ impl VMFileDisk {
delete: false,
from_image: None,
resize: None,
})
}
@ -148,10 +144,9 @@ impl VMFileDisk {
if file.exists() {
log::debug!("File {file:?} does not exists, so it was not touched");
return Ok(());
}
// Create disk if required
else {
// Determine file format
let format = match self.format {
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
@ -173,17 +168,6 @@ impl VMFileDisk {
src_file.convert(&file, format)?;
}
}
}
// Resize disk file if requested
if self.resize == Some(true) {
let disk = DiskFileInfo::load_file(&file)?;
// Can only increase disk size
if let Err(e) = disk.resize(self.size) {
log::error!("Failed to resize disk file {}: {e:?}", self.name);
}
}
Ok(())
}

View File

@ -4,10 +4,8 @@ import { VMFileDisk, VMInfo } from "./VMApi";
export type DiskImageFormat =
| { format: "Raw"; is_sparse: boolean }
| { format: "QCow2"; virtual_size?: number }
| { format: "GzCompressedQCow2" }
| { format: "GzCompressedRaw" }
| { format: "XzCompressedQCow2" }
| { format: "XzCompressedRaw" };
| { format: "CompressedQCow2" }
| { format: "CompressedRaw" };
export type DiskImage = {
file_size: number;

View File

@ -31,12 +31,8 @@ export interface BaseFileVMDisk {
// For new disk only
from_image?: string;
// Resize disk image after clone
resize?: boolean;
// application attributes
new?: boolean;
originalSize?: number;
deleteType?: "keepfile" | "deletefile";
}

View File

@ -42,15 +42,13 @@ export function ConvertDiskImageDialog(
setFormat({ format: value ?? ("QCow2" as any) });
if (value === "QCow2") setFilename(`${origFilename}.qcow2`);
if (value === "GzCompressedQCow2") setFilename(`${origFilename}.qcow2.gz`);
if (value === "XzCompressedQCow2") setFilename(`${origFilename}.qcow2.xz`);
if (value === "CompressedQCow2") setFilename(`${origFilename}.qcow2.gz`);
if (value === "Raw") {
setFilename(`${origFilename}.raw`);
// Check sparse checkbox by default
setFormat({ format: "Raw", is_sparse: true });
}
if (value === "GzCompressedRaw") setFilename(`${origFilename}.raw.gz`);
if (value === "XzCompressedRaw") setFilename(`${origFilename}.raw.xz`);
if (value === "CompressedRaw") setFilename(`${origFilename}.raw.gz`);
};
const handleSubmit = async () => {
@ -106,10 +104,8 @@ export function ConvertDiskImageDialog(
options={[
{ value: "QCow2" },
{ value: "Raw" },
{ value: "GzCompressedRaw" },
{ value: "XzCompressedRaw" },
{ value: "GzCompressedQCow2" },
{ value: "XzCompressedQCow2" },
{ value: "CompressedRaw" },
{ value: "CompressedQCow2" },
]}
/>

View File

@ -1,5 +1,5 @@
import VisibilityIcon from "@mui/icons-material/Visibility";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import {
Alert,
CircularProgress,
@ -36,9 +36,7 @@ export function LoginRoute(): React.ReactElement {
const canSubmit = username.length > 0 && password.length > 0;
const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => {
setShowPassword((show) => !show);
};
const handleClickShowPassword = () => { setShowPassword((show) => !show); };
const handleMouseDownPassword = (
event: React.MouseEvent<HTMLButtonElement>
@ -107,14 +105,12 @@ export function LoginRoute(): React.ReactElement {
label="Username"
name="username"
value={username}
onChange={(e) => {
setUsername(e.target.value);
}}
onChange={(e) => { setUsername(e.target.value); }}
autoComplete="username"
autoFocus
/>
<FormControl required fullWidth variant="outlined">
<FormControl fullWidth variant="outlined">
<InputLabel htmlFor="password">Password</InputLabel>
<OutlinedInput
required
@ -124,9 +120,7 @@ export function LoginRoute(): React.ReactElement {
type={showPassword ? "text" : "password"}
id="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
onChange={(e) => { setPassword(e.target.value); }}
autoComplete="current-password"
endAdornment={
<InputAdornment position="end">
@ -137,11 +131,7 @@ export function LoginRoute(): React.ReactElement {
onMouseDown={handleMouseDownPassword}
edge="end"
>
{showPassword ? (
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)}
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
</IconButton>
</Tooltip>
</InputAdornment>

View File

@ -1,25 +0,0 @@
import { ServerApi } from "../../api/ServerApi";
import { TextInput } from "./TextInput";
export function DiskSizeInput(p: {
editable: boolean;
label?: string;
value: number;
onChange?: (size: number) => void;
}): React.ReactElement {
return (
<TextInput
editable={p.editable}
label={p.label ?? "Disk size (GB)"}
size={{
min: ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
max: ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
}}
value={(p.value / (1000 * 1000 * 1000)).toString()}
onValueChange={(v) => {
p.onChange?.(Number(v ?? "0") * 1000 * 1000 * 1000);
}}
type="number"
/>
);
}

View File

@ -25,8 +25,6 @@ export function OEMStringFormWidget(p: {
p.onChange?.();
};
if (!p.editable && p.vm.oem_strings.length === 0) return <></>;
return (
<EditSection
title="SMBIOS OEM Strings"

View File

@ -2,8 +2,7 @@ import { mdiHarddiskPlus } from "@mdi/js";
import Icon from "@mdi/react";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import DeleteIcon from "@mui/icons-material/Delete";
import ExpandIcon from "@mui/icons-material/Expand";
import { Button, IconButton, Paper, Tooltip, Typography } from "@mui/material";
import { Button, IconButton, Paper, Tooltip } from "@mui/material";
import React from "react";
import { DiskImage } from "../../api/DiskImageApi";
import { ServerApi } from "../../api/ServerApi";
@ -14,7 +13,6 @@ import { VMDiskFileWidget } from "../vms/VMDiskFileWidget";
import { CheckboxInput } from "./CheckboxInput";
import { DiskBusSelect } from "./DiskBusSelect";
import { DiskImageSelect } from "./DiskImageSelect";
import { DiskSizeInput } from "./DiskSizeInput";
import { SelectInput } from "./SelectInput";
import { TextInput } from "./TextInput";
@ -69,12 +67,6 @@ export function VMDisksList(p: {
/>
))}
{p.vm.file_disks.length === 0 && (
<Typography style={{ textAlign: "center", paddingTop: "25px" }}>
No disk file yet!
</Typography>
)}
{p.editable && <Button onClick={addNewDisk}>Add new disk</Button>}
{/* Disk backup */}
@ -101,19 +93,6 @@ function DiskInfo(p: {
diskImagesList: DiskImage[];
}): React.ReactElement {
const confirm = useConfirm();
const expandDisk = () => {
if (p.disk.resize === true) {
p.disk.resize = false;
p.disk.size = p.disk.originalSize!;
} else {
p.disk.resize = true;
p.disk.originalSize = p.disk.size!;
}
p.onChange?.();
};
const deleteDisk = async () => {
if (p.disk.deleteType) {
p.disk.deleteType = undefined;
@ -136,29 +115,10 @@ function DiskInfo(p: {
if (!p.editable || !p.disk.new)
return (
<>
<VMDiskFileWidget
{...p}
secondaryAction={
<>
{p.editable && !p.disk.deleteType && (
<IconButton
edge="end"
aria-label="expand disk"
onClick={expandDisk}
>
{p.disk.resize === true ? (
<Tooltip title="Cancel disk expansion">
<ExpandIcon color="error" />
</Tooltip>
) : (
<Tooltip title="Increase disk size">
<ExpandIcon />
</Tooltip>
)}
</IconButton>
)}
{p.editable && (
<IconButton
edge="end"
@ -191,20 +151,6 @@ function DiskInfo(p: {
</>
}
/>
{/* New disk size*/}
{p.disk.resize && !p.disk.deleteType && (
<DiskSizeInput
editable
label="New disk size (GB)"
value={p.disk.size}
onChange={(v) => {
p.disk.size = v;
p.onChange?.();
}}
/>
)}
</>
);
return (
@ -266,32 +212,24 @@ function DiskInfo(p: {
/>
)}
{/* Resize disk image */}
{!!p.disk.from_image && (
<CheckboxInput
editable
checked={p.disk.resize}
label="Resize disk file"
<TextInput
editable={true}
label="Disk size (GB)"
size={{
min:
ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
max:
ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
}}
value={(p.disk.size / (1000 * 1000 * 1000)).toString()}
onValueChange={(v) => {
p.disk.resize = v;
p.disk.size = Number(v ?? "0") * 1000 * 1000 * 1000;
p.onChange?.();
}}
type="number"
disabled={!!p.disk.from_image}
/>
)}
{/* Disk size */}
{(!p.disk.from_image || p.disk.resize === true) && (
<DiskSizeInput
editable
value={p.disk.size}
onChange={(v) => {
p.disk.size = v;
p.onChange?.();
}}
/>
)}
{/* Disk image selection */}
<DiskImageSelect
label="Use disk image as template"
list={p.diskImagesList}

View File

@ -10,7 +10,6 @@ import {
ListItemAvatar,
ListItemText,
Tooltip,
Typography,
} from "@mui/material";
import Grid from "@mui/material/Grid";
import { NWFilter } from "../../api/NWFilterApi";
@ -50,12 +49,6 @@ export function VMNetworksList(p: {
</div>
)}
{p.vm.networks.length === 0 && (
<Typography style={{ textAlign: "center", paddingTop: "25px" }}>
No network interface defined yet!
</Typography>
)}
<Grid container spacing={2}>
{/* networks list */}
{p.vm.networks.map((n, num) => (