From c74ca625ffca3fd7db44c8764c89173667ae6141 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 10 Jul 2026 01:08:33 -0400 Subject: [PATCH] persistence endpoints --- ...e74ec1a2c47b69d7ce92a0d175f58dc4b9e63.json | 58 +++ ...7a7dc5ed34b0f0fa156845e3b4b805e397f9d.json | 12 + .../artcraft_api_defs/src/media_file/mod.rs | 3 +- .../src/media_file/project/mod.rs | 8 + .../project/update_editor_2d_project.rs | 21 + .../project/update_mood_board_project.rs | 21 + .../project/update_scene_3d_project.rs | 21 + .../project/update_video_timeline_project.rs | 21 + .../project/upload_new_editor_2d_project.rs | 16 + .../project/upload_new_mood_board_project.rs | 16 + .../project/upload_new_scene_3d_project.rs | 16 + .../upload_new_video_timeline_project.rs | 16 + .../src/queries/media_files/edit/mod.rs | 1 + .../update_project_media_file_contents.rs | 83 ++++ .../lookup_media_file_for_project_update.rs | 62 +++ .../src/queries/media_files/get/mod.rs | 3 +- .../list_user_bookmarks_result.rs | 3 + .../media_files/media_file_project_type.rs | 15 +- .../by_table/media_files/media_file_type.rs | 38 +- .../web/storyteller_web/src/docs/api_doc.rs | 39 ++ .../endpoints/media_files/upload/mod.rs | 1 + .../media_files/upload/project/mod.rs | 9 + .../upload/project/project_upload_shared.rs | 423 ++++++++++++++++++ .../update_editor_2d_project_handler.rs | 51 +++ .../update_mood_board_project_handler.rs | 51 +++ .../update_scene_3d_project_handler.rs | 51 +++ .../update_video_timeline_project_handler.rs | 51 +++ .../upload_new_editor_2d_project_handler.rs | 50 +++ .../upload_new_mood_board_project_handler.rs | 50 +++ .../upload_new_scene_3d_project_handler.rs | 50 +++ ...load_new_video_timeline_project_handler.rs | 50 +++ .../application_routes/media_files_routes.rs | 40 ++ 32 files changed, 1346 insertions(+), 4 deletions(-) create mode 100644 .sqlx/query-8dcd4bb064881f245beb39dd246e74ec1a2c47b69d7ce92a0d175f58dc4b9e63.json create mode 100644 .sqlx/query-a3a86db1b77af3973bb37751d797a7dc5ed34b0f0fa156845e3b4b805e397f9d.json create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_editor_2d_project.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_mood_board_project.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_scene_3d_project.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_video_timeline_project.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_editor_2d_project.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_mood_board_project.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_scene_3d_project.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_video_timeline_project.rs create mode 100644 crates/schema/database/mysql_queries/src/queries/media_files/edit/update_project_media_file_contents.rs create mode 100644 crates/schema/database/mysql_queries/src/queries/media_files/get/lookup_media_file_for_project_update.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/mod.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/project_upload_shared.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_editor_2d_project_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_mood_board_project_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_scene_3d_project_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_video_timeline_project_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_editor_2d_project_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_mood_board_project_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_scene_3d_project_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_video_timeline_project_handler.rs diff --git a/.sqlx/query-8dcd4bb064881f245beb39dd246e74ec1a2c47b69d7ce92a0d175f58dc4b9e63.json b/.sqlx/query-8dcd4bb064881f245beb39dd246e74ec1a2c47b69d7ce92a0d175f58dc4b9e63.json new file mode 100644 index 0000000000..7f2ef9a928 --- /dev/null +++ b/.sqlx/query-8dcd4bb064881f245beb39dd246e74ec1a2c47b69d7ce92a0d175f58dc4b9e63.json @@ -0,0 +1,58 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n maybe_creator_user_token as `maybe_creator_user_token: UserToken`,\n maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: AnonymousVisitorTrackingToken`,\n media_type as `media_type: MediaFileType`,\n maybe_project_type as `maybe_project_type: MediaFileProjectType`\nFROM media_files\nWHERE token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "maybe_creator_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 1, + "name": "maybe_creator_anonymous_visitor_token: AnonymousVisitorTrackingToken", + "type_info": { + "type": "VarString", + "flags": "BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "media_type: MediaFileType", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 64 + } + }, + { + "ordinal": 3, + "name": "maybe_project_type: MediaFileProjectType", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 128 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + true, + false, + true + ] + }, + "hash": "8dcd4bb064881f245beb39dd246e74ec1a2c47b69d7ce92a0d175f58dc4b9e63" +} diff --git a/.sqlx/query-a3a86db1b77af3973bb37751d797a7dc5ed34b0f0fa156845e3b4b805e397f9d.json b/.sqlx/query-a3a86db1b77af3973bb37751d797a7dc5ed34b0f0fa156845e3b4b805e397f9d.json new file mode 100644 index 0000000000..fa8e7e2919 --- /dev/null +++ b/.sqlx/query-a3a86db1b77af3973bb37751d797a7dc5ed34b0f0fa156845e3b4b805e397f9d.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nUPDATE media_files\nSET\n media_class = ?,\n media_type = ?,\n maybe_project_type = ?,\n\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n checksum_sha2 = ?,\n\n creator_ip_address = ?\nWHERE token = ?\nLIMIT 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "a3a86db1b77af3973bb37751d797a7dc5ed34b0f0fa156845e3b4b805e397f9d" +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/mod.rs index a6657bee6d..227e1a6b8b 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/mod.rs @@ -1,3 +1,4 @@ pub mod delete_media_file; pub mod list_batch_generated_media_files; -pub mod list_session_project_media_files; \ No newline at end of file +pub mod list_session_project_media_files; +pub mod project; \ No newline at end of file diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/mod.rs new file mode 100644 index 0000000000..3d0e0387cb --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/mod.rs @@ -0,0 +1,8 @@ +pub mod update_editor_2d_project; +pub mod update_mood_board_project; +pub mod update_scene_3d_project; +pub mod update_video_timeline_project; +pub mod upload_new_editor_2d_project; +pub mod upload_new_mood_board_project; +pub mod upload_new_scene_3d_project; +pub mod upload_new_video_timeline_project; diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_editor_2d_project.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_editor_2d_project.rs new file mode 100644 index 0000000000..acff812e82 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_editor_2d_project.rs @@ -0,0 +1,21 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::media_files::MediaFileToken; + +pub const UPDATE_EDITOR_2D_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/editor_2d/update/{token}"; + +/// For the URL PathInfo +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateEditor2dProjectPathInfo { + pub token: MediaFileToken, +} + +/// Response for overwriting an existing 2D editor document project. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateEditor2dProjectSuccessResponse { + pub success: bool, + + /// Token of the updated project media file (unchanged by the update). + pub media_file_token: MediaFileToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_mood_board_project.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_mood_board_project.rs new file mode 100644 index 0000000000..c33c6e9588 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_mood_board_project.rs @@ -0,0 +1,21 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::media_files::MediaFileToken; + +pub const UPDATE_MOOD_BOARD_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/mood_board/update/{token}"; + +/// For the URL PathInfo +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateMoodBoardProjectPathInfo { + pub token: MediaFileToken, +} + +/// Response for overwriting an existing mood board project. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateMoodBoardProjectSuccessResponse { + pub success: bool, + + /// Token of the updated project media file (unchanged by the update). + pub media_file_token: MediaFileToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_scene_3d_project.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_scene_3d_project.rs new file mode 100644 index 0000000000..3d401b7919 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_scene_3d_project.rs @@ -0,0 +1,21 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::media_files::MediaFileToken; + +pub const UPDATE_SCENE_3D_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/scene_3d/update/{token}"; + +/// For the URL PathInfo +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateScene3dProjectPathInfo { + pub token: MediaFileToken, +} + +/// Response for overwriting an existing 3D scene project. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateScene3dProjectSuccessResponse { + pub success: bool, + + /// Token of the updated project media file (unchanged by the update). + pub media_file_token: MediaFileToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_video_timeline_project.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_video_timeline_project.rs new file mode 100644 index 0000000000..93b7aae902 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/update_video_timeline_project.rs @@ -0,0 +1,21 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::media_files::MediaFileToken; + +pub const UPDATE_VIDEO_TIMELINE_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/video_timeline/update/{token}"; + +/// For the URL PathInfo +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateVideoTimelineProjectPathInfo { + pub token: MediaFileToken, +} + +/// Response for overwriting an existing video editor timeline project. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateVideoTimelineProjectSuccessResponse { + pub success: bool, + + /// Token of the updated project media file (unchanged by the update). + pub media_file_token: MediaFileToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_editor_2d_project.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_editor_2d_project.rs new file mode 100644 index 0000000000..9a5703f5c4 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_editor_2d_project.rs @@ -0,0 +1,16 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::media_files::MediaFileToken; + +pub const UPLOAD_NEW_EDITOR_2D_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/editor_2d/new"; + +/// Response for saving a new 2D editor document project. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UploadNewEditor2dProjectSuccessResponse { + pub success: bool, + + /// Token for the newly created project media file. Use it to query the + /// project and to save subsequent updates via the update endpoint. + pub media_file_token: MediaFileToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_mood_board_project.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_mood_board_project.rs new file mode 100644 index 0000000000..1191cfcee1 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_mood_board_project.rs @@ -0,0 +1,16 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::media_files::MediaFileToken; + +pub const UPLOAD_NEW_MOOD_BOARD_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/mood_board/new"; + +/// Response for saving a new mood board project. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UploadNewMoodBoardProjectSuccessResponse { + pub success: bool, + + /// Token for the newly created project media file. Use it to query the + /// project and to save subsequent updates via the update endpoint. + pub media_file_token: MediaFileToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_scene_3d_project.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_scene_3d_project.rs new file mode 100644 index 0000000000..fde4d873ac --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_scene_3d_project.rs @@ -0,0 +1,16 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::media_files::MediaFileToken; + +pub const UPLOAD_NEW_SCENE_3D_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/scene_3d/new"; + +/// Response for saving a new 3D scene project. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UploadNewScene3dProjectSuccessResponse { + pub success: bool, + + /// Token for the newly created project media file. Use it to query the + /// project and to save subsequent updates via the update endpoint. + pub media_file_token: MediaFileToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_video_timeline_project.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_video_timeline_project.rs new file mode 100644 index 0000000000..2eb2baf3c6 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/media_file/project/upload_new_video_timeline_project.rs @@ -0,0 +1,16 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::media_files::MediaFileToken; + +pub const UPLOAD_NEW_VIDEO_TIMELINE_PROJECT_URL_PATH: &str = "/v1/media_files/upload/project/video_timeline/new"; + +/// Response for saving a new video editor timeline project. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UploadNewVideoTimelineProjectSuccessResponse { + pub success: bool, + + /// Token for the newly created project media file. Use it to query the + /// project and to save subsequent updates via the update endpoint. + pub media_file_token: MediaFileToken, +} diff --git a/crates/schema/database/mysql_queries/src/queries/media_files/edit/mod.rs b/crates/schema/database/mysql_queries/src/queries/media_files/edit/mod.rs index 8d5b322019..c2e4827571 100644 --- a/crates/schema/database/mysql_queries/src/queries/media_files/edit/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/media_files/edit/mod.rs @@ -3,5 +3,6 @@ pub mod set_media_file_cover_image; pub mod update_media_file_animation_type; pub mod update_media_file_engine_category; pub mod update_media_file_stored_cloud_contents; +pub mod update_project_media_file_contents; pub mod update_media_file_visibility; pub mod update_media_file_visibility_transactional; \ No newline at end of file diff --git a/crates/schema/database/mysql_queries/src/queries/media_files/edit/update_project_media_file_contents.rs b/crates/schema/database/mysql_queries/src/queries/media_files/edit/update_project_media_file_contents.rs new file mode 100644 index 0000000000..44387e6fd1 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/media_files/edit/update_project_media_file_contents.rs @@ -0,0 +1,83 @@ +use std::marker::PhantomData; + +use sqlx::{Executor, MySql}; + +use enums::by_table::media_files::media_file_class::MediaFileClass; +use enums::by_table::media_files::media_file_project_type::MediaFileProjectType; +use enums::by_table::media_files::media_file_type::MediaFileType; +use tokens::tokens::media_files::MediaFileToken; + +pub struct UpdateProjectMediaFileContentsArgs<'e, 'c, E> +where + E: 'e + Executor<'c, Database = MySql>, +{ + pub media_file_token: &'e MediaFileToken, + + /// The class/type/project-type the record should hold after the update. + /// Also backfills legacy records that predate the `project` class. + pub media_class: MediaFileClass, + pub media_type: MediaFileType, + pub project_type: MediaFileProjectType, + + pub public_bucket_directory_hash: &'e str, + pub maybe_public_bucket_prefix: Option<&'e str>, + pub maybe_public_bucket_extension: Option<&'e str>, + + pub maybe_mime_type: Option<&'e str>, + pub file_size_bytes: u64, + pub sha256_checksum: &'e str, + + pub update_ip_address: &'e str, + + pub mysql_executor: E, + pub phantom: PhantomData<&'c E>, +} + +/// Point a project media file record at freshly uploaded bucket contents. +pub async fn update_project_media_file_contents<'e, 'c: 'e, E>( + args: UpdateProjectMediaFileContentsArgs<'e, 'c, E>, +) -> Result<(), sqlx::Error> +where + E: 'e + Executor<'c, Database = MySql>, +{ + sqlx::query!( + r#" +UPDATE media_files +SET + media_class = ?, + media_type = ?, + maybe_project_type = ?, + + public_bucket_directory_hash = ?, + maybe_public_bucket_prefix = ?, + maybe_public_bucket_extension = ?, + + maybe_mime_type = ?, + file_size_bytes = ?, + checksum_sha2 = ?, + + creator_ip_address = ? +WHERE token = ? +LIMIT 1 + "#, + args.media_class.to_str(), + args.media_type.to_str(), + args.project_type.to_str(), + + args.public_bucket_directory_hash, + args.maybe_public_bucket_prefix, + args.maybe_public_bucket_extension, + + args.maybe_mime_type, + args.file_size_bytes, + args.sha256_checksum, + + args.update_ip_address, + + args.media_file_token.as_str(), + ) + .execute(args.mysql_executor) + .await?; + + Ok(()) +} diff --git a/crates/schema/database/mysql_queries/src/queries/media_files/get/lookup_media_file_for_project_update.rs b/crates/schema/database/mysql_queries/src/queries/media_files/get/lookup_media_file_for_project_update.rs new file mode 100644 index 0000000000..9abc2d74c1 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/media_files/get/lookup_media_file_for_project_update.rs @@ -0,0 +1,62 @@ +use std::marker::PhantomData; + +use sqlx::{Executor, MySql}; + +use enums::by_table::media_files::media_file_project_type::MediaFileProjectType; +use enums::by_table::media_files::media_file_type::MediaFileType; +use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::users::UserToken; + +#[derive(Debug, Clone)] +pub struct MediaFileForProjectUpdate { + pub maybe_creator_user_token: Option, + pub maybe_creator_anonymous_visitor_token: Option, + pub media_type: MediaFileType, + pub maybe_project_type: Option, +} + +pub struct LookupMediaFileForProjectUpdateArgs<'e, 'c, E> +where + E: 'e + Executor<'c, Database = MySql>, +{ + pub media_file_token: &'e MediaFileToken, + pub mysql_executor: E, + pub phantom: PhantomData<&'c E>, +} + +/// Minimal lookup of a media file for a project-save (overwrite) request: +/// the creator tokens (for ownership checks) and the media/project types +/// (to confirm the record is the right kind of project document). +/// Excludes soft-deleted rows. +pub async fn lookup_media_file_for_project_update<'e, 'c: 'e, E>( + args: LookupMediaFileForProjectUpdateArgs<'e, 'c, E>, +) -> Result, sqlx::Error> +where + E: 'e + Executor<'c, Database = MySql>, +{ + let result = sqlx::query!( + r#" +SELECT + maybe_creator_user_token as `maybe_creator_user_token: UserToken`, + maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: AnonymousVisitorTrackingToken`, + media_type as `media_type: MediaFileType`, + maybe_project_type as `maybe_project_type: MediaFileProjectType` +FROM media_files +WHERE token = ? + AND user_deleted_at IS NULL + AND mod_deleted_at IS NULL +LIMIT 1 + "#, + args.media_file_token.as_str(), + ) + .fetch_optional(args.mysql_executor) + .await?; + + Ok(result.map(|record| MediaFileForProjectUpdate { + maybe_creator_user_token: record.maybe_creator_user_token, + maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token, + media_type: record.media_type, + maybe_project_type: record.maybe_project_type, + })) +} diff --git a/crates/schema/database/mysql_queries/src/queries/media_files/get/mod.rs b/crates/schema/database/mysql_queries/src/queries/media_files/get/mod.rs index f860ee36f4..5e128afe5c 100644 --- a/crates/schema/database/mysql_queries/src/queries/media_files/get/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/media_files/get/mod.rs @@ -3,4 +3,5 @@ pub mod batch_get_media_files; pub mod batch_get_media_files_by_tokens; pub mod get_media_file; pub mod get_media_file_for_inference; -pub mod lookup_media_file_for_cover_check; \ No newline at end of file +pub mod lookup_media_file_for_cover_check; +pub mod lookup_media_file_for_project_update; \ No newline at end of file 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 90a5e44082..c68f73c209 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 @@ -177,6 +177,9 @@ impl RawUserBookmarkRecord { MediaFileType::Spz => Some("3d splat".to_string()), MediaFileType::SceneRon => Some("3d scene".to_string()), MediaFileType::SceneJson => Some("3d scene".to_string()), + MediaFileType::MoodJson => Some("mood board".to_string()), + MediaFileType::TimelineJson => Some("video timeline".to_string()), + MediaFileType::EditorJson => Some("2d editor project".to_string()), MediaFileType::Pmd => Some("polygon data".to_string()), MediaFileType::Vmd => Some("motion data".to_string()), MediaFileType::Pmx => Some("pmx data".to_string()), diff --git a/crates/schema/public/enums/src/by_table/media_files/media_file_project_type.rs b/crates/schema/public/enums/src/by_table/media_files/media_file_project_type.rs index 1e9866d209..f020c50a44 100644 --- a/crates/schema/public/enums/src/by_table/media_files/media_file_project_type.rs +++ b/crates/schema/public/enums/src/by_table/media_files/media_file_project_type.rs @@ -34,6 +34,12 @@ pub enum MediaFileProjectType { /// Video editor project: a nonlinear video editing timeline. VideoTimeline, + + /// 2D editor project: a 2D image editing document. + /// NB: serde's snake_case renames this to "editor2d" (no underscore before + /// the digit), so the rename must be explicit. + #[serde(rename = "editor_2d")] + Editor2d, } impl_enum_display_and_debug_using_to_str!(MediaFileProjectType); @@ -47,6 +53,7 @@ impl MediaFileProjectType { Self::MoodBoard => "mood_board", Self::Workflow => "workflow", Self::VideoTimeline => "video_timeline", + Self::Editor2d => "editor_2d", } } @@ -56,6 +63,7 @@ impl MediaFileProjectType { "mood_board" => Ok(Self::MoodBoard), "workflow" => Ok(Self::Workflow), "video_timeline" => Ok(Self::VideoTimeline), + "editor_2d" => Ok(Self::Editor2d), _ => Err(format!("invalid value: {:?}", value)), } } @@ -67,6 +75,7 @@ impl MediaFileProjectType { Self::MoodBoard, Self::Workflow, Self::VideoTimeline, + Self::Editor2d, ]) } } @@ -85,6 +94,7 @@ mod tests { assert_serialization(MediaFileProjectType::MoodBoard, "mood_board"); assert_serialization(MediaFileProjectType::Workflow, "workflow"); assert_serialization(MediaFileProjectType::VideoTimeline, "video_timeline"); + assert_serialization(MediaFileProjectType::Editor2d, "editor_2d"); } } @@ -97,6 +107,7 @@ mod tests { assert_eq!(MediaFileProjectType::MoodBoard.to_str(), "mood_board"); assert_eq!(MediaFileProjectType::Workflow.to_str(), "workflow"); assert_eq!(MediaFileProjectType::VideoTimeline.to_str(), "video_timeline"); + assert_eq!(MediaFileProjectType::Editor2d.to_str(), "editor_2d"); } #[test] @@ -105,6 +116,7 @@ mod tests { assert_eq!(MediaFileProjectType::from_str("mood_board").unwrap(), MediaFileProjectType::MoodBoard); assert_eq!(MediaFileProjectType::from_str("workflow").unwrap(), MediaFileProjectType::Workflow); assert_eq!(MediaFileProjectType::from_str("video_timeline").unwrap(), MediaFileProjectType::VideoTimeline); + assert_eq!(MediaFileProjectType::from_str("editor_2d").unwrap(), MediaFileProjectType::Editor2d); assert!(MediaFileProjectType::from_str("foo").is_err()); } } @@ -115,11 +127,12 @@ mod tests { #[test] fn all_variants() { let mut variants = MediaFileProjectType::all_variants(); - assert_eq!(variants.len(), 4); + assert_eq!(variants.len(), 5); assert_eq!(variants.pop_first(), Some(MediaFileProjectType::Scene3d)); assert_eq!(variants.pop_first(), Some(MediaFileProjectType::MoodBoard)); assert_eq!(variants.pop_first(), Some(MediaFileProjectType::Workflow)); assert_eq!(variants.pop_first(), Some(MediaFileProjectType::VideoTimeline)); + assert_eq!(variants.pop_first(), Some(MediaFileProjectType::Editor2d)); assert_eq!(variants.pop_first(), None); } } 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 daa1dfac6d..52c3efd536 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 @@ -61,6 +61,15 @@ pub enum MediaFileType { /// Alternate scene files. SceneJson, + /// Mood board project documents (JSON). + MoodJson, + + /// Video editor timeline project documents (JSON). + TimelineJson, + + /// 2D editor project documents (JSON). + EditorJson, + /// "Polygon Model Data", character data for MikuMikuDance /// See: https://mikumikudance.fandom.com/wiki/MMD:Polygon_Model_Data Pmd, @@ -192,6 +201,9 @@ impl MediaFileType { Self::SceneRon | Self::SceneJson + | Self::MoodJson + | Self::TimelineJson + | Self::EditorJson | Self::Json => MediaFileClass::Project, } } @@ -293,6 +305,9 @@ impl MediaFileType { Self::Spz => "spz", Self::SceneRon => "scene_ron", Self::SceneJson => "scene_json", + Self::MoodJson => "mood_json", + Self::TimelineJson => "timeline_json", + Self::EditorJson => "editor_json", Self::Pmd => "pmd", Self::Vmd => "vmd", Self::Pmx => "pmx", @@ -329,6 +344,9 @@ impl MediaFileType { "spz" => Ok(Self::Spz), "scene_ron" => Ok(Self::SceneRon), "scene_json" => Ok(Self::SceneJson), + "mood_json" => Ok(Self::MoodJson), + "timeline_json" => Ok(Self::TimelineJson), + "editor_json" => Ok(Self::EditorJson), "pmd" => Ok(Self::Pmd), "vmd" => Ok(Self::Vmd), "pmx" => Ok(Self::Pmx), @@ -368,6 +386,9 @@ impl MediaFileType { Self::Spz, Self::SceneRon, Self::SceneJson, + Self::MoodJson, + Self::TimelineJson, + Self::EditorJson, Self::Pmd, Self::Vmd, Self::Pmx, @@ -437,6 +458,9 @@ mod tests { assert_serialization(MediaFileType::Spz, "spz"); assert_serialization(MediaFileType::SceneRon, "scene_ron"); assert_serialization(MediaFileType::SceneJson, "scene_json"); + assert_serialization(MediaFileType::MoodJson, "mood_json"); + assert_serialization(MediaFileType::TimelineJson, "timeline_json"); + assert_serialization(MediaFileType::EditorJson, "editor_json"); assert_serialization(MediaFileType::Pmd, "pmd"); assert_serialization(MediaFileType::Vmd, "vmd"); assert_serialization(MediaFileType::Pmx, "pmx"); @@ -474,6 +498,9 @@ mod tests { 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::MoodJson.to_str(), "mood_json"); + assert_eq!(MediaFileType::TimelineJson.to_str(), "timeline_json"); + assert_eq!(MediaFileType::EditorJson.to_str(), "editor_json"); assert_eq!(MediaFileType::Pmd.to_str(), "pmd"); assert_eq!(MediaFileType::Vmd.to_str(), "vmd"); assert_eq!(MediaFileType::Pmx.to_str(), "pmx"); @@ -507,6 +534,9 @@ mod tests { 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("mood_json").unwrap(), MediaFileType::MoodJson); + assert_eq!(MediaFileType::from_str("timeline_json").unwrap(), MediaFileType::TimelineJson); + assert_eq!(MediaFileType::from_str("editor_json").unwrap(), MediaFileType::EditorJson); assert_eq!(MediaFileType::from_str("pmd").unwrap(), MediaFileType::Pmd); assert_eq!(MediaFileType::from_str("vmd").unwrap(), MediaFileType::Vmd); assert_eq!(MediaFileType::from_str("pmx").unwrap(), MediaFileType::Pmx); @@ -543,6 +573,9 @@ mod tests { assert_eq!(MediaFileType::Ply.to_media_class(), MediaFileClass::Splat); assert_eq!(MediaFileType::SceneRon.to_media_class(), MediaFileClass::Project); assert_eq!(MediaFileType::SceneJson.to_media_class(), MediaFileClass::Project); + assert_eq!(MediaFileType::MoodJson.to_media_class(), MediaFileClass::Project); + assert_eq!(MediaFileType::TimelineJson.to_media_class(), MediaFileClass::Project); + assert_eq!(MediaFileType::EditorJson.to_media_class(), MediaFileClass::Project); assert_eq!(MediaFileType::Pmd.to_media_class(), MediaFileClass::Mesh); assert_eq!(MediaFileType::Vmd.to_media_class(), MediaFileClass::Dimensional); assert_eq!(MediaFileType::Pmx.to_media_class(), MediaFileClass::Mesh); @@ -705,7 +738,7 @@ mod tests { #[test] fn all_variants() { let mut variants = MediaFileType::all_variants(); - assert_eq!(variants.len(), 31); + assert_eq!(variants.len(), 34); assert_eq!(variants.pop_first(), Some(MediaFileType::Audio)); assert_eq!(variants.pop_first(), Some(MediaFileType::Image)); assert_eq!(variants.pop_first(), Some(MediaFileType::Video)); @@ -718,6 +751,9 @@ mod tests { 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::MoodJson)); + assert_eq!(variants.pop_first(), Some(MediaFileType::TimelineJson)); + assert_eq!(variants.pop_first(), Some(MediaFileType::EditorJson)); assert_eq!(variants.pop_first(), Some(MediaFileType::Pmd)); assert_eq!(variants.pop_first(), Some(MediaFileType::Vmd)); assert_eq!(variants.pop_first(), Some(MediaFileType::Pmx)); 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 21aac31553..524688cbc5 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -70,6 +70,15 @@ use crate::http_server::endpoints::media_files::upload::upload_generic::upload_m use crate::http_server::endpoints::media_files::upload::upload_image_media_file_handler::*; use crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_media_file_handler::*; use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::*; +use crate::http_server::endpoints::media_files::upload::project::update_mood_board_project_handler::*; +use crate::http_server::endpoints::media_files::upload::project::upload_new_mood_board_project_handler::*; +use crate::http_server::endpoints::media_files::upload::project::update_video_timeline_project_handler::*; +use crate::http_server::endpoints::media_files::upload::project::upload_new_video_timeline_project_handler::*; +use crate::http_server::endpoints::media_files::upload::project::update_editor_2d_project_handler::*; +use crate::http_server::endpoints::media_files::upload::project::upload_new_editor_2d_project_handler::*; +use crate::http_server::endpoints::media_files::upload::project::update_scene_3d_project_handler::*; +use crate::http_server::endpoints::media_files::upload::project::upload_new_scene_3d_project_handler::*; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{NewProjectMultipartForm, UpdateProjectMultipartForm}; 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::*; @@ -329,6 +338,14 @@ use artcraft_api_defs::credits::get_session_credits::*; use artcraft_api_defs::subscriptions::get_session_subscription::*; use artcraft_api_defs::media_file::list_batch_generated_media_files::*; use artcraft_api_defs::media_file::list_session_project_media_files::*; +use artcraft_api_defs::media_file::project::update_mood_board_project::*; +use artcraft_api_defs::media_file::project::upload_new_mood_board_project::*; +use artcraft_api_defs::media_file::project::update_video_timeline_project::*; +use artcraft_api_defs::media_file::project::upload_new_video_timeline_project::*; +use artcraft_api_defs::media_file::project::update_editor_2d_project::*; +use artcraft_api_defs::media_file::project::upload_new_editor_2d_project::*; +use artcraft_api_defs::media_file::project::update_scene_3d_project::*; +use artcraft_api_defs::media_file::project::upload_new_scene_3d_project::*; // Handler modules with locally-defined types use crate::http_server::endpoints::moderation::info::moderator_token_info_handler::*; use artcraft_api_defs::characters::create_character::*; @@ -470,6 +487,14 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux 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::project::upload_new_mood_board_project_handler::upload_new_mood_board_project_handler, + crate::http_server::endpoints::media_files::upload::project::update_mood_board_project_handler::update_mood_board_project_handler, + crate::http_server::endpoints::media_files::upload::project::upload_new_video_timeline_project_handler::upload_new_video_timeline_project_handler, + crate::http_server::endpoints::media_files::upload::project::update_video_timeline_project_handler::update_video_timeline_project_handler, + crate::http_server::endpoints::media_files::upload::project::upload_new_editor_2d_project_handler::upload_new_editor_2d_project_handler, + crate::http_server::endpoints::media_files::upload::project::update_editor_2d_project_handler::update_editor_2d_project_handler, + crate::http_server::endpoints::media_files::upload::project::upload_new_scene_3d_project_handler::upload_new_scene_3d_project_handler, + crate::http_server::endpoints::media_files::upload::project::update_scene_3d_project_handler::update_scene_3d_project_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, @@ -1112,6 +1137,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux ModeratorUserEmailChangeItem, ModeratorUserEmailChangeUserSummary, ModernInferenceQueueStats, + NewProjectMultipartForm, PinnedMediaFile, PinnedModelWeightForList, ProjectMediaFileInfo, @@ -1169,6 +1195,19 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux UploadMediaSuccessResponse, UploadNewEngineAssetFileForm, UploadNewEngineAssetSuccessResponse, + UpdateEditor2dProjectPathInfo, + UpdateProjectMultipartForm, + UpdateEditor2dProjectSuccessResponse, + UpdateMoodBoardProjectPathInfo, + UpdateMoodBoardProjectSuccessResponse, + UpdateScene3dProjectPathInfo, + UpdateScene3dProjectSuccessResponse, + UpdateVideoTimelineProjectPathInfo, + UpdateVideoTimelineProjectSuccessResponse, + UploadNewEditor2dProjectSuccessResponse, + UploadNewMoodBoardProjectSuccessResponse, + UploadNewScene3dProjectSuccessResponse, + UploadNewVideoTimelineProjectSuccessResponse, UploadNewSceneMediaFileForm, UploadNewSceneMediaFileSuccessResponse, UploadSpzMediaFileForm, 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 5c14a12e7e..014872848e 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 @@ -1,6 +1,7 @@ pub mod common_utils; pub mod upload_audio_media_file_handler; pub mod upload_error; +pub mod project; pub mod upload_generic; pub mod upload_image_media_file_handler; pub mod upload_new_engine_asset_media_file_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/mod.rs new file mode 100644 index 0000000000..5c9b1a89cd --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/mod.rs @@ -0,0 +1,9 @@ +pub mod project_upload_shared; +pub mod update_editor_2d_project_handler; +pub mod update_mood_board_project_handler; +pub mod update_scene_3d_project_handler; +pub mod update_video_timeline_project_handler; +pub mod upload_new_editor_2d_project_handler; +pub mod upload_new_mood_board_project_handler; +pub mod upload_new_scene_3d_project_handler; +pub mod upload_new_video_timeline_project_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/project_upload_shared.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/project_upload_shared.rs new file mode 100644 index 0000000000..b06a96e51f --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/project_upload_shared.rs @@ -0,0 +1,423 @@ +//! Shared machinery for the project save endpoints +//! (`/v1/media_files/upload/project/{kind}/new` and `.../update/{token}`). +//! +//! Each project kind (mood board, video timeline, 2D editor, 3D scene) gets a +//! thin handler that delegates here with its own [`ProjectUploadConfig`]. + +use std::io::Read; +use std::marker::PhantomData; + +use actix_multipart::form::tempfile::TempFile; +use actix_multipart::form::text::Text; +use actix_multipart::form::MultipartForm; +use actix_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_engine_category::MediaFileEngineCategory; +use enums::by_table::media_files::media_file_project_type::MediaFileProjectType; +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 mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::create::specialized_insert::insert_media_file_from_file_upload::{insert_media_file_from_file_upload, InsertMediaFileFromUploadArgs, UploadType}; +use mysql_queries::queries::media_files::edit::update_project_media_file_contents::{update_project_media_file_contents, UpdateProjectMediaFileContentsArgs}; +use mysql_queries::queries::media_files::get::lookup_media_file_for_project_update::{lookup_media_file_for_project_update, LookupMediaFileForProjectUpdateArgs}; +use tokens::tokens::media_files::MediaFileToken; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::state::server_state::ServerState; +use crate::util::check_creator_tokens::{check_creator_tokens, CheckCreatorTokenArgs, CheckCreatorTokenResult}; + +const PROJECT_MIMETYPE: &str = "application/json"; + +/// Static per-project-kind configuration for the save endpoints. +pub struct ProjectUploadConfig { + pub project_type: MediaFileProjectType, + pub media_file_type: MediaFileType, + pub maybe_engine_category: Option, + pub bucket_prefix: &'static str, + pub bucket_suffix: &'static str, +} + +pub const MOOD_BOARD_PROJECT_CONFIG: ProjectUploadConfig = ProjectUploadConfig { + project_type: MediaFileProjectType::MoodBoard, + media_file_type: MediaFileType::MoodJson, + maybe_engine_category: None, + bucket_prefix: "artcraft_", + bucket_suffix: ".mood.json", +}; + +pub const VIDEO_TIMELINE_PROJECT_CONFIG: ProjectUploadConfig = ProjectUploadConfig { + project_type: MediaFileProjectType::VideoTimeline, + media_file_type: MediaFileType::TimelineJson, + maybe_engine_category: None, + bucket_prefix: "artcraft_", + bucket_suffix: ".timeline.json", +}; + +pub const EDITOR_2D_PROJECT_CONFIG: ProjectUploadConfig = ProjectUploadConfig { + project_type: MediaFileProjectType::Editor2d, + media_file_type: MediaFileType::EditorJson, + maybe_engine_category: None, + bucket_prefix: "artcraft_", + bucket_suffix: ".editor.json", +}; + +pub const SCENE_3D_PROJECT_CONFIG: ProjectUploadConfig = ProjectUploadConfig { + project_type: MediaFileProjectType::Scene3d, + media_file_type: MediaFileType::SceneJson, + maybe_engine_category: Some(MediaFileEngineCategory::Scene), + bucket_prefix: "scene_", + bucket_suffix: ".scn.json", +}; + +/// Form-multipart request fields for saving a new project. +/// +/// IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `NewProjectMultipartForm` (Under "Schema") FOR DETAILS ON FIELDS AND NULLABILITY. +#[derive(MultipartForm, ToSchema)] +#[multipart(duplicate_field = "deny")] +pub struct NewProjectMultipartForm { + /// UUID for request idempotency + #[multipart(limit = "2 KiB")] + #[schema(value_type = String, format = Binary)] + pub uuid_idempotency_token: Text, + + /// The project JSON document + #[multipart(limit = "512 MiB")] + #[schema(value_type = Vec, format = Binary)] + pub file: TempFile, + + /// Optional: Title (name) of the project + #[multipart(limit = "2 KiB")] + #[schema(value_type = Option, format = Binary)] + pub maybe_title: Option>, + + /// Optional: Visibility of the project + #[multipart(limit = "2 KiB")] + #[schema(value_type = Option, format = Binary)] + pub maybe_visibility: Option>, +} + +/// Form-multipart request fields for overwriting an existing project. +/// +/// IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UpdateProjectMultipartForm` (Under "Schema") FOR DETAILS ON FIELDS AND NULLABILITY. +#[derive(MultipartForm, ToSchema)] +#[multipart(duplicate_field = "deny")] +pub struct UpdateProjectMultipartForm { + /// UUID for request idempotency + #[multipart(limit = "2 KiB")] + #[schema(value_type = String, format = Binary)] + pub uuid_idempotency_token: Text, + + /// The project JSON document + #[multipart(limit = "512 MiB")] + #[schema(value_type = Vec, format = Binary)] + pub file: TempFile, +} + +/// Save a brand new project media file: insert the record and upload the +/// document to the public bucket. Anonymous (logged-out) creators are allowed. +pub async fn save_new_project( + http_request: &HttpRequest, + server_state: &ServerState, + config: &ProjectUploadConfig, + mut form: NewProjectMultipartForm, +) -> Result { + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + error!("MySql pool error: {:?}", err); + CommonWebError::from_error(err) + })?; + + // ==================== SESSION (OPTIONAL) + BAN CHECK ==================== // + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(http_request, &mut mysql_connection) + .await + .map_err(|err| { + error!("Session checker error: {:?}", err); + CommonWebError::from_error(err) + })?; + + if let Some(ref user) = maybe_user_session { + if user.is_banned { + return Err(CommonWebError::NotAuthorized); + } + } + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(http_request); + + // ==================== IDEMPOTENCY ==================== // + + let uuid_idempotency_token = form.uuid_idempotency_token.as_ref(); + + if let Err(reason) = validate_idempotency_token_format(uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + warn!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid or duplicate idempotency token".to_string()) + })?; + + // NB: Release the connection before the bucket upload — never hold a pooled + // connection across a network call. + drop(mysql_connection); + + // ==================== 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()); + + let ip_address = get_request_ip(http_request); + + // ==================== FILE DATA + BUCKET UPLOAD ==================== // + + let (file_bytes, sha256_checksum) = read_and_hash_form_file(&mut form.file)?; + + let public_upload_path = upload_project_to_bucket(server_state, config, &file_bytes).await?; + + // ==================== SAVE RECORD ==================== // + + let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs { + maybe_media_class: Some(MediaFileClass::Project), + maybe_project_type: Some(config.project_type), + media_file_type: config.media_file_type, + maybe_creator_user_token: maybe_user_session.as_ref().map(|session| session.get_user_token()), + maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility, + maybe_prompt_token: None, + maybe_batch_token: None, + upload_type: UploadType::Filesystem, + maybe_engine_category: config.maybe_engine_category, + maybe_animation_type: None, + maybe_mime_type: Some(PROJECT_MIMETYPE), + file_size_bytes: file_bytes.len() as u64, + maybe_duration_millis: None, + sha256_checksum: &sha256_checksum, + maybe_title: maybe_title.as_deref(), + maybe_scene_source_media_file_token: None, + is_intermediate_system_file: false, // NB: is_user_upload = TRUE + maybe_generation_provider: None, + public_bucket_directory_hash: public_upload_path.get_object_hash(), + maybe_public_bucket_prefix: Some(config.bucket_prefix), + maybe_public_bucket_extension: Some(config.bucket_suffix), + pool: &server_state.mysql_pool, + }) + .await + .map_err(|err| { + warn!("New project media file insert error: {:?}", err); + CommonWebError::from_anyhow_error(err) + })?; + + info!("New {} project media file id: {} token: {:?}", + config.project_type, record_id, &token); + + Ok(token) +} + +/// Overwrite an existing project media file. Only the creator may update: +/// user-owned files require a matching user session; anonymously created +/// files require a matching anonymous visitor token. +pub async fn update_project( + http_request: &HttpRequest, + server_state: &ServerState, + config: &ProjectUploadConfig, + media_file_token: &MediaFileToken, + mut form: UpdateProjectMultipartForm, +) -> Result<(), CommonWebError> { + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + error!("MySql pool error: {:?}", err); + CommonWebError::from_error(err) + })?; + + // ==================== SESSION (OPTIONAL) + BAN CHECK ==================== // + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(http_request, &mut mysql_connection) + .await + .map_err(|err| { + error!("Session checker error: {:?}", err); + CommonWebError::from_error(err) + })?; + + if let Some(ref user) = maybe_user_session { + if user.is_banned { + return Err(CommonWebError::NotAuthorized); + } + } + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(http_request); + + // ==================== LOOK UP RECORD + CHECK OWNERSHIP ==================== // + + let media_file = lookup_media_file_for_project_update(LookupMediaFileForProjectUpdateArgs { + media_file_token, + mysql_executor: &mut *mysql_connection, + phantom: PhantomData, + }) + .await + .map_err(|err| { + error!("Error looking up media file for project update: {:?}", err); + CommonWebError::from_error(err) + })? + .ok_or(CommonWebError::NotFound)?; + + // Only overwrite records that are the right kind of project document. + // (Legacy records may predate `maybe_project_type`, so the media type is + // the source of truth; the project type just has to not disagree.) + let is_expected_media_type = media_file.media_type == config.media_file_type; + let project_type_disagrees = media_file.maybe_project_type + .is_some_and(|project_type| project_type != config.project_type); + + if !is_expected_media_type || project_type_disagrees { + return Err(CommonWebError::BadInputWithSimpleMessage(format!( + "media file is not a {} project", config.project_type))); + } + + let creator_check = check_creator_tokens(CheckCreatorTokenArgs { + maybe_creator_user_token: media_file.maybe_creator_user_token.as_ref(), + maybe_current_request_user_token: maybe_user_session.as_ref().map(|session| session.get_user_token()), + maybe_creator_anonymous_visitor_token: media_file.maybe_creator_anonymous_visitor_token.as_ref(), + maybe_current_request_anonymous_visitor_token: maybe_avt_token.as_ref(), + }); + + match creator_check { + CheckCreatorTokenResult::UserTokenMatch => {} // Allowed + CheckCreatorTokenResult::NoUserAnonymousVisitorTokenMatch => {} // Allowed + CheckCreatorTokenResult::UserTokenMismatch + | CheckCreatorTokenResult::NoUserAnonymousVisitorTokenMismatch + | CheckCreatorTokenResult::InsufficientInformation => { + return Err(CommonWebError::NotAuthorized); + } + } + + // ==================== IDEMPOTENCY ==================== // + + let uuid_idempotency_token = form.uuid_idempotency_token.as_ref(); + + if let Err(reason) = validate_idempotency_token_format(uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + warn!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("invalid or duplicate idempotency token".to_string()) + })?; + + // NB: Release the connection before the bucket upload — never hold a pooled + // connection across a network call. + drop(mysql_connection); + + // ==================== FILE DATA + BUCKET UPLOAD ==================== // + + let ip_address = get_request_ip(http_request); + + let (file_bytes, sha256_checksum) = read_and_hash_form_file(&mut form.file)?; + + // NB: Each save writes a fresh bucket object and repoints the record; old + // objects are left behind as cruft to clean up later (same as the legacy + // scene endpoints). + let public_upload_path = upload_project_to_bucket(server_state, config, &file_bytes).await?; + + // ==================== UPDATE RECORD ==================== // + + // NB: The pool is only handed over after the held connection is dropped + // above, so this acquires the request's single connection at a time. + update_project_media_file_contents(UpdateProjectMediaFileContentsArgs { + media_file_token, + media_class: MediaFileClass::Project, + media_type: config.media_file_type, + project_type: config.project_type, + public_bucket_directory_hash: public_upload_path.get_object_hash(), + maybe_public_bucket_prefix: Some(config.bucket_prefix), + maybe_public_bucket_extension: Some(config.bucket_suffix), + maybe_mime_type: Some(PROJECT_MIMETYPE), + file_size_bytes: file_bytes.len() as u64, + sha256_checksum: &sha256_checksum, + update_ip_address: &ip_address, + mysql_executor: &server_state.mysql_pool, + phantom: PhantomData, + }) + .await + .map_err(|err| { + warn!("Project media file update error: {:?}", err); + CommonWebError::from_error(err) + })?; + + Ok(()) +} + +fn read_and_hash_form_file(file: &mut TempFile) -> Result<(Vec, String), CommonWebError> { + let mut file_bytes = Vec::new(); + file.file.read_to_end(&mut file_bytes) + .map_err(|err| { + error!("Problem reading uploaded project file: {:?}", err); + CommonWebError::from_error(err) + })?; + + let sha256_checksum = sha256_hash_bytes(&file_bytes) + .map_err(|err| { + error!("Problem hashing uploaded project file: {:?}", err); + CommonWebError::from_anyhow_error(err) + })?; + + Ok((file_bytes, sha256_checksum)) +} + +async fn upload_project_to_bucket( + server_state: &ServerState, + config: &ProjectUploadConfig, + file_bytes: &[u8], +) -> Result { + let public_upload_path = MediaFileBucketPath::generate_new( + Some(config.bucket_prefix), Some(config.bucket_suffix)); + + info!("Uploading project media to bucket path: {}", + public_upload_path.get_full_object_path_str()); + + server_state.public_bucket_client.upload_file_with_content_type_process( + public_upload_path.get_full_object_path_str(), + file_bytes, + PROJECT_MIMETYPE) + .await + .map_err(|err| { + warn!("Upload project bytes to bucket error: {:?}", err); + CommonWebError::from_anyhow_error(err) + })?; + + Ok(public_upload_path) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_editor_2d_project_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_editor_2d_project_handler.rs new file mode 100644 index 0000000000..29fdb4dbcd --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_editor_2d_project_handler.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use actix_multipart::form::MultipartForm; +use actix_web::web::{Json, Path}; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::media_file::project::update_editor_2d_project::{UpdateEditor2dProjectPathInfo, UpdateEditor2dProjectSuccessResponse}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{update_project, UpdateProjectMultipartForm, EDITOR_2D_PROJECT_CONFIG}; +use crate::state::server_state::ServerState; + +/// Overwrite an existing 2D editor project (multipart JSON document upload). +/// +/// Only the project's creator may update it: user-owned projects require a +/// matching user session, and anonymously created projects require a matching +/// anonymous visitor token. To save a brand new copy instead, call the +/// corresponding "new" endpoint. +#[utoipa::path( + post, + tag = "Media Files (Projects)", + path = "/v1/media_files/upload/project/editor_2d/update/{token}", + responses( + (status = 200, description = "2d editor project updated", body = UpdateEditor2dProjectSuccessResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Not authorized"), + (status = 404, description = "Not found"), + (status = 500, description = "Server error"), + ), + params( + ("path" = UpdateEditor2dProjectPathInfo, description = "Path for Request"), + ( + "request" = UpdateProjectMultipartForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UpdateProjectMultipartForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn update_editor_2d_project_handler( + http_request: HttpRequest, + server_state: web::Data>, + path: Path, + MultipartForm(form): MultipartForm, +) -> Result, CommonWebError> { + update_project( + &http_request, &server_state, &EDITOR_2D_PROJECT_CONFIG, &path.token, form).await?; + + Ok(Json(UpdateEditor2dProjectSuccessResponse { + success: true, + media_file_token: path.into_inner().token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_mood_board_project_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_mood_board_project_handler.rs new file mode 100644 index 0000000000..6f074401bc --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_mood_board_project_handler.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use actix_multipart::form::MultipartForm; +use actix_web::web::{Json, Path}; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::media_file::project::update_mood_board_project::{UpdateMoodBoardProjectPathInfo, UpdateMoodBoardProjectSuccessResponse}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{update_project, UpdateProjectMultipartForm, MOOD_BOARD_PROJECT_CONFIG}; +use crate::state::server_state::ServerState; + +/// Overwrite an existing mood board project (multipart JSON document upload). +/// +/// Only the project's creator may update it: user-owned projects require a +/// matching user session, and anonymously created projects require a matching +/// anonymous visitor token. To save a brand new copy instead, call the +/// corresponding "new" endpoint. +#[utoipa::path( + post, + tag = "Media Files (Projects)", + path = "/v1/media_files/upload/project/mood_board/update/{token}", + responses( + (status = 200, description = "Mood board project updated", body = UpdateMoodBoardProjectSuccessResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Not authorized"), + (status = 404, description = "Not found"), + (status = 500, description = "Server error"), + ), + params( + ("path" = UpdateMoodBoardProjectPathInfo, description = "Path for Request"), + ( + "request" = UpdateProjectMultipartForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UpdateProjectMultipartForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn update_mood_board_project_handler( + http_request: HttpRequest, + server_state: web::Data>, + path: Path, + MultipartForm(form): MultipartForm, +) -> Result, CommonWebError> { + update_project( + &http_request, &server_state, &MOOD_BOARD_PROJECT_CONFIG, &path.token, form).await?; + + Ok(Json(UpdateMoodBoardProjectSuccessResponse { + success: true, + media_file_token: path.into_inner().token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_scene_3d_project_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_scene_3d_project_handler.rs new file mode 100644 index 0000000000..fa5b9ba53a --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_scene_3d_project_handler.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use actix_multipart::form::MultipartForm; +use actix_web::web::{Json, Path}; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::media_file::project::update_scene_3d_project::{UpdateScene3dProjectPathInfo, UpdateScene3dProjectSuccessResponse}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{update_project, UpdateProjectMultipartForm, SCENE_3D_PROJECT_CONFIG}; +use crate::state::server_state::ServerState; + +/// Overwrite an existing 3D scene project (multipart JSON document upload). +/// +/// Only the project's creator may update it: user-owned projects require a +/// matching user session, and anonymously created projects require a matching +/// anonymous visitor token. To save a brand new copy instead, call the +/// corresponding "new" endpoint. +#[utoipa::path( + post, + tag = "Media Files (Projects)", + path = "/v1/media_files/upload/project/scene_3d/update/{token}", + responses( + (status = 200, description = "3d scene project updated", body = UpdateScene3dProjectSuccessResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Not authorized"), + (status = 404, description = "Not found"), + (status = 500, description = "Server error"), + ), + params( + ("path" = UpdateScene3dProjectPathInfo, description = "Path for Request"), + ( + "request" = UpdateProjectMultipartForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UpdateProjectMultipartForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn update_scene_3d_project_handler( + http_request: HttpRequest, + server_state: web::Data>, + path: Path, + MultipartForm(form): MultipartForm, +) -> Result, CommonWebError> { + update_project( + &http_request, &server_state, &SCENE_3D_PROJECT_CONFIG, &path.token, form).await?; + + Ok(Json(UpdateScene3dProjectSuccessResponse { + success: true, + media_file_token: path.into_inner().token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_video_timeline_project_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_video_timeline_project_handler.rs new file mode 100644 index 0000000000..223f092621 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/update_video_timeline_project_handler.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use actix_multipart::form::MultipartForm; +use actix_web::web::{Json, Path}; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::media_file::project::update_video_timeline_project::{UpdateVideoTimelineProjectPathInfo, UpdateVideoTimelineProjectSuccessResponse}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{update_project, UpdateProjectMultipartForm, VIDEO_TIMELINE_PROJECT_CONFIG}; +use crate::state::server_state::ServerState; + +/// Overwrite an existing video editor timeline project (multipart JSON document upload). +/// +/// Only the project's creator may update it: user-owned projects require a +/// matching user session, and anonymously created projects require a matching +/// anonymous visitor token. To save a brand new copy instead, call the +/// corresponding "new" endpoint. +#[utoipa::path( + post, + tag = "Media Files (Projects)", + path = "/v1/media_files/upload/project/video_timeline/update/{token}", + responses( + (status = 200, description = "Video editor timeline project updated", body = UpdateVideoTimelineProjectSuccessResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Not authorized"), + (status = 404, description = "Not found"), + (status = 500, description = "Server error"), + ), + params( + ("path" = UpdateVideoTimelineProjectPathInfo, description = "Path for Request"), + ( + "request" = UpdateProjectMultipartForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `UpdateProjectMultipartForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn update_video_timeline_project_handler( + http_request: HttpRequest, + server_state: web::Data>, + path: Path, + MultipartForm(form): MultipartForm, +) -> Result, CommonWebError> { + update_project( + &http_request, &server_state, &VIDEO_TIMELINE_PROJECT_CONFIG, &path.token, form).await?; + + Ok(Json(UpdateVideoTimelineProjectSuccessResponse { + success: true, + media_file_token: path.into_inner().token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_editor_2d_project_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_editor_2d_project_handler.rs new file mode 100644 index 0000000000..cb3cbd44bd --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_editor_2d_project_handler.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use actix_multipart::form::MultipartForm; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::media_file::project::upload_new_editor_2d_project::UploadNewEditor2dProjectSuccessResponse; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{save_new_project, NewProjectMultipartForm, EDITOR_2D_PROJECT_CONFIG}; +use crate::state::server_state::ServerState; + +/// Save a new 2D editor project (multipart JSON document upload). +/// +/// You'll get back a media file token for accessing and querying the project +/// in the future. To save further changes to the same project, call the +/// corresponding update endpoint — only call this endpoint again if the user +/// wants to save a brand new copy. +/// +/// Anonymous (logged-out) users may also save projects. +#[utoipa::path( + post, + tag = "Media Files (Projects)", + path = "/v1/media_files/upload/project/editor_2d/new", + responses( + (status = 200, description = "New 2D editor project saved", body = UploadNewEditor2dProjectSuccessResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Not authorized"), + (status = 500, description = "Server error"), + ), + params( + ( + "request" = NewProjectMultipartForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `NewProjectMultipartForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn upload_new_editor_2d_project_handler( + http_request: HttpRequest, + server_state: web::Data>, + MultipartForm(form): MultipartForm, +) -> Result, CommonWebError> { + let media_file_token = save_new_project( + &http_request, &server_state, &EDITOR_2D_PROJECT_CONFIG, form).await?; + + Ok(Json(UploadNewEditor2dProjectSuccessResponse { + success: true, + media_file_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_mood_board_project_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_mood_board_project_handler.rs new file mode 100644 index 0000000000..91cec6f4d2 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_mood_board_project_handler.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use actix_multipart::form::MultipartForm; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::media_file::project::upload_new_mood_board_project::UploadNewMoodBoardProjectSuccessResponse; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{save_new_project, NewProjectMultipartForm, MOOD_BOARD_PROJECT_CONFIG}; +use crate::state::server_state::ServerState; + +/// Save a new mood board project (multipart JSON document upload). +/// +/// You'll get back a media file token for accessing and querying the project +/// in the future. To save further changes to the same project, call the +/// corresponding update endpoint — only call this endpoint again if the user +/// wants to save a brand new copy. +/// +/// Anonymous (logged-out) users may also save projects. +#[utoipa::path( + post, + tag = "Media Files (Projects)", + path = "/v1/media_files/upload/project/mood_board/new", + responses( + (status = 200, description = "New mood board project saved", body = UploadNewMoodBoardProjectSuccessResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Not authorized"), + (status = 500, description = "Server error"), + ), + params( + ( + "request" = NewProjectMultipartForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `NewProjectMultipartForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn upload_new_mood_board_project_handler( + http_request: HttpRequest, + server_state: web::Data>, + MultipartForm(form): MultipartForm, +) -> Result, CommonWebError> { + let media_file_token = save_new_project( + &http_request, &server_state, &MOOD_BOARD_PROJECT_CONFIG, form).await?; + + Ok(Json(UploadNewMoodBoardProjectSuccessResponse { + success: true, + media_file_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_scene_3d_project_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_scene_3d_project_handler.rs new file mode 100644 index 0000000000..de68fb10d0 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_scene_3d_project_handler.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use actix_multipart::form::MultipartForm; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::media_file::project::upload_new_scene_3d_project::UploadNewScene3dProjectSuccessResponse; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{save_new_project, NewProjectMultipartForm, SCENE_3D_PROJECT_CONFIG}; +use crate::state::server_state::ServerState; + +/// Save a new 3D scene project (multipart JSON document upload). +/// +/// You'll get back a media file token for accessing and querying the project +/// in the future. To save further changes to the same project, call the +/// corresponding update endpoint — only call this endpoint again if the user +/// wants to save a brand new copy. +/// +/// Anonymous (logged-out) users may also save projects. +#[utoipa::path( + post, + tag = "Media Files (Projects)", + path = "/v1/media_files/upload/project/scene_3d/new", + responses( + (status = 200, description = "New 3D scene project saved", body = UploadNewScene3dProjectSuccessResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Not authorized"), + (status = 500, description = "Server error"), + ), + params( + ( + "request" = NewProjectMultipartForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `NewProjectMultipartForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn upload_new_scene_3d_project_handler( + http_request: HttpRequest, + server_state: web::Data>, + MultipartForm(form): MultipartForm, +) -> Result, CommonWebError> { + let media_file_token = save_new_project( + &http_request, &server_state, &SCENE_3D_PROJECT_CONFIG, form).await?; + + Ok(Json(UploadNewScene3dProjectSuccessResponse { + success: true, + media_file_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_video_timeline_project_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_video_timeline_project_handler.rs new file mode 100644 index 0000000000..c951b05bb7 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/project/upload_new_video_timeline_project_handler.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use actix_multipart::form::MultipartForm; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::media_file::project::upload_new_video_timeline_project::UploadNewVideoTimelineProjectSuccessResponse; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::upload::project::project_upload_shared::{save_new_project, NewProjectMultipartForm, VIDEO_TIMELINE_PROJECT_CONFIG}; +use crate::state::server_state::ServerState; + +/// Save a new video editor timeline project (multipart JSON document upload). +/// +/// You'll get back a media file token for accessing and querying the project +/// in the future. To save further changes to the same project, call the +/// corresponding update endpoint — only call this endpoint again if the user +/// wants to save a brand new copy. +/// +/// Anonymous (logged-out) users may also save projects. +#[utoipa::path( + post, + tag = "Media Files (Projects)", + path = "/v1/media_files/upload/project/video_timeline/new", + responses( + (status = 200, description = "New video editor timeline project saved", body = UploadNewVideoTimelineProjectSuccessResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Not authorized"), + (status = 500, description = "Server error"), + ), + params( + ( + "request" = NewProjectMultipartForm, + description = "IF VIEWING DOCS, PLEASE SEE BOTTOM OF PAGE `NewProjectMultipartForm` (Under 'Schema') FOR DETAILS ON FIELDS AND NULLABILITY." + ), + ) +)] +pub async fn upload_new_video_timeline_project_handler( + http_request: HttpRequest, + server_state: web::Data>, + MultipartForm(form): MultipartForm, +) -> Result, CommonWebError> { + let media_file_token = save_new_project( + &http_request, &server_state, &VIDEO_TIMELINE_PROJECT_CONFIG, form).await?; + + Ok(Json(UploadNewVideoTimelineProjectSuccessResponse { + success: true, + media_file_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 d93fcc2977..351d6a24df 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 @@ -26,6 +26,14 @@ use crate::http_server::endpoints::media_files::upload::upload_generic::upload_m use crate::http_server::endpoints::media_files::upload::upload_image_media_file_handler::upload_image_media_file_handler; use crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_media_file_handler::upload_new_engine_asset_media_file_handler; use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::upload_new_scene_media_file_handler; +use crate::http_server::endpoints::media_files::upload::project::update_mood_board_project_handler::update_mood_board_project_handler; +use crate::http_server::endpoints::media_files::upload::project::upload_new_mood_board_project_handler::upload_new_mood_board_project_handler; +use crate::http_server::endpoints::media_files::upload::project::update_video_timeline_project_handler::update_video_timeline_project_handler; +use crate::http_server::endpoints::media_files::upload::project::upload_new_video_timeline_project_handler::upload_new_video_timeline_project_handler; +use crate::http_server::endpoints::media_files::upload::project::update_editor_2d_project_handler::update_editor_2d_project_handler; +use crate::http_server::endpoints::media_files::upload::project::upload_new_editor_2d_project_handler::upload_new_editor_2d_project_handler; +use crate::http_server::endpoints::media_files::upload::project::update_scene_3d_project_handler::update_scene_3d_project_handler; +use crate::http_server::endpoints::media_files::upload::project::upload_new_scene_3d_project_handler::upload_new_scene_3d_project_handler; 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; @@ -136,6 +144,38 @@ pub fn add_media_file_routes (app: App) -> App .route(web::post().to(upload_new_engine_asset_media_file_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/upload/project/mood_board/new") + .route(web::post().to(upload_new_mood_board_project_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/upload/project/mood_board/update/{token}") + .route(web::post().to(update_mood_board_project_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/upload/project/video_timeline/new") + .route(web::post().to(upload_new_video_timeline_project_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/upload/project/video_timeline/update/{token}") + .route(web::post().to(update_video_timeline_project_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/upload/project/editor_2d/new") + .route(web::post().to(upload_new_editor_2d_project_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/upload/project/editor_2d/update/{token}") + .route(web::post().to(update_editor_2d_project_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/upload/project/scene_3d/new") + .route(web::post().to(upload_new_scene_3d_project_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/upload/project/scene_3d/update/{token}") + .route(web::post().to(update_scene_3d_project_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .service(web::resource("/upload/new_scene") .route(web::post().to(upload_new_scene_media_file_handler)) .route(web::head().to(|| HttpResponse::Ok()))