Can upload ISO files
This commit is contained in:
parent
83ccd3a4b9
commit
036595fb24
1
virtweb_backend/.gitignore
vendored
1
virtweb_backend/.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
target/
|
target/
|
||||||
.idea
|
.idea
|
||||||
|
storage
|
||||||
|
96
virtweb_backend/Cargo.lock
generated
96
virtweb_backend/Cargo.lock
generated
@ -99,6 +99,44 @@ dependencies = [
|
|||||||
"syn 2.0.29",
|
"syn 2.0.29",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-multipart"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d"
|
||||||
|
dependencies = [
|
||||||
|
"actix-multipart-derive",
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web",
|
||||||
|
"bytes",
|
||||||
|
"derive_more",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"httparse",
|
||||||
|
"local-waker",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_plain",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-multipart-derive"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"parse-size",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.29",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-remote-ip"
|
name = "actix-remote-ip"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -656,6 +694,41 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.29",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.29",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@ -996,6 +1069,12 @@ dependencies = [
|
|||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -1280,6 +1359,12 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-size"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
@ -1560,6 +1645,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_plain"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -1895,6 +1989,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
"actix-identity",
|
"actix-identity",
|
||||||
|
"actix-multipart",
|
||||||
"actix-remote-ip",
|
"actix-remote-ip",
|
||||||
"actix-session",
|
"actix-session",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
@ -1907,6 +2002,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -20,3 +20,5 @@ 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"
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
|
actix-multipart = "0.6.1"
|
||||||
|
tempfile = "3.8.0"
|
@ -1,4 +1,5 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// VirtWeb backend API
|
/// VirtWeb backend API
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
@ -64,6 +65,10 @@ pub struct AppConfig {
|
|||||||
#[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
|
#[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
|
||||||
oidc_redirect_url: String,
|
oidc_redirect_url: String,
|
||||||
|
|
||||||
|
/// Storage directory
|
||||||
|
#[arg(long, env, default_value = "storage")]
|
||||||
|
pub storage: String,
|
||||||
|
|
||||||
/// Directory where temporary files are stored
|
/// Directory where temporary files are stored
|
||||||
#[arg(long, env, default_value = "/tmp")]
|
#[arg(long, env, default_value = "/tmp")]
|
||||||
pub temp_dir: String,
|
pub temp_dir: String,
|
||||||
@ -131,6 +136,16 @@ impl AppConfig {
|
|||||||
self.oidc_redirect_url
|
self.oidc_redirect_url
|
||||||
.replace("APP_ORIGIN", &self.website_origin)
|
.replace("APP_ORIGIN", &self.website_origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get root storage directory
|
||||||
|
pub fn storage_path(&self) -> PathBuf {
|
||||||
|
Path::new(&self.storage).canonicalize().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get iso storage directory
|
||||||
|
pub fn iso_storage_path(&self) -> PathBuf {
|
||||||
|
self.storage_path().join("iso")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
@ -15,3 +15,9 @@ pub const ROUTES_WITHOUT_AUTH: [&str; 5] = [
|
|||||||
"/api/auth/start_oidc",
|
"/api/auth/start_oidc",
|
||||||
"/api/auth/finish_oidc",
|
"/api/auth/finish_oidc",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Allowed ISO mimetypes
|
||||||
|
pub const ALLOWED_ISO_MIME_TYPES: [&str; 1] = ["application/x-cd-image"];
|
||||||
|
|
||||||
|
/// ISO max size
|
||||||
|
pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;
|
||||||
|
60
virtweb_backend/src/controllers/iso_controller.rs
Normal file
60
virtweb_backend/src/controllers/iso_controller.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants;
|
||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::utils::files_utils;
|
||||||
|
use actix_multipart::form::tempfile::TempFile;
|
||||||
|
use actix_multipart::form::MultipartForm;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
#[derive(Debug, MultipartForm)]
|
||||||
|
pub struct UploadIsoForm {
|
||||||
|
#[multipart(rename = "file")]
|
||||||
|
files: Vec<TempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload iso file
|
||||||
|
pub async fn upload_file(MultipartForm(mut form): MultipartForm<UploadIsoForm>) -> HttpResult {
|
||||||
|
if form.files.is_empty() {
|
||||||
|
log::error!("Missing uploaded ISO file!");
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Missing file!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = form.files.remove(0);
|
||||||
|
|
||||||
|
if file.size > constants::ISO_MAX_SIZE {
|
||||||
|
log::error!("Uploaded ISO file is too large!");
|
||||||
|
return Ok(HttpResponse::BadRequest().json("File is too large!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(m) = &file.content_type {
|
||||||
|
if !constants::ALLOWED_ISO_MIME_TYPES.contains(&m.to_string().as_str()) {
|
||||||
|
log::error!("Uploaded ISO file has an invalid mimetype!");
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Invalid mimetype!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = match &file.file_name {
|
||||||
|
None => {
|
||||||
|
log::error!("Uploaded ISO file does not have a name!");
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Missing file name!"));
|
||||||
|
}
|
||||||
|
Some(f) => f,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !files_utils::check_file_name(file_name) {
|
||||||
|
log::error!("Bad file name for uploaded iso!");
|
||||||
|
return Ok(HttpResponse::BadRequest().json("Bad file name!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let dest_file = AppConfig::get().iso_storage_path().join(file_name);
|
||||||
|
log::info!("Will save ISO file {:?}", dest_file);
|
||||||
|
|
||||||
|
if dest_file.exists() {
|
||||||
|
log::error!("Conflict with uploaded iso file name!");
|
||||||
|
return Ok(HttpResponse::Conflict().json("The file already exists!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
file.file.persist(dest_file)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Accepted().finish())
|
||||||
|
}
|
@ -5,6 +5,7 @@ use std::fmt::{Display, Formatter};
|
|||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
|
pub mod iso_controller;
|
||||||
pub mod server_controller;
|
pub mod server_controller;
|
||||||
|
|
||||||
/// Custom error to ease controller writing
|
/// Custom error to ease controller writing
|
||||||
@ -58,4 +59,10 @@ impl From<std::num::ParseIntError> for HttpErr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<tempfile::PersistError> for HttpErr {
|
||||||
|
fn from(value: tempfile::PersistError) -> Self {
|
||||||
|
HttpErr { err: value.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type HttpResult = Result<HttpResponse, HttpErr>;
|
pub type HttpResult = Result<HttpResponse, HttpErr>;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::app_config::AppConfig;
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::constants;
|
||||||
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
|
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
|
||||||
use actix_web::{HttpResponse, Responder};
|
use actix_web::{HttpResponse, Responder};
|
||||||
|
|
||||||
@ -10,11 +11,15 @@ pub async fn root_index() -> impl Responder {
|
|||||||
struct StaticConfig {
|
struct StaticConfig {
|
||||||
local_auth_enabled: bool,
|
local_auth_enabled: bool,
|
||||||
oidc_auth_enabled: bool,
|
oidc_auth_enabled: bool,
|
||||||
|
iso_mimetypes: &'static [&'static str],
|
||||||
|
iso_max_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
||||||
HttpResponse::Ok().json(StaticConfig {
|
HttpResponse::Ok().json(StaticConfig {
|
||||||
local_auth_enabled: *local_auth,
|
local_auth_enabled: *local_auth,
|
||||||
oidc_auth_enabled: !AppConfig::get().disable_oidc,
|
oidc_auth_enabled: !AppConfig::get().disable_oidc,
|
||||||
|
iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
|
||||||
|
iso_max_size: constants::ISO_MAX_SIZE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3,3 +3,4 @@ pub mod constants;
|
|||||||
pub mod controllers;
|
pub mod controllers;
|
||||||
pub mod extractors;
|
pub mod extractors;
|
||||||
pub mod middlewares;
|
pub mod middlewares;
|
||||||
|
pub mod utils;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_identity::config::LogoutBehaviour;
|
use actix_identity::config::LogoutBehaviour;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
|
use actix_multipart::form::tempfile::TempFileConfig;
|
||||||
|
use actix_multipart::form::MultipartFormConfig;
|
||||||
use actix_remote_ip::RemoteIPConfig;
|
use actix_remote_ip::RemoteIPConfig;
|
||||||
use actix_session::storage::CookieSessionStore;
|
use actix_session::storage::CookieSessionStore;
|
||||||
use actix_session::SessionMiddleware;
|
use actix_session::SessionMiddleware;
|
||||||
@ -12,16 +14,21 @@ use actix_web::{web, App, HttpServer};
|
|||||||
use light_openid::basic_state_manager::BasicStateManager;
|
use light_openid::basic_state_manager::BasicStateManager;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use virtweb_backend::app_config::AppConfig;
|
use virtweb_backend::app_config::AppConfig;
|
||||||
|
use virtweb_backend::constants;
|
||||||
use virtweb_backend::constants::{
|
use virtweb_backend::constants::{
|
||||||
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
|
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
|
||||||
};
|
};
|
||||||
use virtweb_backend::controllers::{auth_controller, server_controller};
|
use virtweb_backend::controllers::{auth_controller, iso_controller, server_controller};
|
||||||
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
|
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
|
||||||
|
use virtweb_backend::utils::files_utils;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
|
log::debug!("Create required directory, if missing");
|
||||||
|
files_utils::create_directory_if_missing(&AppConfig::get().iso_storage_path()).unwrap();
|
||||||
|
|
||||||
log::info!("Start to listen on {}", AppConfig::get().listen_address);
|
log::info!("Start to listen on {}", AppConfig::get().listen_address);
|
||||||
|
|
||||||
let state_manager = Data::new(BasicStateManager::new());
|
let state_manager = Data::new(BasicStateManager::new());
|
||||||
@ -62,6 +69,10 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.app_data(Data::new(RemoteIPConfig {
|
.app_data(Data::new(RemoteIPConfig {
|
||||||
proxy: AppConfig::get().proxy_ip.clone(),
|
proxy: AppConfig::get().proxy_ip.clone(),
|
||||||
}))
|
}))
|
||||||
|
// Uploaded files
|
||||||
|
.app_data(web::PayloadConfig::new(constants::ISO_MAX_SIZE))
|
||||||
|
.app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE))
|
||||||
|
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
|
||||||
// Server controller
|
// Server controller
|
||||||
.route("/", web::get().to(server_controller::root_index))
|
.route("/", web::get().to(server_controller::root_index))
|
||||||
.route(
|
.route(
|
||||||
@ -89,6 +100,11 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/auth/sign_out",
|
"/api/auth/sign_out",
|
||||||
web::get().to(auth_controller::sign_out),
|
web::get().to(auth_controller::sign_out),
|
||||||
)
|
)
|
||||||
|
// ISO controller
|
||||||
|
.route(
|
||||||
|
"/api/iso/upload",
|
||||||
|
web::post().to(iso_controller::upload_file),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.bind(&AppConfig::get().listen_address)?
|
.bind(&AppConfig::get().listen_address)?
|
||||||
.run()
|
.run()
|
||||||
|
49
virtweb_backend/src/utils/files_utils.rs
Normal file
49
virtweb_backend/src/utils/files_utils.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
const INVALID_CHARS: [&str; 19] = [
|
||||||
|
"@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=",
|
||||||
|
"\t",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Check out whether a file name is valid or not
|
||||||
|
pub fn check_file_name(name: &str) -> bool {
|
||||||
|
!name.is_empty() && !INVALID_CHARS.iter().any(|c| name.contains(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create directory if missing
|
||||||
|
pub fn create_directory_if_missing(path: &PathBuf) -> anyhow::Result<()> {
|
||||||
|
if !path.exists() {
|
||||||
|
std::fs::create_dir_all(path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::utils::files_utils::check_file_name;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_file_name() {
|
||||||
|
assert!(!check_file_name(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parent_dir_file_name() {
|
||||||
|
assert!(!check_file_name("../file.test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_parent_dir_file_name() {
|
||||||
|
assert!(!check_file_name("..\\test.fr"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn special_char_file_name() {
|
||||||
|
assert!(!check_file_name("test:test.@"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid_file_name() {
|
||||||
|
assert!(check_file_name("test.iso"));
|
||||||
|
}
|
||||||
|
}
|
1
virtweb_backend/src/utils/mod.rs
Normal file
1
virtweb_backend/src/utils/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod files_utils;
|
77
virtweb_frontend/package-lock.json
generated
77
virtweb_frontend/package-lock.json
generated
@ -22,6 +22,8 @@
|
|||||||
"@types/node": "^16.18.48",
|
"@types/node": "^16.18.48",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"filesize": "^10.0.12",
|
||||||
|
"mui-file-input": "^3.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
@ -8472,11 +8474,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/filesize": {
|
"node_modules/filesize": {
|
||||||
"version": "8.0.7",
|
"version": "10.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.12.tgz",
|
||||||
"integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==",
|
"integrity": "sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4.0"
|
"node": ">= 10.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
@ -12673,6 +12675,39 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/mui-file-input": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mui-file-input/-/mui-file-input-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-02MtRtyPALqcQubpMdBVhScNK9T28GBcjJwv/Wg33hjJeEQzYaCiVC5OIjMO8WFoH5gEa+lYSfZAZ4rnn5r5cw==",
|
||||||
|
"dependencies": {
|
||||||
|
"pretty-bytes": "^6.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.5.0",
|
||||||
|
"@emotion/styled": "^11.3.0",
|
||||||
|
"@mui/icons-material": "^5.0.0",
|
||||||
|
"@mui/material": "^5.0.0",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mui-file-input/node_modules/pretty-bytes": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/multicast-dns": {
|
"node_modules/multicast-dns": {
|
||||||
"version": "7.2.5",
|
"version": "7.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
|
||||||
@ -14886,6 +14921,14 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-dev-utils/node_modules/filesize": {
|
||||||
|
"version": "8.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
||||||
|
"integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dev-utils/node_modules/has-flag": {
|
"node_modules/react-dev-utils/node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@ -23995,9 +24038,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"version": "8.0.7",
|
"version": "10.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.12.tgz",
|
||||||
"integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ=="
|
"integrity": "sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw=="
|
||||||
},
|
},
|
||||||
"fill-range": {
|
"fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
@ -27010,6 +27053,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"mui-file-input": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mui-file-input/-/mui-file-input-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-02MtRtyPALqcQubpMdBVhScNK9T28GBcjJwv/Wg33hjJeEQzYaCiVC5OIjMO8WFoH5gEa+lYSfZAZ4rnn5r5cw==",
|
||||||
|
"requires": {
|
||||||
|
"pretty-bytes": "^6.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pretty-bytes": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"multicast-dns": {
|
"multicast-dns": {
|
||||||
"version": "7.2.5",
|
"version": "7.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
|
||||||
@ -28422,6 +28480,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||||
},
|
},
|
||||||
|
"filesize": {
|
||||||
|
"version": "8.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
||||||
|
"integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ=="
|
||||||
|
},
|
||||||
"has-flag": {
|
"has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
"@types/node": "^16.18.48",
|
"@types/node": "^16.18.48",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"filesize": "^10.0.12",
|
||||||
|
"mui-file-input": "^3.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
|
@ -12,6 +12,7 @@ import { BaseLoginPage } from "./widgets/BaseLoginPage";
|
|||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||||
import { LoginRoute } from "./routes/auth/LoginRoute";
|
import { LoginRoute } from "./routes/auth/LoginRoute";
|
||||||
import { AuthApi } from "./api/AuthApi";
|
import { AuthApi } from "./api/AuthApi";
|
||||||
|
import { IsoFilesRoute } from "./routes/IsoFilesRoute";
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
@ -32,6 +33,7 @@ export function App() {
|
|||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
signedIn ? (
|
signedIn ? (
|
||||||
<Route path="*" element={<BaseAuthenticatedPage />}>
|
<Route path="*" element={<BaseAuthenticatedPage />}>
|
||||||
|
<Route path="iso" element={<IsoFilesRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
import { AuthApi } from "./AuthApi";
|
import { AuthApi } from "./AuthApi";
|
||||||
|
|
||||||
|
interface RequestParams {
|
||||||
|
uri: string;
|
||||||
|
method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
|
||||||
|
allowFail?: boolean;
|
||||||
|
jsonData?: any;
|
||||||
|
formData?: FormData;
|
||||||
|
progress?: (progress: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface APIResponse {
|
interface APIResponse {
|
||||||
data: any;
|
data: any;
|
||||||
status: number;
|
status: number;
|
||||||
@ -32,14 +41,8 @@ export class APIClient {
|
|||||||
/**
|
/**
|
||||||
* Perform a request on the backend
|
* Perform a request on the backend
|
||||||
*/
|
*/
|
||||||
static async exec(args: {
|
static async exec(args: RequestParams): Promise<APIResponse> {
|
||||||
uri: string;
|
let body: string | undefined | FormData = undefined;
|
||||||
method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
|
|
||||||
allowFail?: boolean;
|
|
||||||
jsonData?: any;
|
|
||||||
formData?: FormData;
|
|
||||||
}): Promise<APIResponse> {
|
|
||||||
let body = undefined;
|
|
||||||
let headers: any = {};
|
let headers: any = {};
|
||||||
|
|
||||||
// JSON request
|
// JSON request
|
||||||
@ -53,7 +56,45 @@ export class APIClient {
|
|||||||
body = args.formData;
|
body = args.formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(this.backendURL() + args.uri, {
|
const url = this.backendURL() + args.uri;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
let status: number;
|
||||||
|
|
||||||
|
// Make the request with XMLHttpRequest
|
||||||
|
if (args.progress) {
|
||||||
|
const res: XMLHttpRequest = await new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.addEventListener("progress", (e) =>
|
||||||
|
args.progress!(e.loaded / e.total)
|
||||||
|
);
|
||||||
|
xhr.addEventListener("load", () => resolve(xhr));
|
||||||
|
xhr.addEventListener("error", () =>
|
||||||
|
reject(new Error("File upload failed"))
|
||||||
|
);
|
||||||
|
xhr.addEventListener("abort", () =>
|
||||||
|
reject(new Error("File upload aborted"))
|
||||||
|
);
|
||||||
|
xhr.addEventListener("timeout", () =>
|
||||||
|
reject(new Error("File upload timeout"))
|
||||||
|
);
|
||||||
|
xhr.open(args.method, url, true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
for (const key in headers) {
|
||||||
|
if (headers.hasOwnProperty(key))
|
||||||
|
xhr.setRequestHeader(key, headers[key]);
|
||||||
|
}
|
||||||
|
xhr.send(body);
|
||||||
|
});
|
||||||
|
|
||||||
|
status = res.status;
|
||||||
|
if (res.responseType === "json") data = JSON.parse(res.responseText);
|
||||||
|
else data = res.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the request with fetch
|
||||||
|
else {
|
||||||
|
const res = await fetch(url, {
|
||||||
method: args.method,
|
method: args.method,
|
||||||
body: body,
|
body: body,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@ -61,23 +102,25 @@ export class APIClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Process response
|
// Process response
|
||||||
let data;
|
|
||||||
if (res.headers.get("content-type") === "application/json")
|
if (res.headers.get("content-type") === "application/json")
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
else data = await res.blob();
|
else data = await res.blob();
|
||||||
|
|
||||||
|
status = res.status;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle expired tokens
|
// Handle expired tokens
|
||||||
if (res.status === 412) {
|
if (status === 412) {
|
||||||
AuthApi.UnsetAuthenticated();
|
AuthApi.UnsetAuthenticated();
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.allowFail && !res.ok)
|
if (!args.allowFail && (status < 200 || status > 299))
|
||||||
throw new ApiError("Request failed!", res.status, data);
|
throw new ApiError("Request failed!", status, data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: data,
|
data: data,
|
||||||
status: res.status,
|
status: status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
virtweb_frontend/src/api/IsoFilesApi.ts
Normal file
21
virtweb_frontend/src/api/IsoFilesApi.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
export class IsoFilesApi {
|
||||||
|
/**
|
||||||
|
* Upload a new ISO file to the server
|
||||||
|
*/
|
||||||
|
static async Upload(
|
||||||
|
file: File,
|
||||||
|
progress: (progress: number) => void
|
||||||
|
): Promise<void> {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", file);
|
||||||
|
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "POST",
|
||||||
|
uri: "/iso/upload",
|
||||||
|
formData: fd,
|
||||||
|
progress: progress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,8 @@ import { APIClient } from "./ApiClient";
|
|||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
local_auth_enabled: boolean;
|
local_auth_enabled: boolean;
|
||||||
oidc_auth_enabled: boolean;
|
oidc_auth_enabled: boolean;
|
||||||
|
iso_mimetypes: string[];
|
||||||
|
iso_max_size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: ServerConfig | null = null;
|
let config: ServerConfig | null = null;
|
||||||
|
68
virtweb_frontend/src/hooks/providers/AlertDialogProvider.tsx
Normal file
68
virtweb_frontend/src/hooks/providers/AlertDialogProvider.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
type AlertContext = (message: string, title?: string) => Promise<void>;
|
||||||
|
|
||||||
|
const AlertContextK = React.createContext<AlertContext | null>(null);
|
||||||
|
|
||||||
|
export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const [title, setTitle] = React.useState<string | undefined>(undefined);
|
||||||
|
const [message, setMessage] = React.useState("");
|
||||||
|
|
||||||
|
const cb = React.useRef<null | (() => void)>(null);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
if (cb.current !== null) cb.current();
|
||||||
|
cb.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hook: AlertContext = (message, title) => {
|
||||||
|
setTitle(title);
|
||||||
|
setMessage(message);
|
||||||
|
setOpen(true);
|
||||||
|
|
||||||
|
return new Promise((res) => {
|
||||||
|
cb.current = res;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AlertContextK.Provider value={hook}>{p.children}</AlertContextK.Provider>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
{title && <DialogTitle id="alert-dialog-title">{title}</DialogTitle>}
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
{message}
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose} autoFocus>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAlert(): AlertContext {
|
||||||
|
return React.useContext(AlertContextK)!;
|
||||||
|
}
|
43
virtweb_frontend/src/hooks/providers/SnackbarProvider.tsx
Normal file
43
virtweb_frontend/src/hooks/providers/SnackbarProvider.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Snackbar } from "@mui/material";
|
||||||
|
|
||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
type SnackbarContext = (message: string, duration?: number) => void;
|
||||||
|
|
||||||
|
const SnackbarContextK = React.createContext<SnackbarContext | null>(null);
|
||||||
|
|
||||||
|
export function SnackbarProvider(p: PropsWithChildren): React.ReactElement {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const [message, setMessage] = React.useState("");
|
||||||
|
const [duration, setDuration] = React.useState(0);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hook: SnackbarContext = (message, duration) => {
|
||||||
|
setMessage(message);
|
||||||
|
setDuration(duration ?? 6000);
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SnackbarContextK.Provider value={hook}>
|
||||||
|
{p.children}
|
||||||
|
</SnackbarContextK.Provider>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={open}
|
||||||
|
autoHideDuration={duration}
|
||||||
|
onClose={handleClose}
|
||||||
|
message={message}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSnackbar(): SnackbarContext {
|
||||||
|
return React.useContext(SnackbarContextK)!;
|
||||||
|
}
|
@ -11,6 +11,8 @@ import reportWebVitals from "./reportWebVitals";
|
|||||||
import { LoadServerConfig } from "./widgets/LoadServerConfig";
|
import { LoadServerConfig } from "./widgets/LoadServerConfig";
|
||||||
import { ThemeProvider, createTheme } from "@mui/material";
|
import { ThemeProvider, createTheme } from "@mui/material";
|
||||||
import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
|
import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
|
||||||
|
import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
|
||||||
|
import { SnackbarProvider } from "./hooks/providers/SnackbarProvider";
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
const darkTheme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@ -24,11 +26,15 @@ const root = ReactDOM.createRoot(
|
|||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
|
<AlertDialogProvider>
|
||||||
|
<SnackbarProvider>
|
||||||
<LoadingMessageProvider>
|
<LoadingMessageProvider>
|
||||||
<LoadServerConfig>
|
<LoadServerConfig>
|
||||||
<App />
|
<App />
|
||||||
</LoadServerConfig>
|
</LoadServerConfig>
|
||||||
</LoadingMessageProvider>
|
</LoadingMessageProvider>
|
||||||
|
</SnackbarProvider>{" "}
|
||||||
|
</AlertDialogProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
90
virtweb_frontend/src/routes/IsoFilesRoute.tsx
Normal file
90
virtweb_frontend/src/routes/IsoFilesRoute.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Button, LinearProgress, Typography } from "@mui/material";
|
||||||
|
import { filesize } from "filesize";
|
||||||
|
import { MuiFileInput } from "mui-file-input";
|
||||||
|
import React from "react";
|
||||||
|
import { IsoFilesApi } from "../api/IsoFilesApi";
|
||||||
|
import { ServerApi } from "../api/ServerApi";
|
||||||
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
|
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
||||||
|
import { VirtWebPaper } from "../widgets/VirtWebPaper";
|
||||||
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
|
|
||||||
|
export function IsoFilesRoute(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<VirtWebRouteContainer label="ISO files management">
|
||||||
|
<UploadIsoFileForm onFileUploaded={() => alert("file uploaded!")} />
|
||||||
|
</VirtWebRouteContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadIsoFileForm(p: {
|
||||||
|
onFileUploaded: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState<File | null>(null);
|
||||||
|
const [uploadProgress, setUploadProgress] = React.useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (newValue: File | null) => {
|
||||||
|
if (newValue && newValue.size > ServerApi.Config.iso_max_size) {
|
||||||
|
alert(
|
||||||
|
`The file is too big (max size allowed: ${filesize(
|
||||||
|
ServerApi.Config.iso_max_size
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue && !ServerApi.Config.iso_mimetypes.includes(newValue.type)) {
|
||||||
|
alert(`Selected file mimetype is not allowed! (${newValue.type})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const upload = async () => {
|
||||||
|
try {
|
||||||
|
setUploadProgress(0);
|
||||||
|
await IsoFilesApi.Upload(value!, setUploadProgress);
|
||||||
|
|
||||||
|
setValue(null);
|
||||||
|
snackbar("The file was successfully uploaded!");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
await alert("Failed to perform file upload! " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadProgress(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (uploadProgress !== null) {
|
||||||
|
return (
|
||||||
|
<VirtWebPaper label="File upload">
|
||||||
|
<Typography variant="body1">
|
||||||
|
Upload in progress ({Math.floor(uploadProgress * 100)}%)...
|
||||||
|
</Typography>
|
||||||
|
<LinearProgress variant="determinate" value={uploadProgress * 100} />
|
||||||
|
</VirtWebPaper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtWebPaper label="File upload">
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<span style={{ width: "10px" }}></span>
|
||||||
|
<MuiFileInput
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
inputProps={{ accept: ServerApi.Config.iso_mimetypes.join(",") }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{value && <Button onClick={upload}>Upload file</Button>}
|
||||||
|
</div>
|
||||||
|
</VirtWebPaper>
|
||||||
|
);
|
||||||
|
}
|
18
virtweb_frontend/src/widgets/VirtWebPaper.tsx
Normal file
18
virtweb_frontend/src/widgets/VirtWebPaper.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Paper, Typography } from "@mui/material";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
export function VirtWebPaper(
|
||||||
|
p: { label: string } & PropsWithChildren
|
||||||
|
): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Paper elevation={2} style={{ padding: "10px" }}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
style={{ marginBottom: "10px", fontWeight: "bold" }}
|
||||||
|
>
|
||||||
|
{p.label}
|
||||||
|
</Typography>
|
||||||
|
{p.children}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
18
virtweb_frontend/src/widgets/VirtWebRouteContainer.tsx
Normal file
18
virtweb_frontend/src/widgets/VirtWebRouteContainer.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Typography } from "@mui/material";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
export function VirtWebRouteContainer(
|
||||||
|
p: {
|
||||||
|
label: string;
|
||||||
|
} & PropsWithChildren
|
||||||
|
): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div style={{ margin: "50px" }}>
|
||||||
|
<Typography variant="h4" style={{ marginBottom: "20px" }}>
|
||||||
|
{p.label}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{p.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user