diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index d7d54485bf..e0f93f0edf 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -1,65 +1,5 @@ use utoipa::OpenApi; -use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest; -use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest; -use artcraft_api_defs::common::responses::media_links::*; -use artcraft_api_defs::common::responses::simple_generic_json_success::SimpleGenericJsonSuccess; -use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::*; -use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio; -use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages; -use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest; -use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse; -use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio; -use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages; -use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest; -use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse; -use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio; -use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages; -use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest; -use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse; -use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio; -use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages; -use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest; -use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse; -use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageQuality; -use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageSize; -use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageNumImages; -use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageRequest; -use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageResponse; -use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundRequest; -use artcraft_api_defs::generate::image::remove_image_background::RemoveImageBackgroundResponse; -use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest; -use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse; -use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest; -use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse; -use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio; -use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration; -use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest; -use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse; -use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio; -use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration; -use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest; -use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse; -use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio; -use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration; -use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest; -use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse; -use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration; -use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest; -use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse; -use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution; -use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio; -use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration; -use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest; -use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse; -use artcraft_api_defs::jobs::list_session_jobs::*; -use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFilePathInfo; -use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFileRequest; -use artcraft_api_defs::prompts::create_prompt::CreatePromptRequest; -use artcraft_api_defs::prompts::create_prompt::CreatePromptResponse; -use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError; -use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*; -use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*; use crate::http_server::common_requests::auto_product_category::AutoProductCategory; use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo; use crate::http_server::common_responses::media::cover_image_links::CoverImageLinks; @@ -137,6 +77,7 @@ use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_f use crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::*; use crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::*; use crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::*; +use crate::http_server::endpoints::media_files::upload::upload_spz_media_file_handler::*; use crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::*; use crate::http_server::endpoints::media_files::upload::upload_video_new::upload_new_video_media_file_handler::*; use crate::http_server::endpoints::media_files::upload::upload_video_old::upload_video_media_file_handler::*; @@ -148,8 +89,8 @@ use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_fea use crate::http_server::endpoints::prompts::get_prompt_handler::*; use crate::http_server::endpoints::service::status_alert_handler::*; use crate::http_server::endpoints::stats::get_unified_queue_stats_handler::*; -use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::*; use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::EnqueueStudioGen2Request; +use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::*; use crate::http_server::endpoints::tags::list_tags_for_entity_handler::*; use crate::http_server::endpoints::tags::set_tags_for_entity_handler::*; use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::*; @@ -181,6 +122,66 @@ use crate::http_server::endpoints::weights::list::list_weights_by_user_handler:: use crate::http_server::endpoints::weights::search::search_model_weights_impl::*; use crate::http_server::endpoints::weights::update::set_model_weight_cover_image_handler::*; use crate::http_server::endpoints::weights::update::update_weight_handler::*; +use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest; +use artcraft_api_defs::common::responses::job_details::JobDetailsLivePortraitRequest; +use artcraft_api_defs::common::responses::media_links::*; +use artcraft_api_defs::common::responses::simple_generic_json_success::SimpleGenericJsonSuccess; +use artcraft_api_defs::generate::image::bg_removal::remove_image_background::RemoveImageBackgroundRequest; +use artcraft_api_defs::generate::image::bg_removal::remove_image_background::RemoveImageBackgroundResponse; +use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::*; +use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageAspectRatio; +use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages; +use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest; +use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageResponse; +use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageAspectRatio; +use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageNumImages; +use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest; +use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageResponse; +use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageAspectRatio; +use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages; +use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest; +use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageResponse; +use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageAspectRatio; +use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages; +use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest; +use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageResponse; +use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageQuality; +use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageImageSize; +use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageNumImages; +use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageRequest; +use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageResponse; +use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dRequest; +use artcraft_api_defs::generate::object::generate_hunyuan_2_0_image_to_3d::GenerateHunyuan20ImageTo3dResponse; +use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dRequest; +use artcraft_api_defs::generate::object::generate_hunyuan_2_1_image_to_3d::GenerateHunyuan21ImageTo3dResponse; +use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProAspectRatio; +use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProDuration; +use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::GenerateKling16ProImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterAspectRatio; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterDuration; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::GenerateKling21MasterImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProAspectRatio; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProDuration; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::GenerateKling21ProImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteDuration; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteImageToVideoResponse; +use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::GenerateSeedance10LiteResolution; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2AspectRatio; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2Duration; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoRequest; +use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::GenerateVeo2ImageToVideoResponse; +use artcraft_api_defs::jobs::list_session_jobs::*; +use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFilePathInfo; +use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFileRequest; +use artcraft_api_defs::prompts::create_prompt::CreatePromptRequest; +use artcraft_api_defs::prompts::create_prompt::CreatePromptResponse; +use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_error::CreateCheckoutSessionError; +use billing_component::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::*; +use billing_component::users::http_endpoints::list_active_user_subscriptions_handler::*; use enums::by_table::beta_keys::beta_key_product::BetaKeyProduct; use enums::by_table::comments::comment_entity_type::CommentEntityType; use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType; @@ -285,6 +286,7 @@ use tokens::tokens::zs_voice_datasets::*; crate::http_server::endpoints::media_files::upload::upload_image_media_file_handler::upload_image_media_file_handler, crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_media_file_handler::upload_new_engine_asset_media_file_handler, crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::upload_new_scene_media_file_handler, + crate::http_server::endpoints::media_files::upload::upload_spz_media_file_handler::upload_spz_media_file_handler, crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::upload_pmx_media_file_handler, crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::upload_saved_scene_media_file_handler, crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::upload_scene_snapshot_media_file_handler, @@ -753,6 +755,8 @@ use tokens::tokens::zs_voice_datasets::*; UploadNewEngineAssetSuccessResponse, UploadNewSceneMediaFileForm, UploadNewSceneMediaFileSuccessResponse, + UploadSpzMediaFileForm, + UploadSpzMediaFileSuccessResponse, UploadNewVideoMediaFileForm, UploadNewVideoMediaFileSuccessResponse, UploadPmxFileForm, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/mod.rs index 70d3caf04f..40b5331fcd 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/mod.rs @@ -5,6 +5,7 @@ pub mod upload_generic; pub mod upload_image_media_file_handler; pub mod upload_new_engine_asset_media_file_handler; pub mod upload_new_scene_media_file_handler; +pub mod upload_spz_media_file_handler; pub mod upload_pmx; pub mod upload_saved_scene_media_file_handler; pub mod upload_scene_snapshot_media_file_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_spz_media_file_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_spz_media_file_handler.rs new file mode 100644 index 0000000000..750cb477fa --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_spz_media_file_handler.rs @@ -0,0 +1,273 @@ +use std::io::Read; +use std::path::PathBuf; +use std::sync::Arc; + +use actix_multipart::form::tempfile::TempFile; +use actix_multipart::form::text::Text; +use actix_multipart::form::MultipartForm; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use log::{error, info, warn}; +use utoipa::ToSchema; + +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::by_table::media_files::media_file_class::MediaFileClass; +use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; +use enums::by_table::media_files::media_file_type::MediaFileType; +use enums::common::visibility::Visibility; +use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes; +use http_server_common::request::get_request_ip::get_request_ip; +use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder; +use mysql_queries::queries::media_files::create::specialized_insert::insert_media_file_from_file_upload::{insert_media_file_from_file_upload, InsertMediaFileFromUploadArgs, UploadType}; +use tokens::tokens::media_files::MediaFileToken; + +use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::state::server_state::ServerState; + +const GZIP_MIME_TYPE: &str = "application/gzip"; +const SPZ_EXTENSION: &str = ".spz"; +const CERAMIC_SPZ_EXTENSION: &str = ".ceramic.spz"; + +/// Form-multipart request fields for SPZ (Gaussian Splat) upload. +/// +/// IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UploadSpzMediaFileForm` (Under "Schema") FOR DETAILS ON FIELDS AND NULLABILITY. +#[derive(MultipartForm, ToSchema)] +#[multipart(duplicate_field = "deny")] +pub struct UploadSpzMediaFileForm { + /// UUID for request idempotency + #[multipart(limit = "2 KiB")] + #[schema(value_type = String, format = Binary)] + uuid_idempotency_token: Text, + + /// The uploaded SPZ file + #[multipart(limit = "512 MiB")] + #[schema(value_type = Vec, format = Binary)] + file: TempFile, + + /// Optional: Title (name) of the Gaussian splat + #[multipart(limit = "2 KiB")] + #[schema(value_type = Option, format = Binary)] + maybe_title: Option>, + + /// Optional: Visibility of the Gaussian splat + #[multipart(limit = "2 KiB")] + #[schema(value_type = Option, format = Binary)] + maybe_visibility: Option>, +} + +#[derive(Serialize, ToSchema)] +pub struct UploadSpzMediaFileSuccessResponse { + pub success: bool, + pub media_file_token: MediaFileToken, +} + +/// This endpoint is for uploading SPZ (Gaussian Splat) files. +/// +/// SPZ files are compressed Gaussian splat files used for 3D scene representation. +/// The file must have a `.spz` extension and be gzip-compressed. +#[utoipa::path( + post, + tag = "Media Files (Upload)", + path = "/v1/media_files/upload/spz", + responses( + (status = 200, description = "Success", body = UploadSpzMediaFileSuccessResponse), + (status = 400, description = "Bad input", body = MediaFileUploadError), + (status = 401, description = "Not authorized", body = MediaFileUploadError), + (status = 429, description = "Too many requests", body = MediaFileUploadError), + (status = 500, description = "Server error", body = MediaFileUploadError), + ), + params( + ( + "request" = UploadSpzMediaFileForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UploadSpzMediaFileForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn upload_spz_media_file_handler( + http_request: HttpRequest, + server_state: web::Data>, + MultipartForm(mut form): MultipartForm, +) -> Result, MediaFileUploadError> { + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + error!("MySql pool error: {:?}", err); + MediaFileUploadError::ServerError + })?; + + // ==================== READ SESSION ==================== // + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + error!("Session checker error: {:?}", e); + MediaFileUploadError::ServerError + })?; + + let maybe_user_token = maybe_user_session + .as_ref() + .map(|session| session.get_strongly_typed_user_token()); + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // ==================== BANNED USERS ==================== // + + if let Some(ref user) = maybe_user_session { + if user.is_banned { + return Err(MediaFileUploadError::NotAuthorizedVerbose("user is banned".to_string())); + } + } + + // ==================== RATE LIMIT ==================== // + + let rate_limiter = match maybe_user_session { + None => &server_state.redis_rate_limiters.file_upload_logged_out, + Some(ref _session) => &server_state.redis_rate_limiters.file_upload_logged_in, + }; + + if let Err(_err) = rate_limiter.rate_limit_request(&http_request).await { + return Err(MediaFileUploadError::RateLimited); + } + + // ==================== HANDLE IDEMPOTENCY ==================== // + + let uuid_idempotency_token = form.uuid_idempotency_token.as_ref(); + + if let Err(reason) = validate_idempotency_token_format(uuid_idempotency_token) { + return Err(MediaFileUploadError::BadInput(reason)); + } + + insert_idempotency_token(uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + MediaFileUploadError::BadInput("invalid idempotency token".to_string()) + })?; + + // ==================== UPLOAD METADATA ==================== // + + let maybe_title = form.maybe_title + .map(|title| title.trim().to_string()) + .filter(|title| !title.is_empty()); + + let creator_set_visibility = form.maybe_visibility + .map(|visibility| visibility.0) + .or_else(|| { + maybe_user_session + .as_ref() + .map(|user_session| user_session.preferred_tts_result_visibility) + }) + .unwrap_or(Visibility::default()); + + // ==================== USER DATA ==================== // + + let ip_address = get_request_ip(&http_request); + + // ==================== FILE VALIDATION ==================== // + + let maybe_filename = form.file.file_name.as_deref() + .as_deref() + .map(|filename| PathBuf::from(filename)); + + let filename_lowercase = maybe_filename + .as_ref() + .and_then(|f| f.to_str()) + .map(|s| s.to_ascii_lowercase()) + .unwrap_or_default(); + + // Validate file extension + if !filename_lowercase.ends_with(".spz") { + return Err(MediaFileUploadError::BadInput( + "File must have .spz extension".to_string() + )); + } + + let mut file_bytes = Vec::new(); + form.file.file.read_to_end(&mut file_bytes) + .map_err(|e| { + error!("Problem reading file: {:?}", e); + MediaFileUploadError::ServerError + })?; + + // Validate mimetype (SPZ files are gzip-compressed) + let detected_mimetype = get_mimetype_for_bytes(file_bytes.as_ref()); + + if detected_mimetype != Some(GZIP_MIME_TYPE) { + return Err(MediaFileUploadError::BadInput( + "SPZ file must be gzip-compressed".to_string() + )); + } + + let file_size_bytes = file_bytes.len(); + + let hash = sha256_hash_bytes(&file_bytes) + .map_err(|io_error| { + error!("Problem hashing bytes: {:?}", io_error); + MediaFileUploadError::ServerError + })?; + + // ==================== UPLOAD AND SAVE ==================== // + + // Check for WorldLabs ceramic SPZ files + let is_world_labs_spz = filename_lowercase.contains("ceramic") && filename_lowercase.ends_with(".spz"); + + let extension = if is_world_labs_spz { + CERAMIC_SPZ_EXTENSION + } else { + SPZ_EXTENSION + }; + + const PREFIX: Option<&str> = Some("artcraft_"); + + let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(extension)); + + info!("Uploading SPZ media to bucket path: {}", public_upload_path.get_full_object_path_str()); + + server_state.public_bucket_client.upload_file_with_content_type( + public_upload_path.get_full_object_path_str(), + file_bytes.as_ref(), + GZIP_MIME_TYPE) + .await + .map_err(|e| { + warn!("Upload SPZ bytes to bucket error: {:?}", e); + MediaFileUploadError::ServerError + })?; + + let media_token = MediaFileInsertBuilder::new() + .media_file_class(MediaFileClass::Dimensional) + .media_file_type(MediaFileType::Spz) + .maybe_creator_user(maybe_user_token.as_ref()) + .maybe_creator_anonymous_visitor(maybe_avt_token.as_ref()) + .creator_ip_address(&ip_address) + .creator_set_visibility(creator_set_visibility) + .media_file_origin_category(MediaFileOriginCategory::Upload) + .mime_type(GZIP_MIME_TYPE) + .file_size_bytes(file_size_bytes as u64) + .checksum_sha2(&hash) + .maybe_title(maybe_title.as_deref()) + .is_intermediate_system_file(false) + .public_bucket_directory_hash(&public_upload_path) + .maybe_origin_filename(form.file.file_name.as_deref()) + .insert_pool(&server_state.mysql_pool) + .await + .map_err(|err| { + warn!("New SPZ file creation DB error: {:?}", err); + MediaFileUploadError::ServerError + })?; + + info!("new SPZ media file token: {:?}", &media_token); + + Ok(Json(UploadSpzMediaFileSuccessResponse { + success: true, + media_file_token: media_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/media_files_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/media_files_routes.rs index 41f7da99f1..cc630931a2 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/media_files_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/media_files_routes.rs @@ -29,6 +29,7 @@ use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_f use crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::upload_pmx_media_file_handler; use crate::http_server::endpoints::media_files::upload::upload_saved_scene_media_file_handler::upload_saved_scene_media_file_handler; use crate::http_server::endpoints::media_files::upload::upload_scene_snapshot_media_file_handler::upload_scene_snapshot_media_file_handler; +use crate::http_server::endpoints::media_files::upload::upload_spz_media_file_handler::upload_spz_media_file_handler; use crate::http_server::endpoints::media_files::upload::upload_studio_shot::upload_studio_shot_media_file_handler::upload_studio_shot_media_file_handler; use crate::http_server::endpoints::media_files::upload::upload_video_new::upload_new_video_media_file_handler::upload_new_video_media_file_handler; use crate::http_server::endpoints::media_files::upload::upload_video_old::upload_video_media_file_handler::upload_video_media_file_handler; @@ -149,6 +150,10 @@ pub fn add_media_file_routes (app: App) -> App .route(web::post().to(upload_new_scene_media_file_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/upload/spz") + .route(web::post().to(upload_spz_media_file_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .service(web::resource("/upload/scene_snapshot") .route(web::post().to(upload_scene_snapshot_media_file_handler)) .route(web::head().to(|| HttpResponse::Ok()))