From 19ec9992be8594f12252169f0cec646340504fac Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 27 May 2025 21:17:16 +0200 Subject: [PATCH] Can get the list of uploaded disk images --- .../src/controllers/disk_images_controller.rs | 12 ++ virtweb_backend/src/main.rs | 11 +- virtweb_backend/src/utils/file_disks_utils.rs | 124 ++++++++++++++---- 3 files changed, 117 insertions(+), 30 deletions(-) diff --git a/virtweb_backend/src/controllers/disk_images_controller.rs b/virtweb_backend/src/controllers/disk_images_controller.rs index 8bd1dc0..94071bc 100644 --- a/virtweb_backend/src/controllers/disk_images_controller.rs +++ b/virtweb_backend/src/controllers/disk_images_controller.rs @@ -1,6 +1,7 @@ use crate::app_config::AppConfig; use crate::constants; use crate::controllers::HttpResult; +use crate::utils::file_disks_utils::DiskFileInfo; use crate::utils::files_utils; use actix_multipart::form::MultipartForm; use actix_multipart::form::tempfile::TempFile; @@ -55,3 +56,14 @@ pub async fn upload(MultipartForm(mut form): MultipartForm) Ok(HttpResponse::Ok().json("Successfully uploaded disk image!")) } + +/// Get disk images list +pub async fn get_list() -> HttpResult { + let mut list = vec![]; + for entry in AppConfig::get().disk_images_storage_path().read_dir()? { + let entry = entry?; + list.push(DiskFileInfo::load_file(&entry.path())?); + } + + Ok(HttpResponse::Ok().json(list)) +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 7d38406..855875a 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -1,4 +1,3 @@ -use std::cmp::max; use actix::Actor; use actix_cors::Cors; use actix_identity::IdentityMiddleware; @@ -14,6 +13,7 @@ use actix_web::middleware::Logger; use actix_web::web::Data; use actix_web::{App, HttpServer, web}; use light_openid::basic_state_manager::BasicStateManager; +use std::cmp::max; use std::time::Duration; use virtweb_backend::actors::libvirt_actor::LibVirtActor; use virtweb_backend::actors::vnc_tokens_actor::VNCTokensManager; @@ -121,7 +121,10 @@ async fn main() -> std::io::Result<()> { })) .app_data(conn.clone()) // Uploaded files - .app_data(MultipartFormConfig::default().total_limit(max(constants::DISK_IMAGE_MAX_SIZE,constants::ISO_MAX_SIZE))) + .app_data( + MultipartFormConfig::default() + .total_limit(max(constants::DISK_IMAGE_MAX_SIZE, constants::ISO_MAX_SIZE)), + ) .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir)) // Server controller .route( @@ -337,6 +340,10 @@ async fn main() -> std::io::Result<()> { "/api/disk_images/upload", web::post().to(disk_images_controller::upload), ) + .route( + "/api/disk_images/list", + web::get().to(disk_images_controller::get_list), + ) // API tokens controller .route( "/api/token/create", diff --git a/virtweb_backend/src/utils/file_disks_utils.rs b/virtweb_backend/src/utils/file_disks_utils.rs index 6cfdbcb..baa5e1a 100644 --- a/virtweb_backend/src/utils/file_disks_utils.rs +++ b/virtweb_backend/src/utils/file_disks_utils.rs @@ -6,6 +6,7 @@ use lazy_regex::regex; use std::os::linux::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::process::Command; +use std::time::UNIX_EPOCH; #[derive(thiserror::Error, Debug)] enum DisksError { @@ -52,35 +53,28 @@ impl FileDisk { pub fn load_from_file(path: &str) -> anyhow::Result { let file = Path::new(path); - if !file.is_file() { - return Err(DisksError::Parse("Path is not a file!").into()); - } - - let metadata = file.metadata()?; - let name = file.file_stem().and_then(|s| s.to_str()).unwrap_or("disk"); - let ext = file.extension().and_then(|s| s.to_str()).unwrap_or("raw"); - - // Approximate raw file estimation - let is_raw_sparse = metadata.len() / 512 >= metadata.st_blocks(); - - let format = match ext { - "qcow2" => DiskFormat::QCow2, - "raw" => DiskFormat::Raw { - alloc_type: match is_raw_sparse { - true => DiskAllocType::Sparse, - false => DiskAllocType::Fixed, - }, - }, - _ => anyhow::bail!("Unsupported disk extension: {ext}!"), - }; + let info = DiskFileInfo::load_file(file)?; Ok(Self { - name: name.to_string(), - size: match format { - DiskFormat::Raw { .. } => metadata.len() as usize / (1000 * 1000), - DiskFormat::QCow2 => qcow_virt_size(path)? / (1000 * 1000), + name: info.name, + + // Get only the virtual size of the file + size: match info.format { + DiskFileFormat::Raw { .. } => info.file_size, + DiskFileFormat::QCow2 { virtual_size } => virtual_size, + _ => anyhow::bail!("Unsupported image format: {:?}", info.format), + }, + + format: match info.format { + DiskFileFormat::Raw { is_sparse } => DiskFormat::Raw { + alloc_type: match is_sparse { + true => DiskAllocType::Sparse, + false => DiskAllocType::Fixed, + }, + }, + DiskFileFormat::QCow2 { .. } => DiskFormat::QCow2, + _ => anyhow::bail!("Unsupported image format: {:?}", info.format), }, - format, delete: false, }) } @@ -180,6 +174,74 @@ impl FileDisk { } } +#[derive(Debug, serde::Serialize)] +pub enum DiskFileFormat { + Raw { is_sparse: bool }, + QCow2 { virtual_size: usize }, + CompressedRaw, + CompressedQCow2, +} + +/// Disk file information +#[derive(serde::Serialize)] +pub struct DiskFileInfo { + file_size: usize, + format: DiskFileFormat, + file_name: String, + name: String, + created: u64, +} + +impl DiskFileInfo { + /// Get disk image file information + pub fn load_file(file: &Path) -> anyhow::Result { + if !file.is_file() { + return Err(DisksError::Parse("Path is not a file!").into()); + } + + // Get file metadata + let metadata = file.metadata()?; + let mut name = file + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("disk") + .to_string(); + let ext = file.extension().and_then(|s| s.to_str()).unwrap_or("raw"); + + // Determine file format + let format = match ext { + "qcow2" => DiskFileFormat::QCow2 { + virtual_size: qcow_virt_size(file)? / (1000 * 1000), + }, + "raw" => DiskFileFormat::Raw { + is_sparse: metadata.len() / 512 >= metadata.st_blocks(), + }, + "gz" if name.ends_with(".qcow2") => { + name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string(); + DiskFileFormat::CompressedQCow2 + } + "gz" => DiskFileFormat::CompressedRaw, + _ => anyhow::bail!("Unsupported disk extension: {ext}!"), + }; + + Ok(Self { + name, + file_size: metadata.len() as usize / (1000 * 1000), + format, + file_name: file + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(), + created: metadata + .created()? + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + }) + } +} + #[derive(serde::Deserialize)] struct QCowInfoOutput { #[serde(rename = "virtual-size")] @@ -187,10 +249,16 @@ struct QCowInfoOutput { } /// Get QCow2 virtual size -fn qcow_virt_size(path: &str) -> anyhow::Result { +fn qcow_virt_size(path: &Path) -> anyhow::Result { // Run qemu-img let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM); - cmd.args(["info", path, "--output", "json", "--force-share"]); + cmd.args([ + "info", + path.to_str().unwrap_or(""), + "--output", + "json", + "--force-share", + ]); let output = cmd.output()?; if !output.status.success() { anyhow::bail!(