Add the logic which will call disk image conversion from disk image route
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is failing
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	continuous-integration/drone/push Build is failing
				
			This commit is contained in:
		@@ -1,7 +1,7 @@
 | 
				
			|||||||
use crate::app_config::AppConfig;
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
use crate::constants;
 | 
					use crate::constants;
 | 
				
			||||||
use crate::controllers::HttpResult;
 | 
					use crate::controllers::HttpResult;
 | 
				
			||||||
use crate::utils::file_disks_utils::DiskFileInfo;
 | 
					use crate::utils::file_disks_utils::{DiskFileFormat, DiskFileInfo};
 | 
				
			||||||
use crate::utils::files_utils;
 | 
					use crate::utils::files_utils;
 | 
				
			||||||
use actix_files::NamedFile;
 | 
					use actix_files::NamedFile;
 | 
				
			||||||
use actix_multipart::form::MultipartForm;
 | 
					use actix_multipart::form::MultipartForm;
 | 
				
			||||||
@@ -91,6 +91,59 @@ pub async fn download(p: web::Path<DiskFilePath>, req: HttpRequest) -> HttpResul
 | 
				
			|||||||
    Ok(NamedFile::open(file_path)?.into_response(&req))
 | 
					    Ok(NamedFile::open(file_path)?.into_response(&req))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(serde::Deserialize)]
 | 
				
			||||||
 | 
					pub struct ConvertDiskImageRequest {
 | 
				
			||||||
 | 
					    dest_file_name: String,
 | 
				
			||||||
 | 
					    #[serde(flatten)]
 | 
				
			||||||
 | 
					    format: DiskFileFormat,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Convert disk image into a new format
 | 
				
			||||||
 | 
					pub async fn convert(
 | 
				
			||||||
 | 
					    p: web::Path<DiskFilePath>,
 | 
				
			||||||
 | 
					    req: web::Json<ConvertDiskImageRequest>,
 | 
				
			||||||
 | 
					) -> HttpResult {
 | 
				
			||||||
 | 
					    if !files_utils::check_file_name(&p.filename) {
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if !files_utils::check_file_name(&req.dest_file_name) {
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Invalid destination file name!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if !req
 | 
				
			||||||
 | 
					        .format
 | 
				
			||||||
 | 
					        .ext()
 | 
				
			||||||
 | 
					        .iter()
 | 
				
			||||||
 | 
					        .any(|e| req.dest_file_name.ends_with(e))
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let src_file_path = AppConfig::get()
 | 
				
			||||||
 | 
					        .disk_images_storage_path()
 | 
				
			||||||
 | 
					        .join(&p.filename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let src = DiskFileInfo::load_file(&src_file_path)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let dst_file_path = AppConfig::get()
 | 
				
			||||||
 | 
					        .disk_images_storage_path()
 | 
				
			||||||
 | 
					        .join(&req.dest_file_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if dst_file_path.exists() {
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::Conflict().json("Specified destination file already exists!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Perform conversion
 | 
				
			||||||
 | 
					    if let Err(e) = src.convert(&dst_file_path, req.format) {
 | 
				
			||||||
 | 
					        log::error!("Disk file conversion error: {e}");
 | 
				
			||||||
 | 
					        return Ok(
 | 
				
			||||||
 | 
					            HttpResponse::InternalServerError().json(format!("Disk file conversion error: {e}"))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(HttpResponse::Ok().json(src))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Delete a disk image
 | 
					/// Delete a disk image
 | 
				
			||||||
pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult {
 | 
					pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult {
 | 
				
			||||||
    if !files_utils::check_file_name(&p.filename) {
 | 
					    if !files_utils::check_file_name(&p.filename) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,6 +46,7 @@ struct ServerConstraints {
 | 
				
			|||||||
    memory_size: LenConstraints,
 | 
					    memory_size: LenConstraints,
 | 
				
			||||||
    disk_name_size: LenConstraints,
 | 
					    disk_name_size: LenConstraints,
 | 
				
			||||||
    disk_size: LenConstraints,
 | 
					    disk_size: LenConstraints,
 | 
				
			||||||
 | 
					    disk_image_name_size: LenConstraints,
 | 
				
			||||||
    net_name_size: LenConstraints,
 | 
					    net_name_size: LenConstraints,
 | 
				
			||||||
    net_title_size: LenConstraints,
 | 
					    net_title_size: LenConstraints,
 | 
				
			||||||
    net_nat_comment_size: LenConstraints,
 | 
					    net_nat_comment_size: LenConstraints,
 | 
				
			||||||
@@ -91,6 +92,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
				
			|||||||
                max: DISK_SIZE_MAX.as_bytes(),
 | 
					                max: DISK_SIZE_MAX.as_bytes(),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            disk_image_name_size: LenConstraints { min: 5, max: 220 },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            net_name_size: LenConstraints { min: 2, max: 50 },
 | 
					            net_name_size: LenConstraints { min: 2, max: 50 },
 | 
				
			||||||
            net_title_size: LenConstraints { min: 0, max: 50 },
 | 
					            net_title_size: LenConstraints { min: 0, max: 50 },
 | 
				
			||||||
            net_nat_comment_size: LenConstraints {
 | 
					            net_nat_comment_size: LenConstraints {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -349,6 +349,10 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
                "/api/disk_images/{filename}",
 | 
					                "/api/disk_images/{filename}",
 | 
				
			||||||
                web::get().to(disk_images_controller::download),
 | 
					                web::get().to(disk_images_controller::download),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            .route(
 | 
				
			||||||
 | 
					                "/api/disk_images/{filename}/convert",
 | 
				
			||||||
 | 
					                web::post().to(disk_images_controller::convert),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            .route(
 | 
					            .route(
 | 
				
			||||||
                "/api/disk_images/{filename}",
 | 
					                "/api/disk_images/{filename}",
 | 
				
			||||||
                web::delete().to(disk_images_controller::delete),
 | 
					                web::delete().to(disk_images_controller::delete),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,15 +13,32 @@ enum DisksError {
 | 
				
			|||||||
    Create,
 | 
					    Create,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, serde::Serialize)]
 | 
					#[derive(Debug, serde::Serialize, serde::Deserialize, Copy, Clone)]
 | 
				
			||||||
#[serde(tag = "format")]
 | 
					#[serde(tag = "format")]
 | 
				
			||||||
pub enum DiskFileFormat {
 | 
					pub enum DiskFileFormat {
 | 
				
			||||||
    Raw { is_sparse: bool },
 | 
					    Raw {
 | 
				
			||||||
    QCow2 { virtual_size: FileSize },
 | 
					        #[serde(default)]
 | 
				
			||||||
 | 
					        is_sparse: bool,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    QCow2 {
 | 
				
			||||||
 | 
					        #[serde(default)]
 | 
				
			||||||
 | 
					        virtual_size: FileSize,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    CompressedRaw,
 | 
					    CompressedRaw,
 | 
				
			||||||
    CompressedQCow2,
 | 
					    CompressedQCow2,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl DiskFileFormat {
 | 
				
			||||||
 | 
					    pub fn ext(&self) -> &'static [&'static str] {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            DiskFileFormat::Raw { .. } => &["", "raw"],
 | 
				
			||||||
 | 
					            DiskFileFormat::QCow2 { .. } => &["qcow2"],
 | 
				
			||||||
 | 
					            DiskFileFormat::CompressedRaw => &["raw.gz"],
 | 
				
			||||||
 | 
					            DiskFileFormat::CompressedQCow2 => &["qcow2.gz"],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Disk file information
 | 
					/// Disk file information
 | 
				
			||||||
#[derive(serde::Serialize)]
 | 
					#[derive(serde::Serialize)]
 | 
				
			||||||
pub struct DiskFileInfo {
 | 
					pub struct DiskFileInfo {
 | 
				
			||||||
@@ -125,6 +142,11 @@ impl DiskFileInfo {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Copy / convert file disk image into a new destination with optionally a new file format
 | 
				
			||||||
 | 
					    pub fn convert(&self, dest_file: &Path, dest_format: DiskFileFormat) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
					        todo!()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Deserialize)]
 | 
					#[derive(serde::Deserialize)]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,14 @@
 | 
				
			|||||||
#[derive(
 | 
					#[derive(
 | 
				
			||||||
    serde::Serialize, serde::Deserialize, Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord,
 | 
					    serde::Serialize,
 | 
				
			||||||
 | 
					    serde::Deserialize,
 | 
				
			||||||
 | 
					    Copy,
 | 
				
			||||||
 | 
					    Clone,
 | 
				
			||||||
 | 
					    Debug,
 | 
				
			||||||
 | 
					    Eq,
 | 
				
			||||||
 | 
					    PartialEq,
 | 
				
			||||||
 | 
					    PartialOrd,
 | 
				
			||||||
 | 
					    Ord,
 | 
				
			||||||
 | 
					    Default,
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
pub struct FileSize(usize);
 | 
					pub struct FileSize(usize);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,17 @@
 | 
				
			|||||||
import { APIClient } from "./ApiClient";
 | 
					import { APIClient } from "./ApiClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type DiskImageFormat =
 | 
				
			||||||
 | 
					  | { format: "Raw"; is_sparse: boolean }
 | 
				
			||||||
 | 
					  | { format: "QCow2"; virtual_size?: number }
 | 
				
			||||||
 | 
					  | { format: "CompressedQCow2" }
 | 
				
			||||||
 | 
					  | { format: "CompressedRaw" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type DiskImage = {
 | 
					export type DiskImage = {
 | 
				
			||||||
  file_size: number;
 | 
					  file_size: number;
 | 
				
			||||||
  file_name: string;
 | 
					  file_name: string;
 | 
				
			||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  created: number;
 | 
					  created: number;
 | 
				
			||||||
} & (
 | 
					} & DiskImageFormat;
 | 
				
			||||||
  | { format: "Raw"; is_sparse: boolean }
 | 
					 | 
				
			||||||
  | { format: "QCow2"; virtual_size: number }
 | 
					 | 
				
			||||||
  | { format: "CompressedQCow2" }
 | 
					 | 
				
			||||||
  | { format: "CompressedRaw" }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class DiskImageApi {
 | 
					export class DiskImageApi {
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -61,6 +62,21 @@ export class DiskImageApi {
 | 
				
			|||||||
    ).data;
 | 
					    ).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
 | 
					   * Delete disk image file
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ export interface ServerConstraints {
 | 
				
			|||||||
  memory_size: LenConstraint;
 | 
					  memory_size: LenConstraint;
 | 
				
			||||||
  disk_name_size: LenConstraint;
 | 
					  disk_name_size: LenConstraint;
 | 
				
			||||||
  disk_size: LenConstraint;
 | 
					  disk_size: LenConstraint;
 | 
				
			||||||
 | 
					  disk_image_name_size: LenConstraint;
 | 
				
			||||||
  net_name_size: LenConstraint;
 | 
					  net_name_size: LenConstraint;
 | 
				
			||||||
  net_title_size: LenConstraint;
 | 
					  net_title_size: LenConstraint;
 | 
				
			||||||
  net_nat_comment_size: LenConstraint;
 | 
					  net_nat_comment_size: LenConstraint;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										106
									
								
								virtweb_frontend/src/dialogs/ConvertDiskImageDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								virtweb_frontend/src/dialogs/ConvertDiskImageDialog.tsx
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,6 +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 RefreshIcon from "@mui/icons-material/Refresh";
 | 
				
			||||||
 | 
					import LoopIcon from "@mui/icons-material/Loop";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Alert,
 | 
					  Alert,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
@@ -25,6 +26,7 @@ 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 { 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>();
 | 
				
			||||||
@@ -162,8 +164,16 @@ function DiskImageList(p: {
 | 
				
			|||||||
  const confirm = useConfirm();
 | 
					  const confirm = useConfirm();
 | 
				
			||||||
  const loadingMessage = useLoadingMessage();
 | 
					  const loadingMessage = useLoadingMessage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [currConversion, setCurrConversion] = React.useState<
 | 
				
			||||||
 | 
					    DiskImage | undefined
 | 
				
			||||||
 | 
					  >();
 | 
				
			||||||
  const [dlProgress, setDlProgress] = React.useState<undefined | number>();
 | 
					  const [dlProgress, setDlProgress] = React.useState<undefined | number>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Convert disk image file
 | 
				
			||||||
 | 
					  const convertDiskImage = async (entry: DiskImage) => {
 | 
				
			||||||
 | 
					    setCurrConversion(entry);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Download disk image file
 | 
					  // Download disk image file
 | 
				
			||||||
  const downloadDiskImage = async (entry: DiskImage) => {
 | 
					  const downloadDiskImage = async (entry: DiskImage) => {
 | 
				
			||||||
    setDlProgress(0);
 | 
					    setDlProgress(0);
 | 
				
			||||||
@@ -236,11 +246,16 @@ function DiskImageList(p: {
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
      field: "actions",
 | 
					      field: "actions",
 | 
				
			||||||
      headerName: "",
 | 
					      headerName: "",
 | 
				
			||||||
      width: 120,
 | 
					      width: 140,
 | 
				
			||||||
      renderCell(params) {
 | 
					      renderCell(params) {
 | 
				
			||||||
        return (
 | 
					        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)}>
 | 
					              <IconButton onClick={() => downloadDiskImage(params.row)}>
 | 
				
			||||||
                <DownloadIcon />
 | 
					                <DownloadIcon />
 | 
				
			||||||
              </IconButton>
 | 
					              </IconButton>
 | 
				
			||||||
@@ -281,6 +296,20 @@ function DiskImageList(p: {
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </Alert>
 | 
					        </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} />
 | 
					      <DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} />
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								virtweb_frontend/src/widgets/FileDiskImageWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								virtweb_frontend/src/widgets/FileDiskImageWidget.tsx
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user