Compare commits
9 Commits
90f4bf35e9
...
d765f9c2c3
Author | SHA1 | Date | |
---|---|---|---|
d765f9c2c3 | |||
21fd5de139 | |||
42f22c110c | |||
9822c5a72a | |||
452a395525 | |||
80d81c34bb | |||
b9353326f5 | |||
3ffc64f129 | |||
e869517bb1 |
@ -126,3 +126,12 @@ pub const IP_PROGRAM: &str = "/usr/sbin/ip";
|
|||||||
|
|
||||||
/// Copy program path
|
/// Copy program path
|
||||||
pub const COPY_PROGRAM: &str = "/bin/cp";
|
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";
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
use crate::constants;
|
use crate::constants;
|
||||||
use crate::utils::file_size_utils::FileSize;
|
use crate::utils::file_size_utils::FileSize;
|
||||||
|
use std::fs::File;
|
||||||
use std::os::linux::fs::MetadataExt;
|
use std::os::linux::fs::MetadataExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@ -34,7 +35,7 @@ pub enum DiskFileFormat {
|
|||||||
impl DiskFileFormat {
|
impl DiskFileFormat {
|
||||||
pub fn ext(&self) -> &'static [&'static str] {
|
pub fn ext(&self) -> &'static [&'static str] {
|
||||||
match self {
|
match self {
|
||||||
DiskFileFormat::Raw { .. } => &["", "raw"],
|
DiskFileFormat::Raw { .. } => &["raw", ""],
|
||||||
DiskFileFormat::QCow2 { .. } => &["qcow2"],
|
DiskFileFormat::QCow2 { .. } => &["qcow2"],
|
||||||
DiskFileFormat::CompressedRaw => &["raw.gz"],
|
DiskFileFormat::CompressedRaw => &["raw.gz"],
|
||||||
DiskFileFormat::CompressedQCow2 => &["qcow2.gz"],
|
DiskFileFormat::CompressedQCow2 => &["qcow2.gz"],
|
||||||
@ -152,10 +153,118 @@ impl DiskFileInfo {
|
|||||||
pub fn convert(&self, dest_file: &Path, dest_format: DiskFileFormat) -> anyhow::Result<()> {
|
pub fn convert(&self, dest_file: &Path, dest_format: DiskFileFormat) -> anyhow::Result<()> {
|
||||||
// Create a temporary directory to perform the operation
|
// Create a temporary directory to perform the operation
|
||||||
let temp_dir = tempfile::tempdir_in(&AppConfig::get().temp_dir)?;
|
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
|
// Prepare the conversion
|
||||||
let mut cmd = match (self.format, dest_format) {
|
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
|
// Dumb copy of file
|
||||||
(a, b) if a == b => {
|
(a, b) if a == b => {
|
||||||
let mut cmd = Command::new(constants::COPY_PROGRAM);
|
let mut cmd = Command::new(constants::COPY_PROGRAM);
|
||||||
|
@ -38,7 +38,11 @@ export function ConvertDiskImageDialog(p: {
|
|||||||
if (value === "QCow2") setFilename(`${p.image.file_name}.qcow2`);
|
if (value === "QCow2") setFilename(`${p.image.file_name}.qcow2`);
|
||||||
if (value === "CompressedQCow2")
|
if (value === "CompressedQCow2")
|
||||||
setFilename(`${p.image.file_name}.qcow2.gz`);
|
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`);
|
if (value === "CompressedRaw") setFilename(`${p.image.file_name}.raw.gz`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
|
||||||
import LoopIcon from "@mui/icons-material/Loop";
|
import LoopIcon from "@mui/icons-material/Loop";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
@ -16,17 +16,17 @@ import { filesize } from "filesize";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { DiskImage, DiskImageApi } from "../api/DiskImageApi";
|
import { DiskImage, DiskImageApi } from "../api/DiskImageApi";
|
||||||
import { ServerApi } from "../api/ServerApi";
|
import { ServerApi } from "../api/ServerApi";
|
||||||
|
import { ConvertDiskImageDialog } from "../dialogs/ConvertDiskImageDialog";
|
||||||
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
|
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
|
||||||
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
|
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
|
||||||
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
||||||
|
import { downloadBlob } from "../utils/FilesUtils";
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { DateWidget } from "../widgets/DateWidget";
|
import { DateWidget } from "../widgets/DateWidget";
|
||||||
import { FileInput } from "../widgets/forms/FileInput";
|
import { FileInput } from "../widgets/forms/FileInput";
|
||||||
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
import { downloadBlob } from "../utils/FilesUtils";
|
|
||||||
import { ConvertDiskImageDialog } from "../dialogs/ConvertDiskImageDialog";
|
|
||||||
|
|
||||||
export function DiskImagesRoute(): React.ReactElement {
|
export function DiskImagesRoute(): React.ReactElement {
|
||||||
const [list, setList] = React.useState<DiskImage[] | undefined>();
|
const [list, setList] = React.useState<DiskImage[] | undefined>();
|
||||||
@ -226,13 +226,28 @@ function DiskImageList(p: {
|
|||||||
field: "format",
|
field: "format",
|
||||||
headerName: "Format",
|
headerName: "Format",
|
||||||
flex: 1,
|
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",
|
field: "file_size",
|
||||||
headerName: "File size",
|
headerName: "File size",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
renderCell(params) {
|
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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user