Can upload disk images on the server
	
		
			
	
		
	
	
		
	
		
			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:
		@@ -250,6 +250,11 @@ impl AppConfig {
 | 
				
			|||||||
        self.storage_path().join("iso")
 | 
					        self.storage_path().join("iso")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Get disk images storage directory
 | 
				
			||||||
 | 
					    pub fn disk_images_storage_path(&self) -> PathBuf {
 | 
				
			||||||
 | 
					        self.storage_path().join("disk_images")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Get VM vnc sockets directory
 | 
					    /// Get VM vnc sockets directory
 | 
				
			||||||
    pub fn vnc_sockets_path(&self) -> PathBuf {
 | 
					    pub fn vnc_sockets_path(&self) -> PathBuf {
 | 
				
			||||||
        self.storage_path().join("vnc")
 | 
					        self.storage_path().join("vnc")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,13 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
 | 
				
			|||||||
/// ISO max size
 | 
					/// ISO max size
 | 
				
			||||||
pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;
 | 
					pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Allowed uploaded disk images formats
 | 
				
			||||||
 | 
					pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 2] =
 | 
				
			||||||
 | 
					    ["application/x-qemu-disk", "application/gzip"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Disk image max size
 | 
				
			||||||
 | 
					pub const DISK_IMAGE_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000 * 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Min VM memory size (MB)
 | 
					/// Min VM memory size (MB)
 | 
				
			||||||
pub const MIN_VM_MEMORY: usize = 100;
 | 
					pub const MIN_VM_MEMORY: usize = 100;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										57
									
								
								virtweb_backend/src/controllers/disk_images_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								virtweb_backend/src/controllers/disk_images_controller.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					use crate::app_config::AppConfig;
 | 
				
			||||||
 | 
					use crate::constants;
 | 
				
			||||||
 | 
					use crate::controllers::HttpResult;
 | 
				
			||||||
 | 
					use crate::utils::files_utils;
 | 
				
			||||||
 | 
					use actix_multipart::form::MultipartForm;
 | 
				
			||||||
 | 
					use actix_multipart::form::tempfile::TempFile;
 | 
				
			||||||
 | 
					use actix_web::HttpResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, MultipartForm)]
 | 
				
			||||||
 | 
					pub struct UploadDiskImageForm {
 | 
				
			||||||
 | 
					    #[multipart(rename = "file")]
 | 
				
			||||||
 | 
					    files: Vec<TempFile>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Upload disk image file
 | 
				
			||||||
 | 
					pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>) -> HttpResult {
 | 
				
			||||||
 | 
					    if form.files.is_empty() {
 | 
				
			||||||
 | 
					        log::error!("Missing uploaded disk file!");
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Missing file!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let file = form.files.remove(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check uploaded file size
 | 
				
			||||||
 | 
					    if file.size > constants::DISK_IMAGE_MAX_SIZE {
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Disk image max size exceeded!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check file mime type
 | 
				
			||||||
 | 
					    if let Some(mime_type) = file.content_type {
 | 
				
			||||||
 | 
					        if !constants::ALLOWED_DISK_IMAGES_MIME_TYPES.contains(&mime_type.as_ref()) {
 | 
				
			||||||
 | 
					            return Ok(HttpResponse::BadRequest().json(format!(
 | 
				
			||||||
 | 
					                "Unsupported file type for disk upload: {}",
 | 
				
			||||||
 | 
					                mime_type
 | 
				
			||||||
 | 
					            )));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Extract and check file name
 | 
				
			||||||
 | 
					    let Some(file_name) = file.file_name else {
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Missing file name of uploaded file!"));
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    if !files_utils::check_file_name(&file_name) {
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::BadRequest().json("Invalid uploaded file name!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if a file with the same name already exists
 | 
				
			||||||
 | 
					    let dest_path = AppConfig::get().disk_images_storage_path().join(file_name);
 | 
				
			||||||
 | 
					    if dest_path.is_file() {
 | 
				
			||||||
 | 
					        return Ok(HttpResponse::Conflict().json("A file with the same name already exists!"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Copy the file to the destination
 | 
				
			||||||
 | 
					    file.file.persist(dest_path)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(HttpResponse::Ok().json("Successfully uploaded disk image!"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -7,6 +7,7 @@ use std::fmt::{Display, Formatter};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pub mod api_tokens_controller;
 | 
					pub mod api_tokens_controller;
 | 
				
			||||||
pub mod auth_controller;
 | 
					pub mod auth_controller;
 | 
				
			||||||
 | 
					pub mod disk_images_controller;
 | 
				
			||||||
pub mod groups_controller;
 | 
					pub mod groups_controller;
 | 
				
			||||||
pub mod iso_controller;
 | 
					pub mod iso_controller;
 | 
				
			||||||
pub mod network_controller;
 | 
					pub mod network_controller;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ struct StaticConfig {
 | 
				
			|||||||
    local_auth_enabled: bool,
 | 
					    local_auth_enabled: bool,
 | 
				
			||||||
    oidc_auth_enabled: bool,
 | 
					    oidc_auth_enabled: bool,
 | 
				
			||||||
    iso_mimetypes: &'static [&'static str],
 | 
					    iso_mimetypes: &'static [&'static str],
 | 
				
			||||||
 | 
					    disk_images_mimetypes: &'static [&'static str],
 | 
				
			||||||
    net_mac_prefix: &'static str,
 | 
					    net_mac_prefix: &'static str,
 | 
				
			||||||
    builtin_nwfilter_rules: &'static [&'static str],
 | 
					    builtin_nwfilter_rules: &'static [&'static str],
 | 
				
			||||||
    nwfilter_chains: &'static [&'static str],
 | 
					    nwfilter_chains: &'static [&'static str],
 | 
				
			||||||
@@ -37,6 +38,7 @@ struct SLenConstraints {
 | 
				
			|||||||
#[derive(serde::Serialize)]
 | 
					#[derive(serde::Serialize)]
 | 
				
			||||||
struct ServerConstraints {
 | 
					struct ServerConstraints {
 | 
				
			||||||
    iso_max_size: usize,
 | 
					    iso_max_size: usize,
 | 
				
			||||||
 | 
					    disk_image_max_size: usize,
 | 
				
			||||||
    vnc_token_duration: u64,
 | 
					    vnc_token_duration: u64,
 | 
				
			||||||
    vm_name_size: LenConstraints,
 | 
					    vm_name_size: LenConstraints,
 | 
				
			||||||
    vm_title_size: LenConstraints,
 | 
					    vm_title_size: LenConstraints,
 | 
				
			||||||
@@ -63,11 +65,13 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
				
			|||||||
        local_auth_enabled: *local_auth,
 | 
					        local_auth_enabled: *local_auth,
 | 
				
			||||||
        oidc_auth_enabled: !AppConfig::get().disable_oidc,
 | 
					        oidc_auth_enabled: !AppConfig::get().disable_oidc,
 | 
				
			||||||
        iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
 | 
					        iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
 | 
				
			||||||
 | 
					        disk_images_mimetypes: &constants::ALLOWED_DISK_IMAGES_MIME_TYPES,
 | 
				
			||||||
        net_mac_prefix: constants::NET_MAC_ADDR_PREFIX,
 | 
					        net_mac_prefix: constants::NET_MAC_ADDR_PREFIX,
 | 
				
			||||||
        builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
 | 
					        builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
 | 
				
			||||||
        nwfilter_chains: &constants::NETWORK_CHAINS,
 | 
					        nwfilter_chains: &constants::NETWORK_CHAINS,
 | 
				
			||||||
        constraints: ServerConstraints {
 | 
					        constraints: ServerConstraints {
 | 
				
			||||||
            iso_max_size: constants::ISO_MAX_SIZE,
 | 
					            iso_max_size: constants::ISO_MAX_SIZE,
 | 
				
			||||||
 | 
					            disk_image_max_size: constants::DISK_IMAGE_MAX_SIZE,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            vnc_token_duration: VNC_TOKEN_LIFETIME,
 | 
					            vnc_token_duration: VNC_TOKEN_LIFETIME,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					use std::cmp::max;
 | 
				
			||||||
use actix::Actor;
 | 
					use actix::Actor;
 | 
				
			||||||
use actix_cors::Cors;
 | 
					use actix_cors::Cors;
 | 
				
			||||||
use actix_identity::IdentityMiddleware;
 | 
					use actix_identity::IdentityMiddleware;
 | 
				
			||||||
@@ -22,8 +23,9 @@ use virtweb_backend::constants::{
 | 
				
			|||||||
    MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
 | 
					    MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use virtweb_backend::controllers::{
 | 
					use virtweb_backend::controllers::{
 | 
				
			||||||
    api_tokens_controller, auth_controller, groups_controller, iso_controller, network_controller,
 | 
					    api_tokens_controller, auth_controller, disk_images_controller, groups_controller,
 | 
				
			||||||
    nwfilter_controller, server_controller, static_controller, vm_controller,
 | 
					    iso_controller, network_controller, nwfilter_controller, server_controller, static_controller,
 | 
				
			||||||
 | 
					    vm_controller,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use virtweb_backend::libvirt_client::LibVirtClient;
 | 
					use virtweb_backend::libvirt_client::LibVirtClient;
 | 
				
			||||||
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
 | 
					use virtweb_backend::middlewares::auth_middleware::AuthChecker;
 | 
				
			||||||
@@ -55,6 +57,7 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    log::debug!("Create required directory, if missing");
 | 
					    log::debug!("Create required directory, if missing");
 | 
				
			||||||
    files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
 | 
					    files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
 | 
				
			||||||
 | 
					    files_utils::create_directory_if_missing(AppConfig::get().disk_images_storage_path()).unwrap();
 | 
				
			||||||
    files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap();
 | 
					    files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap();
 | 
				
			||||||
    files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap();
 | 
					    files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap();
 | 
				
			||||||
    files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap();
 | 
					    files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap();
 | 
				
			||||||
@@ -118,7 +121,7 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
            }))
 | 
					            }))
 | 
				
			||||||
            .app_data(conn.clone())
 | 
					            .app_data(conn.clone())
 | 
				
			||||||
            // Uploaded files
 | 
					            // Uploaded files
 | 
				
			||||||
            .app_data(MultipartFormConfig::default().total_limit(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))
 | 
					            .app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
 | 
				
			||||||
            // Server controller
 | 
					            // Server controller
 | 
				
			||||||
            .route(
 | 
					            .route(
 | 
				
			||||||
@@ -329,6 +332,11 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
                "/api/nwfilter/{uid}",
 | 
					                "/api/nwfilter/{uid}",
 | 
				
			||||||
                web::delete().to(nwfilter_controller::delete),
 | 
					                web::delete().to(nwfilter_controller::delete),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            // Disk images library
 | 
				
			||||||
 | 
					            .route(
 | 
				
			||||||
 | 
					                "/api/disk_images/upload",
 | 
				
			||||||
 | 
					                web::post().to(disk_images_controller::upload),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            // API tokens controller
 | 
					            // API tokens controller
 | 
				
			||||||
            .route(
 | 
					            .route(
 | 
				
			||||||
                "/api/token/create",
 | 
					                "/api/token/create",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,6 +38,7 @@ import { LoginRoute } from "./routes/auth/LoginRoute";
 | 
				
			|||||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
 | 
					import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
 | 
				
			||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
 | 
					import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
 | 
				
			||||||
import { BaseLoginPage } from "./widgets/BaseLoginPage";
 | 
					import { BaseLoginPage } from "./widgets/BaseLoginPage";
 | 
				
			||||||
 | 
					import { DiskImagesRoute } from "./routes/DiskImagesRoute";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AuthContext {
 | 
					interface AuthContext {
 | 
				
			||||||
  signedIn: boolean;
 | 
					  signedIn: boolean;
 | 
				
			||||||
@@ -63,6 +64,8 @@ export function App() {
 | 
				
			|||||||
        <Route path="*" element={<BaseAuthenticatedPage />}>
 | 
					        <Route path="*" element={<BaseAuthenticatedPage />}>
 | 
				
			||||||
          <Route path="" element={<HomeRoute />} />
 | 
					          <Route path="" element={<HomeRoute />} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Route path="disk_images" element={<DiskImagesRoute />} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Route path="iso" element={<IsoFilesRoute />} />
 | 
					          <Route path="iso" element={<IsoFilesRoute />} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Route path="vms" element={<VMListRoute />} />
 | 
					          <Route path="vms" element={<VMListRoute />} />
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										31
									
								
								virtweb_frontend/src/api/DiskImageApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								virtweb_frontend/src/api/DiskImageApi.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					import { APIClient } from "./ApiClient";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface DiskImage {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DiskImageApi {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Upload a new disk image file to the server
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async Upload(
 | 
				
			||||||
 | 
					    file: File,
 | 
				
			||||||
 | 
					    progress: (progress: number) => void
 | 
				
			||||||
 | 
					  ): Promise<void> {
 | 
				
			||||||
 | 
					    const fd = new FormData();
 | 
				
			||||||
 | 
					    fd.append("file", file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await APIClient.exec({
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      uri: "/disk_images/upload",
 | 
				
			||||||
 | 
					      formData: fd,
 | 
				
			||||||
 | 
					      upProgress: progress,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get the list of disk images
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async GetList(): Promise<DiskImage[]> {
 | 
				
			||||||
 | 
					    // TODO
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,6 +5,7 @@ export interface ServerConfig {
 | 
				
			|||||||
  local_auth_enabled: boolean;
 | 
					  local_auth_enabled: boolean;
 | 
				
			||||||
  oidc_auth_enabled: boolean;
 | 
					  oidc_auth_enabled: boolean;
 | 
				
			||||||
  iso_mimetypes: string[];
 | 
					  iso_mimetypes: string[];
 | 
				
			||||||
 | 
					  disk_images_mimetypes: string[];
 | 
				
			||||||
  net_mac_prefix: string;
 | 
					  net_mac_prefix: string;
 | 
				
			||||||
  builtin_nwfilter_rules: string[];
 | 
					  builtin_nwfilter_rules: string[];
 | 
				
			||||||
  nwfilter_chains: string[];
 | 
					  nwfilter_chains: string[];
 | 
				
			||||||
@@ -13,6 +14,7 @@ export interface ServerConfig {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface ServerConstraints {
 | 
					export interface ServerConstraints {
 | 
				
			||||||
  iso_max_size: number;
 | 
					  iso_max_size: number;
 | 
				
			||||||
 | 
					  disk_image_max_size: number;
 | 
				
			||||||
  vnc_token_duration: number;
 | 
					  vnc_token_duration: number;
 | 
				
			||||||
  vm_name_size: LenConstraint;
 | 
					  vm_name_size: LenConstraint;
 | 
				
			||||||
  vm_title_size: LenConstraint;
 | 
					  vm_title_size: LenConstraint;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										151
									
								
								virtweb_frontend/src/routes/DiskImagesRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								virtweb_frontend/src/routes/DiskImagesRoute.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					import RefreshIcon from "@mui/icons-material/Refresh";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  IconButton,
 | 
				
			||||||
 | 
					  LinearProgress,
 | 
				
			||||||
 | 
					  Tooltip,
 | 
				
			||||||
 | 
					  Typography,
 | 
				
			||||||
 | 
					} from "@mui/material";
 | 
				
			||||||
 | 
					import { filesize } from "filesize";
 | 
				
			||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { DiskImage, DiskImageApi } from "../api/DiskImageApi";
 | 
				
			||||||
 | 
					import { ServerApi } from "../api/ServerApi";
 | 
				
			||||||
 | 
					import { useAlert } from "../hooks/providers/AlertDialogProvider";
 | 
				
			||||||
 | 
					import { useSnackbar } from "../hooks/providers/SnackbarProvider";
 | 
				
			||||||
 | 
					import { AsyncWidget } from "../widgets/AsyncWidget";
 | 
				
			||||||
 | 
					import { FileInput } from "../widgets/forms/FileInput";
 | 
				
			||||||
 | 
					import { VirtWebPaper } from "../widgets/VirtWebPaper";
 | 
				
			||||||
 | 
					import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function DiskImagesRoute(): React.ReactElement {
 | 
				
			||||||
 | 
					  const [list, setList] = React.useState<DiskImage[] | undefined>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loadKey = React.useRef(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const load = async () => {
 | 
				
			||||||
 | 
					    setList(await DiskImageApi.GetList());
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const reload = () => {
 | 
				
			||||||
 | 
					    loadKey.current += 1;
 | 
				
			||||||
 | 
					    setList(undefined);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <VirtWebRouteContainer label="Disk images">
 | 
				
			||||||
 | 
					      <AsyncWidget
 | 
				
			||||||
 | 
					        loadKey={loadKey.current}
 | 
				
			||||||
 | 
					        errMsg="Failed to load disk images list!"
 | 
				
			||||||
 | 
					        load={load}
 | 
				
			||||||
 | 
					        ready={list !== undefined}
 | 
				
			||||||
 | 
					        build={() => (
 | 
				
			||||||
 | 
					          <VirtWebRouteContainer
 | 
				
			||||||
 | 
					            label="Disk images management"
 | 
				
			||||||
 | 
					            actions={
 | 
				
			||||||
 | 
					              <span>
 | 
				
			||||||
 | 
					                <Tooltip title="Refresh Disk images list">
 | 
				
			||||||
 | 
					                  <IconButton onClick={reload}>
 | 
				
			||||||
 | 
					                    <RefreshIcon />
 | 
				
			||||||
 | 
					                  </IconButton>
 | 
				
			||||||
 | 
					                </Tooltip>
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <UploadDiskImageCard onFileUploaded={reload} />
 | 
				
			||||||
 | 
					            <DiskImageList list={list!} onReload={reload} />
 | 
				
			||||||
 | 
					          </VirtWebRouteContainer>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </VirtWebRouteContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function UploadDiskImageCard(p: {
 | 
				
			||||||
 | 
					  onFileUploaded: () => void;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const alert = useAlert();
 | 
				
			||||||
 | 
					  const snackbar = useSnackbar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [value, setValue] = React.useState<File | null>(null);
 | 
				
			||||||
 | 
					  const [uploadProgress, setUploadProgress] = React.useState<number | null>(
 | 
				
			||||||
 | 
					    null
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleChange = (newValue: File | null) => {
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      newValue &&
 | 
				
			||||||
 | 
					      newValue.size > ServerApi.Config.constraints.disk_image_max_size
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      alert(
 | 
				
			||||||
 | 
					        `The file is too big (max size allowed: ${filesize(
 | 
				
			||||||
 | 
					          ServerApi.Config.constraints.disk_image_max_size
 | 
				
			||||||
 | 
					        )}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      newValue &&
 | 
				
			||||||
 | 
					      !ServerApi.Config.disk_images_mimetypes.includes(newValue.type)
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      alert(`Selected file mimetype is not allowed! (${newValue.type})`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setValue(newValue);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const upload = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      setUploadProgress(0);
 | 
				
			||||||
 | 
					      await DiskImageApi.Upload(value!, setUploadProgress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setValue(null);
 | 
				
			||||||
 | 
					      snackbar("The file was successfully uploaded!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      p.onFileUploaded();
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(e);
 | 
				
			||||||
 | 
					      await alert(`Failed to perform file upload! ${e}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setUploadProgress(null);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (uploadProgress !== null) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <VirtWebPaper label="File upload" noHorizontalMargin>
 | 
				
			||||||
 | 
					        <Typography variant="body1">
 | 
				
			||||||
 | 
					          Upload in progress ({Math.floor(uploadProgress * 100)}%)...
 | 
				
			||||||
 | 
					        </Typography>
 | 
				
			||||||
 | 
					        <LinearProgress variant="determinate" value={uploadProgress * 100} />
 | 
				
			||||||
 | 
					      </VirtWebPaper>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <VirtWebPaper label="Disk image upload" noHorizontalMargin>
 | 
				
			||||||
 | 
					      <div style={{ display: "flex", alignItems: "center" }}>
 | 
				
			||||||
 | 
					        <FileInput
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          onChange={handleChange}
 | 
				
			||||||
 | 
					          style={{ flex: 1 }}
 | 
				
			||||||
 | 
					          slotProps={{
 | 
				
			||||||
 | 
					            htmlInput: {
 | 
				
			||||||
 | 
					              accept: ServerApi.Config.disk_images_mimetypes.join(","),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {value && <Button onClick={upload}>Upload</Button>}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </VirtWebPaper>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function DiskImageList(p: {
 | 
				
			||||||
 | 
					  list: DiskImage[];
 | 
				
			||||||
 | 
					  onReload: () => void;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  return <>todo</>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,7 +3,15 @@ import { RouterLink } from "../widgets/RouterLink";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function NotFoundRoute(): React.ReactElement {
 | 
					export function NotFoundRoute(): React.ReactElement {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div style={{ textAlign: "center" }}>
 | 
					    <div
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        textAlign: "center",
 | 
				
			||||||
 | 
					        flex: 1,
 | 
				
			||||||
 | 
					        justifyContent: "center",
 | 
				
			||||||
 | 
					        display: "flex",
 | 
				
			||||||
 | 
					        flexDirection: "column",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <h1>404 Not found</h1>
 | 
					      <h1>404 Not found</h1>
 | 
				
			||||||
      <p>The page you requested was not found!</p>
 | 
					      <p>The page you requested was not found!</p>
 | 
				
			||||||
      <RouterLink to="/">
 | 
					      <RouterLink to="/">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import {
 | 
				
			|||||||
  mdiApi,
 | 
					  mdiApi,
 | 
				
			||||||
  mdiBoxShadow,
 | 
					  mdiBoxShadow,
 | 
				
			||||||
  mdiDisc,
 | 
					  mdiDisc,
 | 
				
			||||||
 | 
					  mdiHarddisk,
 | 
				
			||||||
  mdiHome,
 | 
					  mdiHome,
 | 
				
			||||||
  mdiInformation,
 | 
					  mdiInformation,
 | 
				
			||||||
  mdiLan,
 | 
					  mdiLan,
 | 
				
			||||||
@@ -66,6 +67,11 @@ export function BaseAuthenticatedPage(): React.ReactElement {
 | 
				
			|||||||
            uri="/nwfilter"
 | 
					            uri="/nwfilter"
 | 
				
			||||||
            icon={<Icon path={mdiSecurityNetwork} size={1} />}
 | 
					            icon={<Icon path={mdiSecurityNetwork} size={1} />}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					          <NavLink
 | 
				
			||||||
 | 
					            label="Disk images"
 | 
				
			||||||
 | 
					            uri="/disk_images"
 | 
				
			||||||
 | 
					            icon={<Icon path={mdiHarddisk} size={1} />}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
          <NavLink
 | 
					          <NavLink
 | 
				
			||||||
            label="ISO files"
 | 
					            label="ISO files"
 | 
				
			||||||
            uri="/iso"
 | 
					            uri="/iso"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,7 +35,7 @@ export function FileInput(
 | 
				
			|||||||
              <InputAdornment position="start">
 | 
					              <InputAdornment position="start">
 | 
				
			||||||
                <AttachFileIcon />
 | 
					                <AttachFileIcon />
 | 
				
			||||||
                  
 | 
					                  
 | 
				
			||||||
                {p.value ? p.value.name : "Insert a file"}
 | 
					                {p.value ? p.value.name : "Select a file"}
 | 
				
			||||||
              </InputAdornment>
 | 
					              </InputAdornment>
 | 
				
			||||||
            </>
 | 
					            </>
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user