Can download ISO files
This commit is contained in:
parent
8f65196344
commit
fbe11af121
55
virtweb_backend/Cargo.lock
generated
55
virtweb_backend/Cargo.lock
generated
@ -34,6 +34,29 @@ dependencies = [
|
|||||||
"smallvec",
|
"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]]
|
[[package]]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
@ -418,6 +441,12 @@ version = "1.0.75"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_escape"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.73"
|
version = "0.1.73"
|
||||||
@ -1023,6 +1052,12 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
@ -1241,6 +1276,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
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]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -1922,6 +1967,15 @@ version = "1.16.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
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]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
@ -1999,6 +2053,7 @@ name = "virtweb_backend"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
|
"actix-files",
|
||||||
"actix-identity",
|
"actix-identity",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
"actix-remote-ip",
|
"actix-remote-ip",
|
||||||
|
@ -16,6 +16,7 @@ actix-remote-ip = "0.1.0"
|
|||||||
actix-session = { version = "0.7.2", features = ["cookie-session"] }
|
actix-session = { version = "0.7.2", features = ["cookie-session"] }
|
||||||
actix-identity = "0.5.2"
|
actix-identity = "0.5.2"
|
||||||
actix-cors = "0.6.4"
|
actix-cors = "0.6.4"
|
||||||
|
actix-files = "0.6.2"
|
||||||
serde = { version = "1.0.175", features = ["derive"] }
|
serde = { version = "1.0.175", features = ["derive"] }
|
||||||
serde_json = "1.0.105"
|
serde_json = "1.0.105"
|
||||||
futures-util = "0.3.28"
|
futures-util = "0.3.28"
|
||||||
|
@ -2,9 +2,10 @@ use crate::app_config::AppConfig;
|
|||||||
use crate::constants;
|
use crate::constants;
|
||||||
use crate::controllers::HttpResult;
|
use crate::controllers::HttpResult;
|
||||||
use crate::utils::files_utils;
|
use crate::utils::files_utils;
|
||||||
|
use actix_files::NamedFile;
|
||||||
use actix_multipart::form::tempfile::TempFile;
|
use actix_multipart::form::tempfile::TempFile;
|
||||||
use actix_multipart::form::MultipartForm;
|
use actix_multipart::form::MultipartForm;
|
||||||
use actix_web::{web, HttpResponse};
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@ -128,12 +129,27 @@ pub async fn get_list() -> HttpResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct DeleteFilePath {
|
pub struct DownloadFilePath {
|
||||||
filename: String,
|
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
|
/// 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) {
|
if !files_utils::check_file_name(&p.filename) {
|
||||||
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
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),
|
web::post().to(iso_controller::upload_from_url),
|
||||||
)
|
)
|
||||||
.route("/api/iso/list", web::get().to(iso_controller::get_list))
|
.route("/api/iso/list", web::get().to(iso_controller::get_list))
|
||||||
|
.route(
|
||||||
|
"/api/iso/{filename}",
|
||||||
|
web::get().to(iso_controller::download_file),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/iso/{filename}",
|
"/api/iso/{filename}",
|
||||||
web::delete().to(iso_controller::delete_file),
|
web::delete().to(iso_controller::delete_file),
|
||||||
|
@ -6,7 +6,8 @@ interface RequestParams {
|
|||||||
allowFail?: boolean;
|
allowFail?: boolean;
|
||||||
jsonData?: any;
|
jsonData?: any;
|
||||||
formData?: FormData;
|
formData?: FormData;
|
||||||
progress?: (progress: number) => void;
|
upProgress?: (progress: number) => void;
|
||||||
|
downProgress?: (e: { progress: number; total: number }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIResponse {
|
interface APIResponse {
|
||||||
@ -62,11 +63,11 @@ export class APIClient {
|
|||||||
let status: number;
|
let status: number;
|
||||||
|
|
||||||
// Make the request with XMLHttpRequest
|
// Make the request with XMLHttpRequest
|
||||||
if (args.progress) {
|
if (args.upProgress) {
|
||||||
const res: XMLHttpRequest = await new Promise((resolve, reject) => {
|
const res: XMLHttpRequest = await new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.upload.addEventListener("progress", (e) =>
|
xhr.upload.addEventListener("progress", (e) =>
|
||||||
args.progress!(e.loaded / e.total)
|
args.upProgress!(e.loaded / e.total)
|
||||||
);
|
);
|
||||||
xhr.addEventListener("load", () => resolve(xhr));
|
xhr.addEventListener("load", () => resolve(xhr));
|
||||||
xhr.addEventListener("error", () =>
|
xhr.addEventListener("error", () =>
|
||||||
@ -104,6 +105,48 @@ export class APIClient {
|
|||||||
// Process response
|
// Process response
|
||||||
if (res.headers.get("content-type") === "application/json")
|
if (res.headers.get("content-type") === "application/json")
|
||||||
data = await res.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();
|
else data = await res.blob();
|
||||||
|
|
||||||
status = res.status;
|
status = res.status;
|
||||||
|
@ -20,7 +20,7 @@ export class IsoFilesApi {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
uri: "/iso/upload",
|
uri: "/iso/upload",
|
||||||
formData: fd,
|
formData: fd,
|
||||||
progress: progress,
|
upProgress: progress,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +47,24 @@ export class IsoFilesApi {
|
|||||||
).data;
|
).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
|
* Delete iso file
|
||||||
*/
|
*/
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
IconButton,
|
IconButton,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
TextField,
|
TextField,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||||
import { filesize } from "filesize";
|
import { filesize } from "filesize";
|
||||||
import { MuiFileInput } from "mui-file-input";
|
import { MuiFileInput } from "mui-file-input";
|
||||||
@ -19,6 +23,7 @@ import { AsyncWidget } from "../widgets/AsyncWidget";
|
|||||||
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
|
import { useConfirm } from "../hooks/providers/ConfirmDialogProvider";
|
||||||
|
import { downloadBlob } from "../utils/FilesUtils";
|
||||||
|
|
||||||
export function IsoFilesRoute(): React.ReactElement {
|
export function IsoFilesRoute(): React.ReactElement {
|
||||||
const [list, setList] = React.useState<IsoFile[] | undefined>();
|
const [list, setList] = React.useState<IsoFile[] | undefined>();
|
||||||
@ -185,6 +190,23 @@ function IsoFilesList(p: {
|
|||||||
const loadingMessage = useLoadingMessage();
|
const loadingMessage = useLoadingMessage();
|
||||||
const snackbar = useSnackbar();
|
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) => {
|
const deleteIso = async (entry: IsoFile) => {
|
||||||
if (
|
if (
|
||||||
!(await confirm(
|
!(await confirm(
|
||||||
@ -227,13 +249,20 @@ function IsoFilesList(p: {
|
|||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "",
|
headerName: "",
|
||||||
width: 70,
|
width: 120,
|
||||||
renderCell(params) {
|
renderCell(params) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton onClick={() => deleteIso(params.row)}>
|
<Tooltip title="Download file">
|
||||||
<DeleteIcon />
|
<IconButton onClick={() => downloadIso(params.row)}>
|
||||||
</IconButton>
|
<DownloadIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Delete file">
|
||||||
|
<IconButton onClick={() => deleteIso(params.row)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -241,13 +270,40 @@ function IsoFilesList(p: {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtWebPaper label="Files list">
|
<>
|
||||||
<DataGrid
|
<VirtWebPaper label="Files list">
|
||||||
getRowId={(c) => c.filename}
|
{/* Download notification */}
|
||||||
rows={p.list}
|
{dlProgress !== undefined && (
|
||||||
columns={columns}
|
<Alert severity="info">
|
||||||
autoHeight={true}
|
<div
|
||||||
/>
|
style={{
|
||||||
</VirtWebPaper>
|
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();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user