From 99fd390bc5ea0021605af94cde34962f9e916d2d Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 9 Dec 2025 00:33:14 -0500 Subject: [PATCH] spz file uploads (#924) --- _sql/migrations_squashed/media_files.sql | 1 + .../list_user_bookmarks_result.rs | 1 + .../by_table/media_files/media_file_type.rs | 14 ++++++++++- .../process_upload_media_file.rs | 23 ++++++++++++++++++- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/_sql/migrations_squashed/media_files.sql b/_sql/migrations_squashed/media_files.sql index 56c732cdef..7b336a2274 100644 --- a/_sql/migrations_squashed/media_files.sql +++ b/_sql/migrations_squashed/media_files.sql @@ -152,6 +152,7 @@ CREATE TABLE media_files ( -- * `vmd` -- * `pmx` -- * `csv` + -- * `spz` Gaussian splats media_type VARCHAR(16) NOT NULL, -- Broad class of media: diff --git a/crates/schema/database/mysql_queries/src/queries/users/user_bookmarks/list_user_bookmarks_result.rs b/crates/schema/database/mysql_queries/src/queries/users/user_bookmarks/list_user_bookmarks_result.rs index 2d3fe9f45e..e97b3bf2ac 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user_bookmarks/list_user_bookmarks_result.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user_bookmarks/list_user_bookmarks_result.rs @@ -172,6 +172,7 @@ impl RawUserBookmarkRecord { MediaFileType::Fbx => Some("3d file".to_string()), MediaFileType::Glb => Some("3d file".to_string()), MediaFileType::Gltf => Some("3d file".to_string()), + MediaFileType::Spz => Some("3d splat".to_string()), MediaFileType::SceneRon => Some("3d scene".to_string()), MediaFileType::SceneJson => Some("3d scene".to_string()), MediaFileType::Pmd => Some("polygon data".to_string()), diff --git a/crates/schema/public/enums/src/by_table/media_files/media_file_type.rs b/crates/schema/public/enums/src/by_table/media_files/media_file_type.rs index 16f488627b..8e1e63c827 100644 --- a/crates/schema/public/enums/src/by_table/media_files/media_file_type.rs +++ b/crates/schema/public/enums/src/by_table/media_files/media_file_type.rs @@ -41,6 +41,11 @@ pub enum MediaFileType { /// glTF files (for Bevy) Gltf, + /// Spz Gaussian splats + /// This is a standard file format for Gaussian Splats, used by World Labs. + /// The mime type is application/gzip. + Spz, + /// Bevy's scene files (in RON; Rusty Object Notation) /// This will be replaced with another format in future versions of Bevy SceneRon, @@ -130,6 +135,7 @@ impl MediaFileType { Self::Fbx => "fbx", Self::Glb => "glb", Self::Gltf => "gltf", + Self::Spz => "spz", Self::SceneRon => "scene_ron", Self::SceneJson => "scene_json", Self::Pmd => "pmd", @@ -154,6 +160,7 @@ impl MediaFileType { "fbx" => Ok(Self::Fbx), "glb" => Ok(Self::Glb), "gltf" => Ok(Self::Gltf), + "spz" => Ok(Self::Spz), "scene_ron" => Ok(Self::SceneRon), "scene_json" => Ok(Self::SceneJson), "pmd" => Ok(Self::Pmd), @@ -181,6 +188,7 @@ impl MediaFileType { Self::Fbx, Self::Glb, Self::Gltf, + Self::Spz, Self::SceneRon, Self::SceneJson, Self::Pmd, @@ -237,6 +245,7 @@ mod tests { assert_serialization(MediaFileType::Fbx, "fbx"); assert_serialization(MediaFileType::Glb, "glb"); assert_serialization(MediaFileType::Gltf, "gltf"); + assert_serialization(MediaFileType::Spz, "spz"); assert_serialization(MediaFileType::SceneRon, "scene_ron"); assert_serialization(MediaFileType::SceneJson, "scene_json"); assert_serialization(MediaFileType::Pmd, "pmd"); @@ -264,6 +273,7 @@ mod tests { assert_eq!(MediaFileType::Fbx.to_str(), "fbx"); assert_eq!(MediaFileType::Glb.to_str(), "glb"); assert_eq!(MediaFileType::Gltf.to_str(), "gltf"); + assert_eq!(MediaFileType::Spz.to_str(), "spz"); assert_eq!(MediaFileType::SceneRon.to_str(), "scene_ron"); assert_eq!(MediaFileType::SceneJson.to_str(), "scene_json"); assert_eq!(MediaFileType::Pmd.to_str(), "pmd"); @@ -287,6 +297,7 @@ mod tests { assert_eq!(MediaFileType::from_str("fbx").unwrap(), MediaFileType::Fbx); assert_eq!(MediaFileType::from_str("glb").unwrap(), MediaFileType::Glb); assert_eq!(MediaFileType::from_str("gltf").unwrap(), MediaFileType::Gltf); + assert_eq!(MediaFileType::from_str("spz").unwrap(), MediaFileType::Spz); assert_eq!(MediaFileType::from_str("scene_ron").unwrap(), MediaFileType::SceneRon); assert_eq!(MediaFileType::from_str("scene_json").unwrap(), MediaFileType::SceneJson); assert_eq!(MediaFileType::from_str("pmd").unwrap(), MediaFileType::Pmd); @@ -309,7 +320,7 @@ mod tests { #[test] fn all_variants() { let mut variants = MediaFileType::all_variants(); - assert_eq!(variants.len(), 19); + assert_eq!(variants.len(), 20); assert_eq!(variants.pop_first(), Some(MediaFileType::Audio)); assert_eq!(variants.pop_first(), Some(MediaFileType::Image)); assert_eq!(variants.pop_first(), Some(MediaFileType::Video)); @@ -317,6 +328,7 @@ mod tests { assert_eq!(variants.pop_first(), Some(MediaFileType::Fbx)); assert_eq!(variants.pop_first(), Some(MediaFileType::Glb)); assert_eq!(variants.pop_first(), Some(MediaFileType::Gltf)); + assert_eq!(variants.pop_first(), Some(MediaFileType::Spz)); assert_eq!(variants.pop_first(), Some(MediaFileType::SceneRon)); assert_eq!(variants.pop_first(), Some(MediaFileType::SceneJson)); assert_eq!(variants.pop_first(), Some(MediaFileType::Pmd)); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_generic/process_upload_media_file.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_generic/process_upload_media_file.rs index 7bc921644a..634945374b 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_generic/process_upload_media_file.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_generic/process_upload_media_file.rs @@ -26,6 +26,8 @@ use crate::state::server_state::ServerState; // TODO(bt,2023-12-20): THIS CODE NEEDS CLEANUP. This has been cargo culted three+ times. // It's ridiculous and complicated. +const GZIP_MIME_TYPE : &str = "application/gzip"; + pub enum SuccessCase { MediaAlreadyUploaded { existing_media_file_token: MediaFileToken, @@ -168,9 +170,21 @@ pub async fn process_upload_media_file( let mut maybe_codec_name = None; let mut media_file_type = None; + let mut is_spz = false; + if let Some(mimetype) = maybe_mimetype.as_deref() { - if !allowed_mimetypes.contains(mimetype) { + // `.spz` files are Gaussian splats + let is_spz_extension = upload_media_request.file_name + .as_deref() + .map(|filename| filename.trim().to_ascii_lowercase().ends_with(".spz")) + .unwrap_or(false); + + is_spz = mimetype == GZIP_MIME_TYPE && is_spz_extension; + + let is_allowed_mimetype_bypass = is_spz; + + if !allowed_mimetypes.contains(mimetype) && !is_allowed_mimetype_bypass { // NB: Don't let our error message inject malicious strings let filtered_mimetype = mimetype .chars() @@ -240,6 +254,8 @@ pub async fn process_upload_media_file( "image/jpeg" => false, "image/png" => false, "image/webp" => false, + // Don't decode gzip, gaussian splats (spz) + "application/gzip" => false, _ => true, }; @@ -258,6 +274,10 @@ pub async fn process_upload_media_file( } } + if is_spz { + media_file_type = Some(MediaFileType::Spz); + } + if media_file_type.is_none() && maybe_mimetype.is_none() { // https://research.cs.wisc.edu/graphics/Courses/cs-838-1999/Jeff/BVH.html const BVH_HEADER : &[u8] = "HIERARCHY".as_bytes(); @@ -298,6 +318,7 @@ pub async fn process_upload_media_file( MediaFileType::Fbx => MediaFileClass::Dimensional, MediaFileType::Glb => MediaFileClass::Dimensional, MediaFileType::Gltf => MediaFileClass::Dimensional, + MediaFileType::Spz => MediaFileClass::Dimensional, MediaFileType::SceneRon => MediaFileClass::Dimensional, MediaFileType::SceneJson => MediaFileClass::Dimensional, MediaFileType::Pmd => MediaFileClass::Dimensional,