Can upload ISO files
This commit is contained in:
		
							
								
								
									
										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,31 +56,71 @@ export class APIClient { | |||||||
|       body = args.formData; |       body = args.formData; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const res = await fetch(this.backendURL() + args.uri, { |     const url = this.backendURL() + args.uri; | ||||||
|       method: args.method, |  | ||||||
|       body: body, |  | ||||||
|       headers: headers, |  | ||||||
|       credentials: "include", |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // Process response |  | ||||||
|     let data; |     let data; | ||||||
|     if (res.headers.get("content-type") === "application/json") |     let status: number; | ||||||
|       data = await res.json(); |  | ||||||
|     else data = await res.blob(); |     // 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, | ||||||
|  |         body: body, | ||||||
|  |         headers: headers, | ||||||
|  |         credentials: "include", | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Process response | ||||||
|  |       if (res.headers.get("content-type") === "application/json") | ||||||
|  |         data = await res.json(); | ||||||
|  |       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}> | ||||||
|       <LoadingMessageProvider> |       <AlertDialogProvider> | ||||||
|         <LoadServerConfig> |         <SnackbarProvider> | ||||||
|           <App /> |           <LoadingMessageProvider> | ||||||
|         </LoadServerConfig> |             <LoadServerConfig> | ||||||
|       </LoadingMessageProvider> |               <App /> | ||||||
|  |             </LoadServerConfig> | ||||||
|  |           </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> | ||||||
|  |   ); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user