9 Commits

Author SHA1 Message Date
d765f9c2c3 Improve raw sparse conversion
Some checks failed
continuous-integration/drone/push Build is failing
2025-05-29 15:44:21 +02:00
21fd5de139 Can decompress raw files to sparse file 2025-05-29 15:41:08 +02:00
42f22c110c Can compress raw files 2025-05-29 15:33:32 +02:00
9822c5a72a Can convert QCow2 to QCow2 2025-05-29 15:13:31 +02:00
452a395525 Can convert QCow2 to raw 2025-05-29 15:00:14 +02:00
80d81c34bb Add support of sparse files for QCow2 to sparse conversion 2025-05-29 14:51:57 +02:00
b9353326f5 Can convert QCow2 to raw file 2025-05-29 14:47:29 +02:00
3ffc64f129 Can compress QCow2 2025-05-29 14:22:09 +02:00
e869517bb1 Can decompress QCow2 2025-05-29 14:18:27 +02:00
4 changed files with 144 additions and 7 deletions

View File

@ -126,3 +126,12 @@ pub const IP_PROGRAM: &str = "/usr/sbin/ip";
/// Copy program path
pub const COPY_PROGRAM: &str = "/bin/cp";
/// Gzip program path
pub const GZIP_PROGRAM: &str = "/usr/bin/gzip";
/// Bash program
pub const BASH_PROGRAM: &str = "/usr/bin/bash";
/// DD program
pub const DD_PROGRAM: &str = "/usr/bin/dd";

View File

@ -1,6 +1,7 @@
use crate::app_config::AppConfig;
use crate::constants;
use crate::utils::file_size_utils::FileSize;
use std::fs::File;
use std::os::linux::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::process::Command;
@ -34,7 +35,7 @@ pub enum DiskFileFormat {
impl DiskFileFormat {
pub fn ext(&self) -> &'static [&'static str] {
match self {
DiskFileFormat::Raw { .. } => &["", "raw"],
DiskFileFormat::Raw { .. } => &["raw", ""],
DiskFileFormat::QCow2 { .. } => &["qcow2"],
DiskFileFormat::CompressedRaw => &["raw.gz"],
DiskFileFormat::CompressedQCow2 => &["qcow2.gz"],
@ -152,10 +153,118 @@ impl DiskFileInfo {
pub fn convert(&self, dest_file: &Path, dest_format: DiskFileFormat) -> anyhow::Result<()> {
// Create a temporary directory to perform the operation
let temp_dir = tempfile::tempdir_in(&AppConfig::get().temp_dir)?;
let temp_file = temp_dir.path().join("temp_file");
let temp_file = temp_dir
.path()
.join(format!("temp_file.{}", dest_format.ext()[0]));
// Prepare the conversion
let mut cmd = match (self.format, dest_format) {
// Decompress QCow2
(DiskFileFormat::CompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::GZIP_PROGRAM);
cmd.arg("--keep")
.arg("--decompress")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Compress QCow2
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::CompressedQCow2) => {
let mut cmd = Command::new(constants::GZIP_PROGRAM);
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::QEMU_IMAGE_PROGRAM);
cmd.arg("convert").arg(&self.file_path).arg(&temp_file);
if !is_sparse {
cmd.args(["-S", "0"]);
}
cmd
}
// Clone a QCow file, using qemu-image instead of cp might improve "sparsification" of
// file
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("convert")
.arg("-O")
.arg("qcow2")
.arg(&self.file_path)
.arg(&temp_file);
cmd
}
// Convert Raw to QCow2 file
(DiskFileFormat::Raw { .. }, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("convert").arg(&self.file_path).arg(&temp_file);
cmd
}
// Render raw file non sparse
(DiskFileFormat::Raw { is_sparse: true }, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::COPY_PROGRAM);
cmd.arg("--sparse=never")
.arg(&self.file_path)
.arg(&temp_file);
cmd
}
// Render raw file sparse
(DiskFileFormat::Raw { is_sparse: false }, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::DD_PROGRAM);
cmd.arg("conv=sparse")
.arg(format!("if={}", self.file_path.display()))
.arg(format!("of={}", temp_file.display()));
cmd
}
// Compress Raw
(DiskFileFormat::Raw { .. }, DiskFileFormat::CompressedRaw) => {
let mut cmd = Command::new(constants::GZIP_PROGRAM);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw to not sparse file
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::GZIP_PROGRAM);
cmd.arg("--keep")
.arg("--decompress")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw to sparse file
// https://benou.fr/www/ben/decompressing-sparse-files.html
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::BASH_PROGRAM);
cmd.arg("-c").arg(format!(
"{} -d -c {} | {} conv=sparse of={}",
constants::GZIP_PROGRAM,
self.file_path.display(),
constants::DD_PROGRAM,
temp_file.display()
));
cmd
}
// Dumb copy of file
(a, b) if a == b => {
let mut cmd = Command::new(constants::COPY_PROGRAM);

View File

@ -38,7 +38,11 @@ export function ConvertDiskImageDialog(p: {
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 === "Raw") {
setFilename(`${p.image.file_name}.raw`);
// Check sparse checkbox by default
setFormat({ format: "Raw", is_sparse: true });
}
if (value === "CompressedRaw") setFilename(`${p.image.file_name}.raw.gz`);
};

View File

@ -1,7 +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 RefreshIcon from "@mui/icons-material/Refresh";
import {
Alert,
Button,
@ -16,17 +16,17 @@ import { filesize } from "filesize";
import React from "react";
import { DiskImage, DiskImageApi } from "../api/DiskImageApi";
import { ServerApi } from "../api/ServerApi";
import { ConvertDiskImageDialog } from "../dialogs/ConvertDiskImageDialog";
import { useAlert } from "../hooks/providers/AlertDialogProvider";
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
import { downloadBlob } from "../utils/FilesUtils";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { DateWidget } from "../widgets/DateWidget";
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>();
@ -226,13 +226,28 @@ function DiskImageList(p: {
field: "format",
headerName: "Format",
flex: 1,
renderCell(params) {
let content = params.row.format;
if (params.row.format === "Raw") {
content += params.row.is_sparse ? " (Sparse)" : " (Fixed)";
}
return content;
},
},
{
field: "file_size",
headerName: "File size",
flex: 1,
renderCell(params) {
return filesize(params.row.file_size);
let res = filesize(params.row.file_size);
if (params.row.format === "QCow2") {
res += ` (${filesize(params.row.virtual_size!)})`;
}
return res;
},
},
{