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 (
+
+ );
+}
+
+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)}
+ />
+ >
);
}