Compare commits
3 Commits
20de618568
...
e7ac0198ab
Author | SHA1 | Date | |
---|---|---|---|
e7ac0198ab | |||
927a51cda7 | |||
615dc1ed83 |
@ -245,7 +245,7 @@ impl AppConfig {
|
|||||||
storage_path.canonicalize().unwrap()
|
storage_path.canonicalize().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get iso storage directory
|
/// Get iso files storage directory
|
||||||
pub fn iso_storage_path(&self) -> PathBuf {
|
pub fn iso_storage_path(&self) -> PathBuf {
|
||||||
self.storage_path().join("iso")
|
self.storage_path().join("iso")
|
||||||
}
|
}
|
||||||
@ -265,15 +265,17 @@ impl AppConfig {
|
|||||||
self.vnc_sockets_path().join(format!("vnc-{}", name))
|
self.vnc_sockets_path().join(format!("vnc-{}", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get VM vnc sockets directory
|
/// Get VM root disks storage directory
|
||||||
pub fn disks_storage_path(&self) -> PathBuf {
|
pub fn root_vm_disks_storage_path(&self) -> PathBuf {
|
||||||
self.storage_path().join("disks")
|
self.storage_path().join("disks")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get specific VM disk storage directory
|
||||||
pub fn vm_storage_path(&self, id: XMLUuid) -> PathBuf {
|
pub fn vm_storage_path(&self, id: XMLUuid) -> PathBuf {
|
||||||
self.disks_storage_path().join(id.as_string())
|
self.root_vm_disks_storage_path().join(id.as_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the path were VM definitions are backed up
|
||||||
pub fn definitions_path(&self) -> PathBuf {
|
pub fn definitions_path(&self) -> PathBuf {
|
||||||
self.storage_path().join("definitions")
|
self.storage_path().join("definitions")
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,10 @@ use crate::constants;
|
|||||||
use crate::controllers::HttpResult;
|
use crate::controllers::HttpResult;
|
||||||
use crate::utils::file_disks_utils::DiskFileInfo;
|
use crate::utils::file_disks_utils::DiskFileInfo;
|
||||||
use crate::utils::files_utils;
|
use crate::utils::files_utils;
|
||||||
|
use actix_files::NamedFile;
|
||||||
use actix_multipart::form::MultipartForm;
|
use actix_multipart::form::MultipartForm;
|
||||||
use actix_multipart::form::tempfile::TempFile;
|
use actix_multipart::form::tempfile::TempFile;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::{HttpRequest, HttpResponse, web};
|
||||||
|
|
||||||
#[derive(Debug, MultipartForm)]
|
#[derive(Debug, MultipartForm)]
|
||||||
pub struct UploadDiskImageForm {
|
pub struct UploadDiskImageForm {
|
||||||
@ -67,3 +68,44 @@ pub async fn get_list() -> HttpResult {
|
|||||||
|
|
||||||
Ok(HttpResponse::Ok().json(list))
|
Ok(HttpResponse::Ok().json(list))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct DiskFilePath {
|
||||||
|
filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download disk image
|
||||||
|
pub async fn download(p: web::Path<DiskFilePath>, req: HttpRequest) -> HttpResult {
|
||||||
|
if !files_utils::check_file_name(&p.filename) {
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = AppConfig::get()
|
||||||
|
.disk_images_storage_path()
|
||||||
|
.join(&p.filename);
|
||||||
|
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NamedFile::open(file_path)?.into_response(&req))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a disk image
|
||||||
|
pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult {
|
||||||
|
if !files_utils::check_file_name(&p.filename) {
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = AppConfig::get()
|
||||||
|
.disk_images_storage_path()
|
||||||
|
.join(&p.filename);
|
||||||
|
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::remove_file(file_path)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Accepted().finish())
|
||||||
|
}
|
||||||
|
@ -132,12 +132,12 @@ pub async fn get_list() -> HttpResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct DownloadFilePath {
|
pub struct IsoFilePath {
|
||||||
filename: String,
|
filename: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download ISO file
|
/// Download ISO file
|
||||||
pub async fn download_file(p: web::Path<DownloadFilePath>, req: HttpRequest) -> HttpResult {
|
pub async fn download_file(p: web::Path<IsoFilePath>, req: HttpRequest) -> HttpResult {
|
||||||
if !files_utils::check_file_name(&p.filename) {
|
if !files_utils::check_file_name(&p.filename) {
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ pub async fn download_file(p: web::Path<DownloadFilePath>, req: HttpRequest) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Delete ISO file
|
/// Delete ISO file
|
||||||
pub async fn delete_file(p: web::Path<DownloadFilePath>) -> HttpResult {
|
pub async fn delete_file(p: web::Path<IsoFilePath>) -> HttpResult {
|
||||||
if !files_utils::check_file_name(&p.filename) {
|
if !files_utils::check_file_name(&p.filename) {
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,8 @@ async fn main() -> std::io::Result<()> {
|
|||||||
files_utils::create_directory_if_missing(AppConfig::get().disk_images_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().root_vm_disks_storage_path())
|
||||||
|
.unwrap();
|
||||||
files_utils::create_directory_if_missing(AppConfig::get().nat_path()).unwrap();
|
files_utils::create_directory_if_missing(AppConfig::get().nat_path()).unwrap();
|
||||||
files_utils::create_directory_if_missing(AppConfig::get().definitions_path()).unwrap();
|
files_utils::create_directory_if_missing(AppConfig::get().definitions_path()).unwrap();
|
||||||
files_utils::create_directory_if_missing(AppConfig::get().api_tokens_path()).unwrap();
|
files_utils::create_directory_if_missing(AppConfig::get().api_tokens_path()).unwrap();
|
||||||
@ -344,6 +345,14 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/disk_images/list",
|
"/api/disk_images/list",
|
||||||
web::get().to(disk_images_controller::get_list),
|
web::get().to(disk_images_controller::get_list),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/disk_images/{filename}",
|
||||||
|
web::get().to(disk_images_controller::download),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/disk_images/{filename}",
|
||||||
|
web::delete().to(disk_images_controller::delete),
|
||||||
|
)
|
||||||
// API tokens controller
|
// API tokens controller
|
||||||
.route(
|
.route(
|
||||||
"/api/token/create",
|
"/api/token/create",
|
||||||
|
@ -42,4 +42,32 @@ export class DiskImageApi {
|
|||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download disk image file
|
||||||
|
*/
|
||||||
|
static async Download(
|
||||||
|
file: DiskImage,
|
||||||
|
progress: (p: number) => void
|
||||||
|
): Promise<Blob> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: `/disk_images/${file.file_name}`,
|
||||||
|
downProgress(e) {
|
||||||
|
progress(Math.floor(100 * (e.progress / e.total)));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete disk image file
|
||||||
|
*/
|
||||||
|
static async Delete(file: DiskImage): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "DELETE",
|
||||||
|
uri: `/disk_images/${file.file_name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,30 @@
|
|||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
IconButton,
|
IconButton,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||||
import { filesize } from "filesize";
|
import { filesize } from "filesize";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { DiskImage, DiskImageApi } from "../api/DiskImageApi";
|
import { DiskImage, DiskImageApi } from "../api/DiskImageApi";
|
||||||
import { ServerApi } from "../api/ServerApi";
|
import { ServerApi } from "../api/ServerApi";
|
||||||
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
|
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
|
||||||
|
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
|
||||||
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
|
import { DateWidget } from "../widgets/DateWidget";
|
||||||
import { FileInput } from "../widgets/forms/FileInput";
|
import { FileInput } from "../widgets/forms/FileInput";
|
||||||
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
|
import { downloadBlob } from "../utils/FilesUtils";
|
||||||
|
|
||||||
export function DiskImagesRoute(): React.ReactElement {
|
export function DiskImagesRoute(): React.ReactElement {
|
||||||
const [list, setList] = React.useState<DiskImage[] | undefined>();
|
const [list, setList] = React.useState<DiskImage[] | undefined>();
|
||||||
@ -32,13 +41,6 @@ export function DiskImagesRoute(): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtWebRouteContainer label="Disk images">
|
|
||||||
<AsyncWidget
|
|
||||||
loadKey={loadKey.current}
|
|
||||||
errMsg="Failed to load disk images list!"
|
|
||||||
load={load}
|
|
||||||
ready={list !== undefined}
|
|
||||||
build={() => (
|
|
||||||
<VirtWebRouteContainer
|
<VirtWebRouteContainer
|
||||||
label="Disk images management"
|
label="Disk images management"
|
||||||
actions={
|
actions={
|
||||||
@ -51,9 +53,16 @@ export function DiskImagesRoute(): React.ReactElement {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={loadKey.current}
|
||||||
|
errMsg="Failed to load disk images list!"
|
||||||
|
load={load}
|
||||||
|
ready={list !== undefined}
|
||||||
|
build={() => (
|
||||||
|
<>
|
||||||
<UploadDiskImageCard onFileUploaded={reload} />
|
<UploadDiskImageCard onFileUploaded={reload} />
|
||||||
<DiskImageList list={list!} onReload={reload} />
|
<DiskImageList list={list!} onReload={reload} />
|
||||||
</VirtWebRouteContainer>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</VirtWebRouteContainer>
|
</VirtWebRouteContainer>
|
||||||
@ -148,5 +157,131 @@ function DiskImageList(p: {
|
|||||||
list: DiskImage[];
|
list: DiskImage[];
|
||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return <>todo</>;
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
|
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
|
||||||
|
|
||||||
|
// Download disk image file
|
||||||
|
const downloadDiskImage = async (entry: DiskImage) => {
|
||||||
|
setDlProgress(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await DiskImageApi.Download(entry, setDlProgress);
|
||||||
|
|
||||||
|
downloadBlob(blob, entry.file_name);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert(`Failed to download disk image file! ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDlProgress(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete disk image
|
||||||
|
const deleteDiskImage = async (entry: DiskImage) => {
|
||||||
|
if (
|
||||||
|
!(await confirm(
|
||||||
|
`Do you really want to delete this disk image (${entry.file_name}) ?`
|
||||||
|
))
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
loadingMessage.show("Deleting disk image file...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DiskImageApi.Delete(entry);
|
||||||
|
snackbar("The disk image has been successfully deleted!");
|
||||||
|
p.onReload();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert(`Failed to delete disk image!\n${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingMessage.hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (p.list.length === 0)
|
||||||
|
return (
|
||||||
|
<Typography variant="body1" style={{ textAlign: "center" }}>
|
||||||
|
No disk image uploaded for now.
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: GridColDef<(typeof p.list)[number]>[] = [
|
||||||
|
{ field: "file_name", headerName: "File name", flex: 3 },
|
||||||
|
{
|
||||||
|
field: "format",
|
||||||
|
headerName: "Format",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "file_size",
|
||||||
|
headerName: "File size",
|
||||||
|
flex: 1,
|
||||||
|
renderCell(params) {
|
||||||
|
return filesize(params.row.file_size);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "created",
|
||||||
|
headerName: "Created",
|
||||||
|
flex: 1,
|
||||||
|
renderCell(params) {
|
||||||
|
return <DateWidget time={params.row.created} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "actions",
|
||||||
|
headerName: "",
|
||||||
|
width: 120,
|
||||||
|
renderCell(params) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Download image">
|
||||||
|
<IconButton onClick={() => downloadDiskImage(params.row)}>
|
||||||
|
<DownloadIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Delete disk image">
|
||||||
|
<IconButton onClick={() => deleteDiskImage(params.row)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Download notification */}
|
||||||
|
{dlProgress !== undefined && (
|
||||||
|
<Alert severity="info">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">
|
||||||
|
Downloading... {dlProgress}%
|
||||||
|
</Typography>
|
||||||
|
<CircularProgress
|
||||||
|
variant="determinate"
|
||||||
|
size={"1.5rem"}
|
||||||
|
style={{ marginLeft: "10px" }}
|
||||||
|
value={dlProgress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
13
virtweb_frontend/src/widgets/DateWidget.tsx
Normal file
13
virtweb_frontend/src/widgets/DateWidget.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export function DateWidget(p: { time: number }): React.ReactElement {
|
||||||
|
const date = new Date(p.time * 1000);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{pad(date.getDate())}/{pad(date.getMonth() + 1)}/{date.getFullYear()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(num: number): string {
|
||||||
|
return num.toString().padStart(2, "0");
|
||||||
|
}
|
Reference in New Issue
Block a user