diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 31453b3..66d3308 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -2877,6 +2877,7 @@ version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" dependencies = [ + "mime_guess", "sha2", "walkdir", ] @@ -3745,7 +3746,6 @@ dependencies = [ "lazy_static", "light-openid", "log", - "mime_guess", "nix", "num", "quick-xml", diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index 7806ae4..9c75bd0 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -40,8 +40,7 @@ tokio = { version = "1.45.0", features = ["rt", "time", "macros"] } futures = "0.3.31" ipnetwork = { version = "0.21.1", features = ["serde"] } num = "0.4.3" -rust-embed = { version = "8.7.2" } -mime_guess = "2.0.5" +rust-embed = { version = "8.7.2", features = ["mime-guess"] } dotenvy = "0.15.7" nix = { version = "0.30.1", features = ["net"] } basic-jwt = "0.3.0" diff --git a/virtweb_backend/assets/img/debian.svg b/virtweb_backend/assets/img/debian.svg new file mode 100644 index 0000000..50dcb70 --- /dev/null +++ b/virtweb_backend/assets/img/debian.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/virtweb_backend/assets/img/kvm.png b/virtweb_backend/assets/img/kvm.png new file mode 100644 index 0000000..4cfb7e6 Binary files /dev/null and b/virtweb_backend/assets/img/kvm.png differ diff --git a/virtweb_backend/assets/img/ubuntu.svg b/virtweb_backend/assets/img/ubuntu.svg new file mode 100644 index 0000000..f217bc8 --- /dev/null +++ b/virtweb_backend/assets/img/ubuntu.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/virtweb_backend/assets/img/windows.svg b/virtweb_backend/assets/img/windows.svg new file mode 100644 index 0000000..2c7392e --- /dev/null +++ b/virtweb_backend/assets/img/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/virtweb_backend/assets/iso_catalog.json b/virtweb_backend/assets/iso_catalog.json new file mode 100644 index 0000000..6088298 --- /dev/null +++ b/virtweb_backend/assets/iso_catalog.json @@ -0,0 +1,47 @@ +[ + { + "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" + } +] \ No newline at end of file diff --git a/virtweb_backend/src/controllers/static_controller.rs b/virtweb_backend/src/controllers/static_controller.rs index 9960852..4e132d4 100644 --- a/virtweb_backend/src/controllers/static_controller.rs +++ b/virtweb_backend/src/controllers/static_controller.rs @@ -3,6 +3,27 @@ pub use serve_static_debug::{root_index, serve_static_content}; #[cfg(not(debug_assertions))] 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) -> 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)] mod serve_static_debug { use actix_web::{HttpResponse, Responder}; @@ -16,6 +37,7 @@ mod serve_static_debug { } } +/// Web asset hosting in release mode #[cfg(not(debug_assertions))] mod serve_static_release { use actix_web::{HttpResponse, Responder, web}; @@ -23,12 +45,12 @@ mod serve_static_release { #[derive(RustEmbed)] #[folder = "static/"] - struct Asset; + struct WebAsset; fn handle_embedded_file(path: &str, can_fallback: bool) -> HttpResponse { - match (Asset::get(path), can_fallback) { + match (WebAsset::get(path), can_fallback) { (Some(content), _) => HttpResponse::Ok() - .content_type(mime_guess::from_path(path).first_or_octet_stream().as_ref()) + .content_type(content.metadata.mimetype()) .body(content.data.into_owned()), (None, false) => HttpResponse::NotFound().body("404 Not Found"), (None, true) => handle_embedded_file("index.html", false), diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index a051640..e3d1bbf 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -337,6 +337,11 @@ async fn main() -> std::io::Result<()> { web::delete().to(api_tokens_controller::delete), ) // 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( "/{tail:.*}", diff --git a/virtweb_frontend/src/api/IsoFilesApi.ts b/virtweb_frontend/src/api/IsoFilesApi.ts index 4bad0a9..b36a430 100644 --- a/virtweb_frontend/src/api/IsoFilesApi.ts +++ b/virtweb_frontend/src/api/IsoFilesApi.ts @@ -5,6 +5,15 @@ export interface IsoFile { size: number; } +/** + * ISO catalog entries + */ +export interface ISOCatalogEntry { + name: string; + url: string; + image: string; +} + export class IsoFilesApi { /** * Upload a new ISO file to the server @@ -74,4 +83,23 @@ export class IsoFilesApi { uri: `/iso/${file.filename}`, }); } + + /** + * Get iso catalog + */ + static async Catalog(): Promise { + 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; + } } diff --git a/virtweb_frontend/src/dialogs/IsoCatalogDialog.tsx b/virtweb_frontend/src/dialogs/IsoCatalogDialog.tsx new file mode 100644 index 0000000..24fbda8 --- /dev/null +++ b/virtweb_frontend/src/dialogs/IsoCatalogDialog.tsx @@ -0,0 +1,75 @@ +import { + Avatar, + 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(); + + const load = async () => { + setCatalog(await IsoFilesApi.Catalog()); + }; + + return ( + + Iso catalog + + } + /> + + + + + + ); +} + +export function IsoCatalogDialogInner(p: { + catalog: ISOCatalogEntry[]; +}): React.ReactElement { + return ( + + {p.catalog.map((entry) => ( + + + + + + + + + + + ))} + + ); +} diff --git a/virtweb_frontend/src/routes/IsoFilesRoute.tsx b/virtweb_frontend/src/routes/IsoFilesRoute.tsx index eaa92f3..48e3444 100644 --- a/virtweb_frontend/src/routes/IsoFilesRoute.tsx +++ b/virtweb_frontend/src/routes/IsoFilesRoute.tsx @@ -24,9 +24,12 @@ import { AsyncWidget } from "../widgets/AsyncWidget"; import { FileInput } from "../widgets/forms/FileInput"; import { VirtWebPaper } from "../widgets/VirtWebPaper"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import MenuBookIcon from "@mui/icons-material/MenuBook"; +import { IsoCatalogDialog } from "../dialogs/IsoCatalogDialog"; export function IsoFilesRoute(): React.ReactElement { const [list, setList] = React.useState(); + const [isoCatalog, setIsoCatalog] = React.useState(false); const loadKey = React.useRef(1); @@ -40,19 +43,34 @@ export function IsoFilesRoute(): React.ReactElement { }; return ( - ( - - - - - - )} - /> + <> + ( + + setIsoCatalog(true)}> + + + + } + > + + + + + )} + /> + setIsoCatalog(false)} + /> + ); }