Compare commits
1 Commits
a1e94ea399
...
9c7207ea06
Author | SHA1 | Date | |
---|---|---|---|
9c7207ea06 |
2
virtweb_backend/Cargo.lock
generated
2
virtweb_backend/Cargo.lock
generated
@ -2877,7 +2877,6 @@ version = "8.7.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
|
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mime_guess",
|
|
||||||
"sha2",
|
"sha2",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
@ -3746,6 +3745,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"light-openid",
|
"light-openid",
|
||||||
"log",
|
"log",
|
||||||
|
"mime_guess",
|
||||||
"nix",
|
"nix",
|
||||||
"num",
|
"num",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
|
@ -40,7 +40,8 @@ tokio = { version = "1.45.0", features = ["rt", "time", "macros"] }
|
|||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||||
num = "0.4.3"
|
num = "0.4.3"
|
||||||
rust-embed = { version = "8.7.2", features = ["mime-guess"] }
|
rust-embed = { version = "8.7.2" }
|
||||||
|
mime_guess = "2.0.5"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
nix = { version = "0.30.1", features = ["net"] }
|
nix = { version = "0.30.1", features = ["net"] }
|
||||||
basic-jwt = "0.3.0"
|
basic-jwt = "0.3.0"
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 10.0, SVG Export Plug-In . SVG Version: 3.0.0 Build 77) -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [
|
|
||||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
|
||||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
|
||||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
|
||||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
|
||||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
|
||||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
|
||||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
|
||||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
|
||||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
|
||||||
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
|
|
||||||
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
|
|
||||||
]>
|
|
||||||
<svg
|
|
||||||
xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;" i:viewOrigin="262 450" i:rulerOrigin="0 0" i:pageBounds="0 792 612 0"
|
|
||||||
xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
|
||||||
width="87.041" height="108.445" viewBox="0 0 87.041 108.445" overflow="visible" enable-background="new 0 0 87.041 108.445"
|
|
||||||
xml:space="preserve">
|
|
||||||
<metadata>
|
|
||||||
<variableSets xmlns="&ns_vars;">
|
|
||||||
<variableSet varSetName="binding1" locked="none">
|
|
||||||
<variables></variables>
|
|
||||||
<v:sampleDataSets xmlns="&ns_custom;" xmlns:v="&ns_vars;"></v:sampleDataSets>
|
|
||||||
</variableSet>
|
|
||||||
</variableSets>
|
|
||||||
<sfw xmlns="&ns_sfw;">
|
|
||||||
<slices></slices>
|
|
||||||
<sliceSourceBounds y="341.555" x="262" width="87.041" height="108.445" bottomLeftOrigin="true"></sliceSourceBounds>
|
|
||||||
</sfw>
|
|
||||||
</metadata>
|
|
||||||
<g id="Layer_1" i:layer="yes" i:dimmedPercent="50" i:rgbTrio="#4F008000FFFF">
|
|
||||||
<g>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M51.986,57.297c-1.797,0.025,0.34,0.926,2.686,1.287
|
|
||||||
c0.648-0.506,1.236-1.018,1.76-1.516C54.971,57.426,53.484,57.434,51.986,57.297"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M61.631,54.893c1.07-1.477,1.85-3.094,2.125-4.766c-0.24,1.192-0.887,2.221-1.496,3.307
|
|
||||||
c-3.359,2.115-0.316-1.256-0.002-2.537C58.646,55.443,61.762,53.623,61.631,54.893"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M65.191,45.629c0.217-3.236-0.637-2.213-0.924-0.978
|
|
||||||
C64.602,44.825,64.867,46.932,65.191,45.629"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M45.172,1.399c0.959,0.172,2.072,0.304,1.916,0.533
|
|
||||||
C48.137,1.702,48.375,1.49,45.172,1.399"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M47.088,1.932l-0.678,0.14l0.631-0.056L47.088,1.932"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M76.992,46.856c0.107,2.906-0.85,4.316-1.713,6.812l-1.553,0.776
|
|
||||||
c-1.271,2.468,0.123,1.567-0.787,3.53c-1.984,1.764-6.021,5.52-7.313,5.863c-0.943-0.021,0.639-1.113,0.846-1.541
|
|
||||||
c-2.656,1.824-2.131,2.738-6.193,3.846l-0.119-0.264c-10.018,4.713-23.934-4.627-23.751-17.371
|
|
||||||
c-0.107,0.809-0.304,0.607-0.526,0.934c-0.517-6.557,3.028-13.143,9.007-15.832c5.848-2.895,12.704-1.707,16.893,2.197
|
|
||||||
c-2.301-3.014-6.881-6.209-12.309-5.91c-5.317,0.084-10.291,3.463-11.951,7.131c-2.724,1.715-3.04,6.611-4.227,7.507
|
|
||||||
C31.699,56.271,36.3,61.342,44.083,67.307c1.225,0.826,0.345,0.951,0.511,1.58c-2.586-1.211-4.954-3.039-6.901-5.277
|
|
||||||
c1.033,1.512,2.148,2.982,3.589,4.137c-2.438-0.826-5.695-5.908-6.646-6.115c4.203,7.525,17.052,13.197,23.78,10.383
|
|
||||||
c-3.113,0.115-7.068,0.064-10.566-1.229c-1.469-0.756-3.467-2.322-3.11-2.615c9.182,3.43,18.667,2.598,26.612-3.771
|
|
||||||
c2.021-1.574,4.229-4.252,4.867-4.289c-0.961,1.445,0.164,0.695-0.574,1.971c2.014-3.248-0.875-1.322,2.082-5.609l1.092,1.504
|
|
||||||
c-0.406-2.696,3.348-5.97,2.967-10.234c0.861-1.304,0.961,1.403,0.047,4.403c1.268-3.328,0.334-3.863,0.66-6.609
|
|
||||||
c0.352,0.923,0.814,1.904,1.051,2.878c-0.826-3.216,0.848-5.416,1.262-7.285c-0.408-0.181-1.275,1.422-1.473-2.377
|
|
||||||
c0.029-1.65,0.459-0.865,0.625-1.271c-0.324-0.186-1.174-1.451-1.691-3.877c0.375-0.57,1.002,1.478,1.512,1.562
|
|
||||||
c-0.328-1.929-0.893-3.4-0.916-4.88c-1.49-3.114-0.527,0.415-1.736-1.337c-1.586-4.947,1.316-1.148,1.512-3.396
|
|
||||||
c2.404,3.483,3.775,8.881,4.404,11.117c-0.48-2.726-1.256-5.367-2.203-7.922c0.73,0.307-1.176-5.609,0.949-1.691
|
|
||||||
c-2.27-8.352-9.715-16.156-16.564-19.818c0.838,0.767,1.896,1.73,1.516,1.881c-3.406-2.028-2.807-2.186-3.295-3.043
|
|
||||||
c-2.775-1.129-2.957,0.091-4.795,0.002c-5.23-2.774-6.238-2.479-11.051-4.217l0.219,1.023c-3.465-1.154-4.037,0.438-7.782,0.004
|
|
||||||
c-0.228-0.178,1.2-0.644,2.375-0.815c-3.35,0.442-3.193-0.66-6.471,0.122c0.808-0.567,1.662-0.942,2.524-1.424
|
|
||||||
c-2.732,0.166-6.522,1.59-5.352,0.295c-4.456,1.988-12.37,4.779-16.811,8.943l-0.14-0.933c-2.035,2.443-8.874,7.296-9.419,10.46
|
|
||||||
l-0.544,0.127c-1.059,1.793-1.744,3.825-2.584,5.67c-1.385,2.36-2.03,0.908-1.833,1.278c-2.724,5.523-4.077,10.164-5.246,13.97
|
|
||||||
c0.833,1.245,0.02,7.495,0.335,12.497c-1.368,24.704,17.338,48.69,37.785,54.228c2.997,1.072,7.454,1.031,11.245,1.141
|
|
||||||
c-4.473-1.279-5.051-0.678-9.408-2.197c-3.143-1.48-3.832-3.17-6.058-5.102l0.881,1.557c-4.366-1.545-2.539-1.912-6.091-3.037
|
|
||||||
l0.941-1.229c-1.415-0.107-3.748-2.385-4.386-3.646l-1.548,0.061c-1.86-2.295-2.851-3.949-2.779-5.23l-0.5,0.891
|
|
||||||
c-0.567-0.973-6.843-8.607-3.587-6.83c-0.605-0.553-1.409-0.9-2.281-2.484l0.663-0.758c-1.567-2.016-2.884-4.6-2.784-5.461
|
|
||||||
c0.836,1.129,1.416,1.34,1.99,1.533c-3.957-9.818-4.179-0.541-7.176-9.994l0.634-0.051c-0.486-0.732-0.781-1.527-1.172-2.307
|
|
||||||
l0.276-2.75C4.667,58.121,6.719,47.409,7.13,41.534c0.285-2.389,2.378-4.932,3.97-8.92l-0.97-0.167
|
|
||||||
c1.854-3.234,10.586-12.988,14.63-12.486c1.959-2.461-0.389-0.009-0.772-0.629c4.303-4.453,5.656-3.146,8.56-3.947
|
|
||||||
c3.132-1.859-2.688,0.725-1.203-0.709c5.414-1.383,3.837-3.144,10.9-3.846c0.745,0.424-1.729,0.655-2.35,1.205
|
|
||||||
c4.511-2.207,14.275-1.705,20.617,1.225c7.359,3.439,15.627,13.605,15.953,23.17l0.371,0.1
|
|
||||||
c-0.188,3.802,0.582,8.199-0.752,12.238L76.992,46.856"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M32.372,59.764l-0.252,1.26c1.181,1.604,2.118,3.342,3.626,4.596
|
|
||||||
C34.661,63.502,33.855,62.627,32.372,59.764"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M35.164,59.654c-0.625-0.691-0.995-1.523-1.409-2.352
|
|
||||||
c0.396,1.457,1.207,2.709,1.962,3.982L35.164,59.654"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M84.568,48.916l-0.264,0.662c-0.484,3.438-1.529,6.84-3.131,9.994
|
|
||||||
C82.943,56.244,84.088,52.604,84.568,48.916"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M45.527,0.537C46.742,0.092,48.514,0.293,49.803,0c-1.68,0.141-3.352,0.225-5.003,0.438
|
|
||||||
L45.527,0.537"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M2.872,23.219c0.28,2.592-1.95,3.598,0.494,1.889
|
|
||||||
C4.676,22.157,2.854,24.293,2.872,23.219"/>
|
|
||||||
<path i:knockout="Off" fill="#A80030" d="M0,35.215c0.563-1.728,0.665-2.766,0.88-3.766C-0.676,33.438,0.164,33.862,0,35.215"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 194 KiB |
@ -1,8 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100">
|
|
||||||
<circle fill="#f47421" cy="50" cx="50" r="45"/>
|
|
||||||
<circle fill="none" stroke="#ffffff" stroke-width="8.55" cx="50" cy="50" r="21.825"/>
|
|
||||||
<g id="friend"><circle fill="#f47421" cx="19.4" cy="50" r="8.4376"/>
|
|
||||||
<path stroke="#f47421" stroke-width="3.2378" d="M67,50H77"/>
|
|
||||||
<circle fill="#ffffff" cx="19.4" cy="50" r="6.00745"/></g>
|
|
||||||
<use xlink:href="#friend" transform="rotate(120,50,50)"/>
|
|
||||||
<use xlink:href="#friend" transform="rotate(240,50,50)"/></svg>
|
|
Before Width: | Height: | Size: 550 B |
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="88" width="88" xmlns:v="https://vecta.io/nano"><path d="M0 12.402l35.687-4.86.016 34.423-35.67.203zm35.67 33.529l.028 34.453L.028 75.48.026 45.7zm4.326-39.025L87.314 0v41.527l-47.318.376zm47.329 39.349l-.011 41.34-47.318-6.678-.066-34.739z" fill="#00adef"/></svg>
|
|
Before Width: | Height: | Size: 311 B |
@ -1,47 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "Ubuntu releases",
|
|
||||||
"url": "https://releases.ubuntu.com",
|
|
||||||
"image": "/assets/img/ubuntu.svg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Old ubuntu releases",
|
|
||||||
"url": "https://old-releases.ubuntu.com/releases/",
|
|
||||||
"image": "/assets/img/ubuntu.svg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Current Debian releases (amd64)",
|
|
||||||
"url": "https://cdimage.debian.org/mirror/cdimage/release/current/amd64/iso-dvd/",
|
|
||||||
"image": "/assets/img/debian.svg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Old Debian releases",
|
|
||||||
"url": "https://cdimage.debian.org/mirror/cdimage/archive/",
|
|
||||||
"image": "/assets/img/debian.svg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Latest stable Virtio driver",
|
|
||||||
"url": "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso",
|
|
||||||
"image": "/assets/img/kvm.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Windows server 2025",
|
|
||||||
"url": "https://www.microsoft.com/en-us/evalcenter/download-windows-server-2025",
|
|
||||||
"image": "/assets/img/windows.svg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Windows server 2022",
|
|
||||||
"url": "https://www.microsoft.com/en-us/evalcenter/download-windows-server-2022",
|
|
||||||
"image": "/assets/img/windows.svg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Windows 11",
|
|
||||||
"url": "https://www.microsoft.com/en-us/software-download/windows11",
|
|
||||||
"image": "/assets/img/windows.svg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Windows 11 Iot Enterprise LTSC 2024",
|
|
||||||
"url": "https://www.microsoft.com/en-us/evalcenter/download-windows-11-iot-enterprise-ltsc-eval",
|
|
||||||
"image": "/assets/img/windows.svg"
|
|
||||||
}
|
|
||||||
]
|
|
@ -3,27 +3,6 @@ pub use serve_static_debug::{root_index, serve_static_content};
|
|||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
pub use serve_static_release::{root_index, serve_static_content};
|
pub use serve_static_release::{root_index, serve_static_content};
|
||||||
|
|
||||||
/// Static API assets hosting
|
|
||||||
pub mod serve_assets {
|
|
||||||
use actix_web::{HttpResponse, web};
|
|
||||||
use rust_embed::RustEmbed;
|
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
|
||||||
#[folder = "assets/"]
|
|
||||||
struct Asset;
|
|
||||||
|
|
||||||
/// Serve API assets
|
|
||||||
pub async fn serve_api_assets(file: web::Path<String>) -> HttpResponse {
|
|
||||||
match Asset::get(&file) {
|
|
||||||
None => HttpResponse::NotFound().body("File not found"),
|
|
||||||
Some(asset) => HttpResponse::Ok()
|
|
||||||
.content_type(asset.metadata.mimetype())
|
|
||||||
.body(asset.data),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Web asset hosting placeholder in debug mode
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
mod serve_static_debug {
|
mod serve_static_debug {
|
||||||
use actix_web::{HttpResponse, Responder};
|
use actix_web::{HttpResponse, Responder};
|
||||||
@ -37,7 +16,6 @@ mod serve_static_debug {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Web asset hosting in release mode
|
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
mod serve_static_release {
|
mod serve_static_release {
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{HttpResponse, Responder, web};
|
||||||
@ -45,12 +23,12 @@ mod serve_static_release {
|
|||||||
|
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "static/"]
|
#[folder = "static/"]
|
||||||
struct WebAsset;
|
struct Asset;
|
||||||
|
|
||||||
fn handle_embedded_file(path: &str, can_fallback: bool) -> HttpResponse {
|
fn handle_embedded_file(path: &str, can_fallback: bool) -> HttpResponse {
|
||||||
match (WebAsset::get(path), can_fallback) {
|
match (Asset::get(path), can_fallback) {
|
||||||
(Some(content), _) => HttpResponse::Ok()
|
(Some(content), _) => HttpResponse::Ok()
|
||||||
.content_type(content.metadata.mimetype())
|
.content_type(mime_guess::from_path(path).first_or_octet_stream().as_ref())
|
||||||
.body(content.data.into_owned()),
|
.body(content.data.into_owned()),
|
||||||
(None, false) => HttpResponse::NotFound().body("404 Not Found"),
|
(None, false) => HttpResponse::NotFound().body("404 Not Found"),
|
||||||
(None, true) => handle_embedded_file("index.html", false),
|
(None, true) => handle_embedded_file("index.html", false),
|
||||||
|
@ -4,7 +4,7 @@ use crate::libvirt_lib_structures::XMLUuid;
|
|||||||
use crate::libvirt_lib_structures::domain::*;
|
use crate::libvirt_lib_structures::domain::*;
|
||||||
use crate::libvirt_rest_structures::LibVirtStructError;
|
use crate::libvirt_rest_structures::LibVirtStructError;
|
||||||
use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
|
use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
|
||||||
use crate::utils::file_disks_utils::{DiskFormat, FileDisk};
|
use crate::utils::disks_utils::Disk;
|
||||||
use crate::utils::files_utils;
|
use crate::utils::files_utils;
|
||||||
use crate::utils::files_utils::convert_size_unit_to_mb;
|
use crate::utils::files_utils::convert_size_unit_to_mb;
|
||||||
use lazy_regex::regex;
|
use lazy_regex::regex;
|
||||||
@ -78,7 +78,7 @@ pub struct VMInfo {
|
|||||||
/// Attach ISO file(s)
|
/// Attach ISO file(s)
|
||||||
pub iso_files: Vec<String>,
|
pub iso_files: Vec<String>,
|
||||||
/// Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest
|
/// Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest
|
||||||
pub disks: Vec<FileDisk>,
|
pub disks: Vec<Disk>,
|
||||||
/// Network cards
|
/// Network cards
|
||||||
pub networks: Vec<Network>,
|
pub networks: Vec<Network>,
|
||||||
/// Add a TPM v2.0 module
|
/// Add a TPM v2.0 module
|
||||||
@ -129,7 +129,6 @@ impl VMInfo {
|
|||||||
|
|
||||||
let mut disks = vec![];
|
let mut disks = vec![];
|
||||||
|
|
||||||
// Add ISO files
|
|
||||||
for iso_file in &self.iso_files {
|
for iso_file in &self.iso_files {
|
||||||
if !files_utils::check_file_name(iso_file) {
|
if !files_utils::check_file_name(iso_file) {
|
||||||
return Err(StructureExtraction("ISO filename is invalid!").into());
|
return Err(StructureExtraction("ISO filename is invalid!").into());
|
||||||
@ -268,10 +267,7 @@ impl VMInfo {
|
|||||||
device: "disk".to_string(),
|
device: "disk".to_string(),
|
||||||
driver: DiskDriverXML {
|
driver: DiskDriverXML {
|
||||||
name: "qemu".to_string(),
|
name: "qemu".to_string(),
|
||||||
r#type: match disk.format {
|
r#type: "raw".to_string(),
|
||||||
DiskFormat::Raw { .. } => "raw".to_string(),
|
|
||||||
DiskFormat::QCow2 => "qcow2".to_string(),
|
|
||||||
},
|
|
||||||
cache: "none".to_string(),
|
cache: "none".to_string(),
|
||||||
},
|
},
|
||||||
source: DiskSourceXML {
|
source: DiskSourceXML {
|
||||||
@ -433,7 +429,7 @@ impl VMInfo {
|
|||||||
.disks
|
.disks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|d| d.device == "disk")
|
.filter(|d| d.device == "disk")
|
||||||
.map(|d| FileDisk::load_from_file(&d.source.file).unwrap())
|
.map(|d| Disk::load_from_file(&d.source.file).unwrap())
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
||||||
networks: domain
|
networks: domain
|
||||||
|
@ -337,11 +337,6 @@ async fn main() -> std::io::Result<()> {
|
|||||||
web::delete().to(api_tokens_controller::delete),
|
web::delete().to(api_tokens_controller::delete),
|
||||||
)
|
)
|
||||||
// Static assets
|
// Static assets
|
||||||
.route(
|
|
||||||
"/api/assets/{tail:.*}",
|
|
||||||
web::get().to(static_controller::serve_assets::serve_api_assets),
|
|
||||||
)
|
|
||||||
// Static web frontend
|
|
||||||
.route("/", web::get().to(static_controller::root_index))
|
.route("/", web::get().to(static_controller::root_index))
|
||||||
.route(
|
.route(
|
||||||
"/{tail:.*}",
|
"/{tail:.*}",
|
||||||
|
133
virtweb_backend/src/utils/disks_utils.rs
Normal file
133
virtweb_backend/src/utils/disks_utils.rs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants;
|
||||||
|
use crate::libvirt_lib_structures::XMLUuid;
|
||||||
|
use crate::utils::files_utils;
|
||||||
|
use lazy_regex::regex;
|
||||||
|
use std::os::linux::fs::MetadataExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum DisksError {
|
||||||
|
#[error("DiskParseError: {0}")]
|
||||||
|
Parse(&'static str),
|
||||||
|
#[error("DiskConfigError: {0}")]
|
||||||
|
Config(&'static str),
|
||||||
|
#[error("DiskCreateError")]
|
||||||
|
Create,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type of disk allocation
|
||||||
|
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum DiskAllocType {
|
||||||
|
Fixed,
|
||||||
|
Sparse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Disk {
|
||||||
|
/// Disk size, in megabytes
|
||||||
|
pub size: usize,
|
||||||
|
/// Disk name
|
||||||
|
pub name: String,
|
||||||
|
pub alloc_type: DiskAllocType,
|
||||||
|
/// Set this variable to true to delete the disk
|
||||||
|
pub delete: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Disk {
|
||||||
|
pub fn load_from_file(path: &str) -> anyhow::Result<Self> {
|
||||||
|
let file = Path::new(path);
|
||||||
|
|
||||||
|
if !file.is_file() {
|
||||||
|
return Err(DisksError::Parse("Path is not a file!").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
|
||||||
|
// Approximate estimation
|
||||||
|
let is_sparse = metadata.len() / 512 >= metadata.st_blocks();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
size: metadata.len() as usize / (1000 * 1000),
|
||||||
|
name: path.rsplit_once('/').unwrap().1.to_string(),
|
||||||
|
alloc_type: match is_sparse {
|
||||||
|
true => DiskAllocType::Sparse,
|
||||||
|
false => DiskAllocType::Fixed,
|
||||||
|
},
|
||||||
|
delete: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_config(&self) -> anyhow::Result<()> {
|
||||||
|
if constants::DISK_NAME_MIN_LEN > self.name.len()
|
||||||
|
|| constants::DISK_NAME_MAX_LEN < self.name.len()
|
||||||
|
{
|
||||||
|
return Err(DisksError::Config("Disk name length is invalid").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
|
||||||
|
return Err(DisksError::Config("Disk name contains invalid characters!").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.size < constants::DISK_SIZE_MIN || self.size > constants::DISK_SIZE_MAX {
|
||||||
|
return Err(DisksError::Config("Disk size is invalid!").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get disk path
|
||||||
|
pub fn disk_path(&self, id: XMLUuid) -> PathBuf {
|
||||||
|
let domain_dir = AppConfig::get().vm_storage_path(id);
|
||||||
|
domain_dir.join(&self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply disk configuration
|
||||||
|
pub fn apply_config(&self, id: XMLUuid) -> anyhow::Result<()> {
|
||||||
|
self.check_config()?;
|
||||||
|
|
||||||
|
let file = self.disk_path(id);
|
||||||
|
files_utils::create_directory_if_missing(file.parent().unwrap())?;
|
||||||
|
|
||||||
|
// Delete file if requested
|
||||||
|
if self.delete {
|
||||||
|
if !file.exists() {
|
||||||
|
log::debug!("File {file:?} does not exists, so it was not deleted");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Deleting {file:?}");
|
||||||
|
std::fs::remove_file(file)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.exists() {
|
||||||
|
log::debug!("File {file:?} does not exists, so it was not touched");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = Command::new("/usr/bin/dd");
|
||||||
|
cmd.arg("if=/dev/zero")
|
||||||
|
.arg(format!("of={}", file.to_string_lossy()))
|
||||||
|
.arg("bs=1M");
|
||||||
|
|
||||||
|
match self.alloc_type {
|
||||||
|
DiskAllocType::Fixed => cmd.arg(format!("count={}", self.size)),
|
||||||
|
DiskAllocType::Sparse => cmd.arg(format!("seek={}", self.size)).arg("count=0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = cmd.output()?;
|
||||||
|
|
||||||
|
if !res.status.success() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to create disk! stderr={} stdout={}",
|
||||||
|
String::from_utf8_lossy(&res.stderr),
|
||||||
|
String::from_utf8_lossy(&res.stdout)
|
||||||
|
);
|
||||||
|
return Err(DisksError::Create.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,207 +0,0 @@
|
|||||||
use crate::app_config::AppConfig;
|
|
||||||
use crate::constants;
|
|
||||||
use crate::libvirt_lib_structures::XMLUuid;
|
|
||||||
use crate::utils::files_utils;
|
|
||||||
use lazy_regex::regex;
|
|
||||||
use std::os::linux::fs::MetadataExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
enum DisksError {
|
|
||||||
#[error("DiskParseError: {0}")]
|
|
||||||
Parse(&'static str),
|
|
||||||
#[error("DiskConfigError: {0}")]
|
|
||||||
Config(&'static str),
|
|
||||||
#[error("DiskCreateError")]
|
|
||||||
Create,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type of disk allocation
|
|
||||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum DiskAllocType {
|
|
||||||
Fixed,
|
|
||||||
Sparse,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Disk allocation type
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
#[serde(tag = "format")]
|
|
||||||
pub enum DiskFormat {
|
|
||||||
Raw {
|
|
||||||
/// Type of disk allocation
|
|
||||||
alloc_type: DiskAllocType,
|
|
||||||
},
|
|
||||||
QCow2,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct FileDisk {
|
|
||||||
/// Disk name
|
|
||||||
pub name: String,
|
|
||||||
/// Disk size, in megabytes
|
|
||||||
pub size: usize,
|
|
||||||
/// Disk format
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub format: DiskFormat,
|
|
||||||
/// Set this variable to true to delete the disk
|
|
||||||
pub delete: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileDisk {
|
|
||||||
pub fn load_from_file(path: &str) -> anyhow::Result<Self> {
|
|
||||||
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}!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
format,
|
|
||||||
delete: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_config(&self) -> anyhow::Result<()> {
|
|
||||||
if constants::DISK_NAME_MIN_LEN > self.name.len()
|
|
||||||
|| constants::DISK_NAME_MAX_LEN < self.name.len()
|
|
||||||
{
|
|
||||||
return Err(DisksError::Config("Disk name length is invalid").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
|
|
||||||
return Err(DisksError::Config("Disk name contains invalid characters!").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check disk size
|
|
||||||
if !(constants::DISK_SIZE_MIN..=constants::DISK_SIZE_MAX).contains(&self.size) {
|
|
||||||
return Err(DisksError::Config("Disk size is invalid!").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get disk path
|
|
||||||
pub fn disk_path(&self, id: XMLUuid) -> PathBuf {
|
|
||||||
let domain_dir = AppConfig::get().vm_storage_path(id);
|
|
||||||
let file_name = match self.format {
|
|
||||||
DiskFormat::Raw { .. } => self.name.to_string(),
|
|
||||||
DiskFormat::QCow2 => format!("{}.qcow2", self.name),
|
|
||||||
};
|
|
||||||
domain_dir.join(&file_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply disk configuration
|
|
||||||
pub fn apply_config(&self, id: XMLUuid) -> anyhow::Result<()> {
|
|
||||||
self.check_config()?;
|
|
||||||
|
|
||||||
let file = self.disk_path(id);
|
|
||||||
files_utils::create_directory_if_missing(file.parent().unwrap())?;
|
|
||||||
|
|
||||||
// Delete file if requested
|
|
||||||
if self.delete {
|
|
||||||
if !file.exists() {
|
|
||||||
log::debug!("File {file:?} does not exists, so it was not deleted");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Deleting {file:?}");
|
|
||||||
std::fs::remove_file(file)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.exists() {
|
|
||||||
log::debug!("File {file:?} does not exists, so it was not touched");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare command to create file
|
|
||||||
let res = match self.format {
|
|
||||||
DiskFormat::Raw { alloc_type } => {
|
|
||||||
let mut cmd = Command::new("/usr/bin/dd");
|
|
||||||
cmd.arg("if=/dev/zero")
|
|
||||||
.arg(format!("of={}", file.to_string_lossy()))
|
|
||||||
.arg("bs=1M");
|
|
||||||
|
|
||||||
match alloc_type {
|
|
||||||
DiskAllocType::Fixed => cmd.arg(format!("count={}", self.size)),
|
|
||||||
DiskAllocType::Sparse => cmd.arg(format!("seek={}", self.size)).arg("count=0"),
|
|
||||||
};
|
|
||||||
|
|
||||||
cmd.output()?
|
|
||||||
}
|
|
||||||
|
|
||||||
DiskFormat::QCow2 => {
|
|
||||||
let mut cmd = Command::new("/usr/bin/qemu-img");
|
|
||||||
cmd.arg("create")
|
|
||||||
.arg("-f")
|
|
||||||
.arg("qcow2")
|
|
||||||
.arg(file)
|
|
||||||
.arg(format!("{}M", self.size));
|
|
||||||
|
|
||||||
cmd.output()?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute Linux command
|
|
||||||
if !res.status.success() {
|
|
||||||
log::error!(
|
|
||||||
"Failed to create disk! stderr={} stdout={}",
|
|
||||||
String::from_utf8_lossy(&res.stderr),
|
|
||||||
String::from_utf8_lossy(&res.stdout)
|
|
||||||
);
|
|
||||||
return Err(DisksError::Create.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct QCowInfoOutput {
|
|
||||||
#[serde(rename = "virtual-size")]
|
|
||||||
virtual_size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get QCow2 virtual size
|
|
||||||
fn qcow_virt_size(path: &str) -> anyhow::Result<usize> {
|
|
||||||
// Run qemu-img
|
|
||||||
let mut cmd = Command::new("qemu-img");
|
|
||||||
cmd.args(["info", path, "--output", "json", "--force-share"]);
|
|
||||||
let output = cmd.output()?;
|
|
||||||
if !output.status.success() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"qemu-img info failed, status: {}, stderr: {}",
|
|
||||||
output.status,
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let res_json = String::from_utf8(output.stdout)?;
|
|
||||||
|
|
||||||
// Decode JSON
|
|
||||||
let decoded: QCowInfoOutput = serde_json::from_str(&res_json)?;
|
|
||||||
Ok(decoded.virtual_size)
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
pub mod file_disks_utils;
|
pub mod disks_utils;
|
||||||
pub mod files_utils;
|
pub mod files_utils;
|
||||||
pub mod net_utils;
|
pub mod net_utils;
|
||||||
pub mod rand_utils;
|
pub mod rand_utils;
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
## References
|
|
||||||
|
|
||||||
### LibVirt XML documentation
|
|
||||||
* Online: https://libvirt.org/format.html
|
|
||||||
|
|
||||||
* Offline with Ubuntu:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install libvirt-doc
|
|
||||||
firefox /usr/share/doc/libvirt-doc/html/index.html
|
|
||||||
```
|
|
@ -5,15 +5,6 @@ export interface IsoFile {
|
|||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ISO catalog entries
|
|
||||||
*/
|
|
||||||
export interface ISOCatalogEntry {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
image: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IsoFilesApi {
|
export class IsoFilesApi {
|
||||||
/**
|
/**
|
||||||
* Upload a new ISO file to the server
|
* Upload a new ISO file to the server
|
||||||
@ -83,23 +74,4 @@ export class IsoFilesApi {
|
|||||||
uri: `/iso/${file.filename}`,
|
uri: `/iso/${file.filename}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get iso catalog
|
|
||||||
*/
|
|
||||||
static async Catalog(): Promise<ISOCatalogEntry[]> {
|
|
||||||
return (
|
|
||||||
await APIClient.exec({
|
|
||||||
method: "GET",
|
|
||||||
uri: "/assets/iso_catalog.json",
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get catalog image URL
|
|
||||||
*/
|
|
||||||
static CatalogImageURL(entry: ISOCatalogEntry): string {
|
|
||||||
return APIClient.backendURL() + entry.image;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemAvatar,
|
|
||||||
ListItemButton,
|
|
||||||
ListItemText,
|
|
||||||
} from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
import { ISOCatalogEntry, IsoFilesApi } from "../api/IsoFilesApi";
|
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
|
||||||
|
|
||||||
export function IsoCatalogDialog(p: {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}): React.ReactElement {
|
|
||||||
const [catalog, setCatalog] = React.useState<ISOCatalogEntry[] | undefined>();
|
|
||||||
|
|
||||||
const load = async () => {
|
|
||||||
setCatalog(await IsoFilesApi.Catalog());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={p.open} onClose={p.onClose}>
|
|
||||||
<DialogTitle>ISO catalog</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<AsyncWidget
|
|
||||||
loadKey={1}
|
|
||||||
load={load}
|
|
||||||
errMsg="Failed to load catalog"
|
|
||||||
build={() => <IsoCatalogDialogInner catalog={catalog!} />}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button autoFocus onClick={p.onClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IsoCatalogDialogInner(p: {
|
|
||||||
catalog: ISOCatalogEntry[];
|
|
||||||
}): React.ReactElement {
|
|
||||||
return (
|
|
||||||
<List dense>
|
|
||||||
{p.catalog.map((entry) => (
|
|
||||||
<a
|
|
||||||
key={entry.name}
|
|
||||||
href={entry.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
style={{ color: "inherit", textDecoration: "none" }}
|
|
||||||
>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<img
|
|
||||||
src={IsoFilesApi.CatalogImageURL(entry)}
|
|
||||||
style={{ width: "2em" }}
|
|
||||||
/>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary={entry.name} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +1,5 @@
|
|||||||
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 MenuBookIcon from "@mui/icons-material/MenuBook";
|
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
@ -17,7 +15,6 @@ import { filesize } from "filesize";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { IsoFile, IsoFilesApi } from "../api/IsoFilesApi";
|
import { IsoFile, IsoFilesApi } from "../api/IsoFilesApi";
|
||||||
import { ServerApi } from "../api/ServerApi";
|
import { ServerApi } from "../api/ServerApi";
|
||||||
import { IsoCatalogDialog } from "../dialogs/IsoCatalogDialog";
|
|
||||||
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";
|
||||||
@ -30,7 +27,6 @@ import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
|||||||
|
|
||||||
export function IsoFilesRoute(): React.ReactElement {
|
export function IsoFilesRoute(): React.ReactElement {
|
||||||
const [list, setList] = React.useState<IsoFile[] | undefined>();
|
const [list, setList] = React.useState<IsoFile[] | undefined>();
|
||||||
const [isoCatalog, setIsoCatalog] = React.useState(false);
|
|
||||||
|
|
||||||
const loadKey = React.useRef(1);
|
const loadKey = React.useRef(1);
|
||||||
|
|
||||||
@ -44,41 +40,19 @@ export function IsoFilesRoute(): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<AsyncWidget
|
<AsyncWidget
|
||||||
loadKey={loadKey.current}
|
loadKey={loadKey.current}
|
||||||
errMsg="Failed to load ISO files list!"
|
errMsg="Failed to load ISO files list!"
|
||||||
load={load}
|
load={load}
|
||||||
ready={list !== undefined}
|
ready={list !== undefined}
|
||||||
build={() => (
|
build={() => (
|
||||||
<VirtWebRouteContainer
|
<VirtWebRouteContainer label="ISO files management">
|
||||||
label="ISO files management"
|
|
||||||
actions={
|
|
||||||
<span>
|
|
||||||
<Tooltip title="Open the ISO catalog">
|
|
||||||
<IconButton onClick={() => { setIsoCatalog(true); }}>
|
|
||||||
<MenuBookIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Refresh ISO list">
|
|
||||||
<IconButton onClick={reload}>
|
|
||||||
<RefreshIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<UploadIsoFileCard onFileUploaded={reload} />
|
<UploadIsoFileCard onFileUploaded={reload} />
|
||||||
<UploadIsoFileFromUrlCard onFileUploaded={reload} />
|
<UploadIsoFileFromUrlCard onFileUploaded={reload} />
|
||||||
<IsoFilesList list={list!} onReload={reload} />
|
<IsoFilesList list={list!} onReload={reload} />
|
||||||
</VirtWebRouteContainer>
|
</VirtWebRouteContainer>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<IsoCatalogDialog
|
|
||||||
open={isoCatalog}
|
|
||||||
onClose={() => { setIsoCatalog(false); }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +104,7 @@ function UploadIsoFileCard(p: {
|
|||||||
|
|
||||||
if (uploadProgress !== null) {
|
if (uploadProgress !== null) {
|
||||||
return (
|
return (
|
||||||
<VirtWebPaper label="File upload" noHorizontalMargin>
|
<VirtWebPaper label="File upload">
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
Upload in progress ({Math.floor(uploadProgress * 100)}%)...
|
Upload in progress ({Math.floor(uploadProgress * 100)}%)...
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -140,7 +114,7 @@ function UploadIsoFileCard(p: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtWebPaper label="File upload" noHorizontalMargin>
|
<VirtWebPaper label="File upload">
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<FileInput
|
<FileInput
|
||||||
value={value}
|
value={value}
|
||||||
@ -188,7 +162,7 @@ function UploadIsoFileFromUrlCard(p: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtWebPaper label="File upload from URL" noHorizontalMargin>
|
<VirtWebPaper label="File upload from URL">
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="URL"
|
label="URL"
|
||||||
@ -305,6 +279,7 @@ function IsoFilesList(p: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<VirtWebPaper label="Files list">
|
||||||
{/* Download notification */}
|
{/* Download notification */}
|
||||||
{dlProgress !== undefined && (
|
{dlProgress !== undefined && (
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
@ -328,8 +303,14 @@ function IsoFilesList(p: {
|
|||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{/* ISO files list table */}
|
|
||||||
<DataGrid getRowId={(c) => c.filename} rows={p.list} columns={columns} />
|
{/* Files list table */}
|
||||||
|
<DataGrid
|
||||||
|
getRowId={(c) => c.filename}
|
||||||
|
rows={p.list}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</VirtWebPaper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
List,
|
List,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Outlet, useLocation } from "react-router-dom";
|
import { Outlet, useLocation } from "react-router-dom";
|
||||||
import { RouterLink } from "./RouterLink";
|
import { RouterLink } from "./RouterLink";
|
||||||
@ -82,15 +82,7 @@ export function BaseAuthenticatedPage(): React.ReactElement {
|
|||||||
icon={<Icon path={mdiInformation} size={1} />}
|
icon={<Icon path={mdiInformation} size={1} />}
|
||||||
/>
|
/>
|
||||||
</List>
|
</List>
|
||||||
<div
|
<div style={{ flex: 1 }}>
|
||||||
style={{
|
|
||||||
flexGrow: 1,
|
|
||||||
flexShrink: 0,
|
|
||||||
flexBasis: 0,
|
|
||||||
minWidth: 0,
|
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -2,19 +2,10 @@ import { Paper, Typography } from "@mui/material";
|
|||||||
import React, { PropsWithChildren } from "react";
|
import React, { PropsWithChildren } from "react";
|
||||||
|
|
||||||
export function VirtWebPaper(
|
export function VirtWebPaper(
|
||||||
p: {
|
p: { label: string | React.ReactElement } & PropsWithChildren
|
||||||
label: string | React.ReactElement;
|
|
||||||
noHorizontalMargin?: boolean;
|
|
||||||
} & PropsWithChildren
|
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper elevation={2} style={{ padding: "10px", margin: "20px" }}>
|
||||||
elevation={2}
|
|
||||||
style={{
|
|
||||||
padding: "10px",
|
|
||||||
margin: p.noHorizontalMargin ? "20px 0px" : "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
style={{ marginBottom: "10px", fontWeight: "bold" }}
|
style={{ marginBottom: "10px", fontWeight: "bold" }}
|
||||||
|
@ -8,18 +8,7 @@ export function VirtWebRouteContainer(
|
|||||||
} & PropsWithChildren
|
} & PropsWithChildren
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ margin: "50px" }}>
|
||||||
style={{
|
|
||||||
margin: "50px",
|
|
||||||
flex: "1",
|
|
||||||
flexGrow: 1,
|
|
||||||
flexShrink: 0,
|
|
||||||
flexBasis: 0,
|
|
||||||
minWidth: 0,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user