Can download ISO files
This commit is contained in:
		
							
								
								
									
										55
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										55
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -34,6 +34,29 @@ dependencies = [ | ||||
|  "smallvec", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "actix-files" | ||||
| version = "0.6.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" | ||||
| dependencies = [ | ||||
|  "actix-http", | ||||
|  "actix-service", | ||||
|  "actix-utils", | ||||
|  "actix-web", | ||||
|  "askama_escape", | ||||
|  "bitflags 1.3.2", | ||||
|  "bytes", | ||||
|  "derive_more", | ||||
|  "futures-core", | ||||
|  "http-range", | ||||
|  "log", | ||||
|  "mime", | ||||
|  "mime_guess", | ||||
|  "percent-encoding", | ||||
|  "pin-project-lite", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "actix-http" | ||||
| version = "3.4.0" | ||||
| @@ -418,6 +441,12 @@ version = "1.0.75" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" | ||||
|  | ||||
| [[package]] | ||||
| name = "askama_escape" | ||||
| version = "0.10.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" | ||||
|  | ||||
| [[package]] | ||||
| name = "async-trait" | ||||
| version = "0.1.73" | ||||
| @@ -1023,6 +1052,12 @@ dependencies = [ | ||||
|  "pin-project-lite", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "http-range" | ||||
| version = "0.1.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" | ||||
|  | ||||
| [[package]] | ||||
| name = "httparse" | ||||
| version = "1.8.0" | ||||
| @@ -1241,6 +1276,16 @@ version = "0.3.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | ||||
|  | ||||
| [[package]] | ||||
| name = "mime_guess" | ||||
| version = "2.0.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" | ||||
| dependencies = [ | ||||
|  "mime", | ||||
|  "unicase", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "miniz_oxide" | ||||
| version = "0.7.1" | ||||
| @@ -1922,6 +1967,15 @@ version = "1.16.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" | ||||
|  | ||||
| [[package]] | ||||
| name = "unicase" | ||||
| version = "2.7.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" | ||||
| dependencies = [ | ||||
|  "version_check", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "unicode-bidi" | ||||
| version = "0.3.13" | ||||
| @@ -1999,6 +2053,7 @@ name = "virtweb_backend" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "actix-cors", | ||||
|  "actix-files", | ||||
|  "actix-identity", | ||||
|  "actix-multipart", | ||||
|  "actix-remote-ip", | ||||
|   | ||||
| @@ -16,6 +16,7 @@ actix-remote-ip = "0.1.0" | ||||
| actix-session = { version = "0.7.2", features = ["cookie-session"] } | ||||
| actix-identity = "0.5.2" | ||||
| actix-cors = "0.6.4" | ||||
| actix-files = "0.6.2" | ||||
| serde = { version = "1.0.175", features = ["derive"] } | ||||
| serde_json = "1.0.105" | ||||
| futures-util = "0.3.28" | ||||
|   | ||||
| @@ -2,9 +2,10 @@ use crate::app_config::AppConfig; | ||||
| use crate::constants; | ||||
| use crate::controllers::HttpResult; | ||||
| use crate::utils::files_utils; | ||||
| use actix_files::NamedFile; | ||||
| use actix_multipart::form::tempfile::TempFile; | ||||
| use actix_multipart::form::MultipartForm; | ||||
| use actix_web::{web, HttpResponse}; | ||||
| use actix_web::{web, HttpRequest, HttpResponse}; | ||||
| use futures_util::StreamExt; | ||||
| use std::fs::File; | ||||
| use std::io::Write; | ||||
| @@ -128,12 +129,27 @@ pub async fn get_list() -> HttpResult { | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct DeleteFilePath { | ||||
| pub struct DownloadFilePath { | ||||
|     filename: String, | ||||
| } | ||||
|  | ||||
| /// Download ISO file | ||||
| pub async fn download_file(p: web::Path<DownloadFilePath>, req: HttpRequest) -> 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!")); | ||||
|     } | ||||
|  | ||||
|     Ok(NamedFile::open(file_path)?.into_response(&req)) | ||||
| } | ||||
|  | ||||
| /// Delete ISO file | ||||
| pub async fn delete_file(p: web::Path<DeleteFilePath>) -> HttpResult { | ||||
| pub async fn delete_file(p: web::Path<DownloadFilePath>) -> HttpResult { | ||||
|     if !files_utils::check_file_name(&p.filename) { | ||||
|         return Ok(HttpResponse::BadRequest().json("Invalid file name!")); | ||||
|     } | ||||
|   | ||||
| @@ -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::get().to(iso_controller::download_file), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/iso/{filename}", | ||||
|                 web::delete().to(iso_controller::delete_file), | ||||
|   | ||||
| @@ -6,7 +6,8 @@ interface RequestParams { | ||||
|   allowFail?: boolean; | ||||
|   jsonData?: any; | ||||
|   formData?: FormData; | ||||
|   progress?: (progress: number) => void; | ||||
|   upProgress?: (progress: number) => void; | ||||
|   downProgress?: (e: { progress: number; total: number }) => void; | ||||
| } | ||||
|  | ||||
| interface APIResponse { | ||||
| @@ -62,11 +63,11 @@ export class APIClient { | ||||
|     let status: number; | ||||
|  | ||||
|     // Make the request with XMLHttpRequest | ||||
|     if (args.progress) { | ||||
|     if (args.upProgress) { | ||||
|       const res: XMLHttpRequest = await new Promise((resolve, reject) => { | ||||
|         const xhr = new XMLHttpRequest(); | ||||
|         xhr.upload.addEventListener("progress", (e) => | ||||
|           args.progress!(e.loaded / e.total) | ||||
|           args.upProgress!(e.loaded / e.total) | ||||
|         ); | ||||
|         xhr.addEventListener("load", () => resolve(xhr)); | ||||
|         xhr.addEventListener("error", () => | ||||
| @@ -104,6 +105,48 @@ export class APIClient { | ||||
|       // Process response | ||||
|       if (res.headers.get("content-type") === "application/json") | ||||
|         data = await res.json(); | ||||
|       // Binary file | ||||
|       else if (res.body !== null && args.downProgress) { | ||||
|         // Track download progress | ||||
|         const contentEncoding = res.headers.get("content-encoding"); | ||||
|         const contentLength = contentEncoding | ||||
|           ? null | ||||
|           : res.headers.get("content-length"); | ||||
|  | ||||
|         const total = parseInt(contentLength ?? "0", 10); | ||||
|         let loaded = 0; | ||||
|  | ||||
|         const resInt = new Response( | ||||
|           new ReadableStream({ | ||||
|             start(controller) { | ||||
|               const reader = res.body!.getReader(); | ||||
|  | ||||
|               const read = async () => { | ||||
|                 try { | ||||
|                   const ret = await reader.read(); | ||||
|                   if (ret.done) { | ||||
|                     controller.close(); | ||||
|                     return; | ||||
|                   } | ||||
|                   loaded += ret.value.byteLength; | ||||
|                   args.downProgress!({ progress: loaded, total }); | ||||
|                   controller.enqueue(ret.value); | ||||
|                   read(); | ||||
|                 } catch (e) { | ||||
|                   console.error(e); | ||||
|                   controller.error(e); | ||||
|                 } | ||||
|               }; | ||||
|  | ||||
|               read(); | ||||
|             }, | ||||
|           }) | ||||
|         ); | ||||
|  | ||||
|         data = await resInt.blob(); | ||||
|       } | ||||
|  | ||||
|       // Do not track progress | ||||
|       else data = await res.blob(); | ||||
|  | ||||
|       status = res.status; | ||||
|   | ||||
| @@ -20,7 +20,7 @@ export class IsoFilesApi { | ||||
|       method: "POST", | ||||
|       uri: "/iso/upload", | ||||
|       formData: fd, | ||||
|       progress: progress, | ||||
|       upProgress: progress, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -47,6 +47,24 @@ export class IsoFilesApi { | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Download an ISO file | ||||
|    */ | ||||
|   static async Download( | ||||
|     file: IsoFile, | ||||
|     progress: (p: number) => void | ||||
|   ): Promise<Blob> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/iso/${file.filename}`, | ||||
|         downProgress(e) { | ||||
|           progress(Math.floor(100 * (e.progress / e.total))); | ||||
|         }, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete iso file | ||||
|    */ | ||||
|   | ||||
| @@ -1,11 +1,15 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import { | ||||
|   Alert, | ||||
|   Button, | ||||
|   CircularProgress, | ||||
|   IconButton, | ||||
|   LinearProgress, | ||||
|   TextField, | ||||
|   Tooltip, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import DownloadIcon from "@mui/icons-material/Download"; | ||||
| import { DataGrid, GridColDef } from "@mui/x-data-grid"; | ||||
| import { filesize } from "filesize"; | ||||
| import { MuiFileInput } from "mui-file-input"; | ||||
| @@ -19,6 +23,7 @@ import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { VirtWebPaper } from "../widgets/VirtWebPaper"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||
| import { downloadBlob } from "../utils/FilesUtils"; | ||||
|  | ||||
| export function IsoFilesRoute(): React.ReactElement { | ||||
|   const [list, setList] = React.useState<IsoFile[] | undefined>(); | ||||
| @@ -185,6 +190,23 @@ function IsoFilesList(p: { | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|   const snackbar = useSnackbar(); | ||||
|  | ||||
|   const [dlProgress, setDlProgress] = React.useState<undefined | number>(); | ||||
|  | ||||
|   const downloadIso = async (entry: IsoFile) => { | ||||
|     setDlProgress(0); | ||||
|  | ||||
|     try { | ||||
|       const blob = await IsoFilesApi.Download(entry, setDlProgress); | ||||
|  | ||||
|       await downloadBlob(blob, entry.filename); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert("Failed to download iso file!"); | ||||
|     } | ||||
|  | ||||
|     setDlProgress(undefined); | ||||
|   }; | ||||
|  | ||||
|   const deleteIso = async (entry: IsoFile) => { | ||||
|     if ( | ||||
|       !(await confirm( | ||||
| @@ -227,13 +249,20 @@ function IsoFilesList(p: { | ||||
|     { | ||||
|       field: "actions", | ||||
|       headerName: "", | ||||
|       width: 70, | ||||
|       width: 120, | ||||
|       renderCell(params) { | ||||
|         return ( | ||||
|           <> | ||||
|             <IconButton onClick={() => deleteIso(params.row)}> | ||||
|               <DeleteIcon /> | ||||
|             </IconButton> | ||||
|             <Tooltip title="Download file"> | ||||
|               <IconButton onClick={() => downloadIso(params.row)}> | ||||
|                 <DownloadIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|             <Tooltip title="Delete file"> | ||||
|               <IconButton onClick={() => deleteIso(params.row)}> | ||||
|                 <DeleteIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|           </> | ||||
|         ); | ||||
|       }, | ||||
| @@ -241,13 +270,40 @@ function IsoFilesList(p: { | ||||
|   ]; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebPaper label="Files list"> | ||||
|       <DataGrid | ||||
|         getRowId={(c) => c.filename} | ||||
|         rows={p.list} | ||||
|         columns={columns} | ||||
|         autoHeight={true} | ||||
|       /> | ||||
|     </VirtWebPaper> | ||||
|     <> | ||||
|       <VirtWebPaper label="Files list"> | ||||
|         {/* 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> | ||||
|         )} | ||||
|  | ||||
|         {/* Files list table */} | ||||
|         <DataGrid | ||||
|           getRowId={(c) => c.filename} | ||||
|           rows={p.list} | ||||
|           columns={columns} | ||||
|           autoHeight={true} | ||||
|         /> | ||||
|       </VirtWebPaper> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								virtweb_frontend/src/utils/FilesUtils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								virtweb_frontend/src/utils/FilesUtils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| export async function downloadBlob(blob: Blob, filename: string) { | ||||
|   const url = URL.createObjectURL(blob); | ||||
|  | ||||
|   const link = document.createElement("a"); | ||||
|   link.href = url; | ||||
|   link.target = "_blank"; | ||||
|   link.rel = "noopener"; | ||||
|   link.download = filename; | ||||
|   link.click(); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user