diff --git a/virtweb_backend/src/controllers/iso_controller.rs b/virtweb_backend/src/controllers/iso_controller.rs index bb0cc46..f206e8a 100644 --- a/virtweb_backend/src/controllers/iso_controller.rs +++ b/virtweb_backend/src/controllers/iso_controller.rs @@ -126,3 +126,25 @@ pub async fn get_list() -> HttpResult { Ok(HttpResponse::Ok().json(list)) } + +#[derive(serde::Deserialize)] +pub struct DeleteFilePath { + filename: String, +} + +/// Delete ISO file +pub async fn delete_file(p: web::Path) -> HttpResult { + if !files_utils::check_file_name(&p.filename) { + return Ok(HttpResponse::BadRequest().json("Invalid file name!")); + } + + let file_path = AppConfig::get().iso_storage_path().join(&p.filename); + + if !file_path.exists() { + return Ok(HttpResponse::NotFound().json("File does not exists!")); + } + + std::fs::remove_file(file_path)?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 75806c1..2a42ca2 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -109,6 +109,10 @@ async fn main() -> std::io::Result<()> { web::post().to(iso_controller::upload_from_url), ) .route("/api/iso/list", web::get().to(iso_controller::get_list)) + .route( + "/api/iso/{filename}", + web::delete().to(iso_controller::delete_file), + ) }) .bind(&AppConfig::get().listen_address)? .run() diff --git a/virtweb_frontend/src/api/IsoFilesApi.ts b/virtweb_frontend/src/api/IsoFilesApi.ts index 1246f4d..05dea7b 100644 --- a/virtweb_frontend/src/api/IsoFilesApi.ts +++ b/virtweb_frontend/src/api/IsoFilesApi.ts @@ -46,4 +46,14 @@ export class IsoFilesApi { }) ).data; } + + /** + * Delete iso file + */ + static async Delete(file: IsoFile): Promise { + await APIClient.exec({ + method: "DELETE", + uri: `/iso/${file.filename}`, + }); + } } diff --git a/virtweb_frontend/src/hooks/providers/ConfirmDialogProvider.tsx b/virtweb_frontend/src/hooks/providers/ConfirmDialogProvider.tsx new file mode 100644 index 0000000..edd2fc9 --- /dev/null +++ b/virtweb_frontend/src/hooks/providers/ConfirmDialogProvider.tsx @@ -0,0 +1,83 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from "@mui/material"; +import React, { PropsWithChildren } from "react"; + +type ConfirmContext = ( + message: string, + title?: string, + confirmButton?: string +) => Promise; + +const ConfirmContextK = React.createContext(null); + +export function ConfirmDialogProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [title, setTitle] = React.useState(undefined); + const [message, setMessage] = React.useState(""); + const [confirmButton, setConfirmButton] = React.useState( + undefined + ); + + const cb = React.useRef void)>(null); + + const handleClose = (confirm: boolean) => { + setOpen(false); + + if (cb.current !== null) cb.current(confirm); + cb.current = null; + }; + + const hook: ConfirmContext = (message, title, confirmButton) => { + setTitle(title); + setMessage(message); + setConfirmButton(confirmButton); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + + {p.children} + + + handleClose(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {title && {title}} + + + {message} + + + + + + + + + ); +} + +export function useConfirm(): ConfirmContext { + return React.useContext(ConfirmContextK)!; +} diff --git a/virtweb_frontend/src/index.tsx b/virtweb_frontend/src/index.tsx index 85635e4..579d9e3 100644 --- a/virtweb_frontend/src/index.tsx +++ b/virtweb_frontend/src/index.tsx @@ -13,6 +13,7 @@ import { ThemeProvider, createTheme } from "@mui/material"; import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider"; import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider"; import { SnackbarProvider } from "./hooks/providers/SnackbarProvider"; +import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider"; const darkTheme = createTheme({ palette: { @@ -26,15 +27,17 @@ const root = ReactDOM.createRoot( root.render( - - - - - - - - {" "} - + + + + + + + + + + + ); diff --git a/virtweb_frontend/src/routes/IsoFilesRoute.tsx b/virtweb_frontend/src/routes/IsoFilesRoute.tsx index 9b350fa..824d499 100644 --- a/virtweb_frontend/src/routes/IsoFilesRoute.tsx +++ b/virtweb_frontend/src/routes/IsoFilesRoute.tsx @@ -1,16 +1,24 @@ -import { Button, LinearProgress, TextField, Typography } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { + Button, + IconButton, + LinearProgress, + TextField, + Typography, +} from "@mui/material"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; import { filesize } from "filesize"; import { MuiFileInput } from "mui-file-input"; import React from "react"; import { IsoFile, IsoFilesApi } from "../api/IsoFilesApi"; import { ServerApi } from "../api/ServerApi"; import { useAlert } from "../hooks/providers/AlertDialogProvider"; +import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider"; +import { AsyncWidget } from "../widgets/AsyncWidget"; import { VirtWebPaper } from "../widgets/VirtWebPaper"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; -import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; -import { AsyncWidget } from "../widgets/AsyncWidget"; -import { DataGrid, GridColDef, GridRowsProp } from "@mui/x-data-grid"; +import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; export function IsoFilesRoute(): React.ReactElement { const [list, setList] = React.useState(); @@ -172,6 +180,33 @@ function IsoFilesList(p: { list: IsoFile[]; onReload: () => void; }): React.ReactElement { + const confirm = useConfirm(); + const alert = useAlert(); + const loadingMessage = useLoadingMessage(); + const snackbar = useSnackbar(); + + const deleteIso = async (entry: IsoFile) => { + if ( + !(await confirm( + `Do you really want to delete this file (${entry.filename}) ?` + )) + ) + return; + + loadingMessage.show("Deleting ISO file..."); + + try { + await IsoFilesApi.Delete(entry); + snackbar("The file has been successfully deleted!"); + p.onReload(); + } catch (e) { + console.error(e); + alert("Failed to delete file!"); + } + + loadingMessage.hide(); + }; + if (p.list.length === 0) return ( @@ -189,6 +224,20 @@ function IsoFilesList(p: { return filesize(params.row.size); }, }, + { + field: "actions", + headerName: "", + width: 70, + renderCell(params) { + return ( + <> + deleteIso(params.row)}> + + + + ); + }, + }, ]; return (