8 new persistence endpoints, 2 for each type (#1766)

This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-07-10 01:32:23 -04:00
1 parent 87f35f3e6f
commit 1dfb62454a
36 files changed
+1454 -4

No files matched your search

@@ -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"
}
@@ -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"
}
@@ -1,3 +1,4 @@
pub mod delete_media_file;
pub mod list_batch_generated_media_files;
pub mod list_session_project_media_files;
pub mod list_session_project_media_files;
pub mod project;
@@ -0,0 +1,8 @@
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;
pub mod upload_updated_editor_2d_project;
pub mod upload_updated_mood_board_project;
pub mod upload_updated_scene_3d_project;
pub mod upload_updated_video_timeline_project;
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -0,0 +1,21 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_UPDATED_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 UploadUpdatedEditor2dProjectPathInfo {
pub token: MediaFileToken,
}
/// Response for overwriting an existing 2D editor project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedEditor2dProjectSuccessResponse {
pub success: bool,
/// Token of the updated project media file (unchanged by the update).
pub media_file_token: MediaFileToken,
}
@@ -0,0 +1,21 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_UPDATED_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 UploadUpdatedMoodBoardProjectPathInfo {
pub token: MediaFileToken,
}
/// Response for overwriting an existing mood board project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedMoodBoardProjectSuccessResponse {
pub success: bool,
/// Token of the updated project media file (unchanged by the update).
pub media_file_token: MediaFileToken,
}
@@ -0,0 +1,21 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_UPDATED_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 UploadUpdatedScene3dProjectPathInfo {
pub token: MediaFileToken,
}
/// Response for overwriting an existing 3D scene project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedScene3dProjectSuccessResponse {
pub success: bool,
/// Token of the updated project media file (unchanged by the update).
pub media_file_token: MediaFileToken,
}
@@ -0,0 +1,21 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
pub const UPLOAD_UPDATED_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 UploadUpdatedVideoTimelineProjectPathInfo {
pub token: MediaFileToken,
}
/// Response for overwriting an existing video editor timeline project.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UploadUpdatedVideoTimelineProjectSuccessResponse {
pub success: bool,
/// Token of the updated project media file (unchanged by the update).
pub media_file_token: MediaFileToken,
}
@@ -26,6 +26,12 @@ pub enum UploadType {
Filesystem,
DeviceCaptureApi,
StorytellerEngine,
/// Internal project documents (scenes, mood boards, timelines, etc.)
/// created by our apps and uploaded by the client. Stored with origin
/// `upload`; the project nature is captured by `media_class = 'project'`
/// and `maybe_project_type`.
ProjectFile,
}
pub struct InsertMediaFileFromUploadArgs<'a> {
@@ -78,6 +84,7 @@ pub async fn insert_media_file_from_file_upload(
let mut origin_category = match args.upload_type {
UploadType::ThirdPartyInference => MediaFileOriginCategory::ThirdPartyInference,
UploadType::Filesystem => MediaFileOriginCategory::Upload,
UploadType::ProjectFile => MediaFileOriginCategory::Upload,
UploadType::DeviceCaptureApi => MediaFileOriginCategory::DeviceApi,
UploadType::StorytellerEngine => MediaFileOriginCategory::StorytellerStudio,
};
@@ -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;
@@ -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(())
}
@@ -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<UserToken>,
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
pub media_type: MediaFileType,
pub maybe_project_type: Option<MediaFileProjectType>,
}
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<Option<MediaFileForProjectUpdate>, 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,
}))
}
@@ -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;
pub mod lookup_media_file_for_cover_check;
pub mod lookup_media_file_for_project_update;
@@ -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()),
@@ -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);
}
}
@@ -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));
@@ -70,6 +70,16 @@ 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::upload_updated_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::upload_updated_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::upload_updated_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::upload_updated_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::save_new_project::NewProjectMultipartForm;
use crate::http_server::endpoints::media_files::upload::project::update_project::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 +339,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::upload_updated_mood_board_project::*;
use artcraft_api_defs::media_file::project::upload_new_mood_board_project::*;
use artcraft_api_defs::media_file::project::upload_updated_video_timeline_project::*;
use artcraft_api_defs::media_file::project::upload_new_video_timeline_project::*;
use artcraft_api_defs::media_file::project::upload_updated_editor_2d_project::*;
use artcraft_api_defs::media_file::project::upload_new_editor_2d_project::*;
use artcraft_api_defs::media_file::project::upload_updated_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 +488,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::upload_updated_mood_board_project_handler::upload_updated_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::upload_updated_video_timeline_project_handler::upload_updated_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::upload_updated_editor_2d_project_handler::upload_updated_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::upload_updated_scene_3d_project_handler::upload_updated_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 +1138,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ModeratorUserEmailChangeItem,
ModeratorUserEmailChangeUserSummary,
ModernInferenceQueueStats,
NewProjectMultipartForm,
PinnedMediaFile,
PinnedModelWeightForList,
ProjectMediaFileInfo,
@@ -1169,6 +1196,19 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
UploadMediaSuccessResponse,
UploadNewEngineAssetFileForm,
UploadNewEngineAssetSuccessResponse,
UploadUpdatedEditor2dProjectPathInfo,
UpdateProjectMultipartForm,
UploadUpdatedEditor2dProjectSuccessResponse,
UploadUpdatedMoodBoardProjectPathInfo,
UploadUpdatedMoodBoardProjectSuccessResponse,
UploadUpdatedScene3dProjectPathInfo,
UploadUpdatedScene3dProjectSuccessResponse,
UploadUpdatedVideoTimelineProjectPathInfo,
UploadUpdatedVideoTimelineProjectSuccessResponse,
UploadNewEditor2dProjectSuccessResponse,
UploadNewMoodBoardProjectSuccessResponse,
UploadNewScene3dProjectSuccessResponse,
UploadNewVideoTimelineProjectSuccessResponse,
UploadNewSceneMediaFileForm,
UploadNewSceneMediaFileSuccessResponse,
UploadSpzMediaFileForm,
@@ -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;
@@ -0,0 +1,12 @@
pub mod project_upload_config;
pub mod project_upload_helpers;
pub mod save_new_project;
pub mod update_project;
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;
pub mod upload_updated_editor_2d_project_handler;
pub mod upload_updated_mood_board_project_handler;
pub mod upload_updated_scene_3d_project_handler;
pub mod upload_updated_video_timeline_project_handler;
@@ -0,0 +1,42 @@
//! Static per-project-kind configuration for the project save endpoints
//! (`/v1/media_files/upload/project/{kind}/new` and `.../update/{token}`).
use enums::by_table::media_files::media_file_project_type::MediaFileProjectType;
use enums::by_table::media_files::media_file_type::MediaFileType;
pub const PROJECT_MIMETYPE: &str = "application/json";
pub struct ProjectUploadConfig {
pub project_type: MediaFileProjectType,
pub media_file_type: MediaFileType,
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,
bucket_prefix: "artcraft_",
bucket_suffix: ".mood.json",
};
pub const VIDEO_TIMELINE_PROJECT_CONFIG: ProjectUploadConfig = ProjectUploadConfig {
project_type: MediaFileProjectType::VideoTimeline,
media_file_type: MediaFileType::TimelineJson,
bucket_prefix: "artcraft_",
bucket_suffix: ".timeline.json",
};
pub const EDITOR_2D_PROJECT_CONFIG: ProjectUploadConfig = ProjectUploadConfig {
project_type: MediaFileProjectType::Editor2d,
media_file_type: MediaFileType::EditorJson,
bucket_prefix: "artcraft_",
bucket_suffix: ".editor.json",
};
pub const SCENE_3D_PROJECT_CONFIG: ProjectUploadConfig = ProjectUploadConfig {
project_type: MediaFileProjectType::Scene3d,
media_file_type: MediaFileType::SceneJson,
bucket_prefix: "scene_",
bucket_suffix: ".scn.json",
};
@@ -0,0 +1,55 @@
//! Small helpers shared by [`super::save_new_project`] and
//! [`super::update_project`].
use std::io::Read;
use actix_multipart::form::tempfile::TempFile;
use log::{error, info, warn};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::media_files::upload::project::project_upload_config::{ProjectUploadConfig, PROJECT_MIMETYPE};
use crate::state::server_state::ServerState;
pub(super) fn read_and_hash_form_file(file: &mut TempFile) -> Result<(Vec<u8>, 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))
}
pub(super) async fn upload_project_to_bucket(
server_state: &ServerState,
config: &ProjectUploadConfig,
file_bytes: &[u8],
) -> Result<MediaFileBucketPath, CommonWebError> {
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)
}
@@ -0,0 +1,176 @@
//! Core logic for the "save new project" endpoints
//! (`/v1/media_files/upload/project/{kind}/new`).
//!
//! Each project kind (mood board, video timeline, 2D editor, 3D scene) gets a
//! thin handler that delegates here with its own
//! [`super::project_upload_config::ProjectUploadConfig`].
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 enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::common::visibility::Visibility;
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 tokens::tokens::media_files::MediaFileToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::media_files::upload::project::project_upload_config::{ProjectUploadConfig, PROJECT_MIMETYPE};
use crate::http_server::endpoints::media_files::upload::project::project_upload_helpers::{read_and_hash_form_file, upload_project_to_bucket};
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
/// 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<String>,
/// The project JSON document
#[multipart(limit = "512 MiB")]
#[schema(value_type = Vec<u8>, format = Binary)]
pub file: TempFile,
/// Optional: Title (name) of the project
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
pub maybe_title: Option<Text<String>>,
/// Optional: Visibility of the project
#[multipart(limit = "2 KiB")]
#[schema(value_type = Option<String>, format = Binary)]
pub maybe_visibility: Option<Text<Visibility>>,
}
pub struct SaveNewProjectArgs<'a> {
pub http_request: &'a HttpRequest,
pub server_state: &'a ServerState,
pub config: &'a ProjectUploadConfig,
pub form: NewProjectMultipartForm,
}
/// 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(args: SaveNewProjectArgs<'_>) -> Result<MediaFileToken, CommonWebError> {
let SaveNewProjectArgs { http_request, server_state, config, mut form } = args;
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::ProjectFile,
maybe_engine_category: None,
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)
}
@@ -0,0 +1,203 @@
//! Core logic for the "upload updated project" endpoints
//! (`/v1/media_files/upload/project/{kind}/update/{token}`).
//!
//! Each project kind (mood board, video timeline, 2D editor, 3D scene) gets a
//! thin handler that delegates here with its own
//! [`super::project_upload_config::ProjectUploadConfig`].
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, warn};
use utoipa::ToSchema;
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 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::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::endpoints::media_files::upload::project::project_upload_config::{ProjectUploadConfig, PROJECT_MIMETYPE};
use crate::http_server::endpoints::media_files::upload::project::project_upload_helpers::{read_and_hash_form_file, upload_project_to_bucket};
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};
/// 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<String>,
/// The project JSON document
#[multipart(limit = "512 MiB")]
#[schema(value_type = Vec<u8>, format = Binary)]
pub file: TempFile,
}
pub struct UpdateProjectArgs<'a> {
pub http_request: &'a HttpRequest,
pub server_state: &'a ServerState,
pub config: &'a ProjectUploadConfig,
pub media_file_token: &'a MediaFileToken,
pub form: UpdateProjectMultipartForm,
}
/// 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(args: UpdateProjectArgs<'_>) -> Result<(), CommonWebError> {
let UpdateProjectArgs { http_request, server_state, config, media_file_token, mut form } = args;
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.
match media_file.maybe_project_type {
// Modern records declare their project type; it must match exactly.
Some(project_type) => {
if project_type != config.project_type {
return Err(CommonWebError::BadInputWithSimpleMessage(format!(
"media file is not a {} project", config.project_type)));
}
}
// Legacy records predate `maybe_project_type`. The only legacy project
// documents are 3D scenes, so only the scene_3d endpoint may adopt them,
// and the record must actually be a JSON scene file.
None => {
let is_legacy_scene_update = config.project_type == MediaFileProjectType::Scene3d
&& media_file.media_type == MediaFileType::SceneJson;
if !is_legacy_scene_update {
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(())
}
@@ -0,0 +1,55 @@
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_config::EDITOR_2D_PROJECT_CONFIG;
use crate::http_server::endpoints::media_files::upload::project::save_new_project::{save_new_project, NewProjectMultipartForm, SaveNewProjectArgs};
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<Arc<ServerState>>,
MultipartForm(form): MultipartForm<NewProjectMultipartForm>,
) -> Result<Json<UploadNewEditor2dProjectSuccessResponse>, CommonWebError> {
let media_file_token = save_new_project(SaveNewProjectArgs {
http_request: &http_request,
server_state: &server_state,
config: &EDITOR_2D_PROJECT_CONFIG,
form,
}).await?;
Ok(Json(UploadNewEditor2dProjectSuccessResponse {
success: true,
media_file_token,
}))
}
@@ -0,0 +1,55 @@
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_config::MOOD_BOARD_PROJECT_CONFIG;
use crate::http_server::endpoints::media_files::upload::project::save_new_project::{save_new_project, NewProjectMultipartForm, SaveNewProjectArgs};
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<Arc<ServerState>>,
MultipartForm(form): MultipartForm<NewProjectMultipartForm>,
) -> Result<Json<UploadNewMoodBoardProjectSuccessResponse>, CommonWebError> {
let media_file_token = save_new_project(SaveNewProjectArgs {
http_request: &http_request,
server_state: &server_state,
config: &MOOD_BOARD_PROJECT_CONFIG,
form,
}).await?;
Ok(Json(UploadNewMoodBoardProjectSuccessResponse {
success: true,
media_file_token,
}))
}
@@ -0,0 +1,55 @@
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_config::SCENE_3D_PROJECT_CONFIG;
use crate::http_server::endpoints::media_files::upload::project::save_new_project::{save_new_project, NewProjectMultipartForm, SaveNewProjectArgs};
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<Arc<ServerState>>,
MultipartForm(form): MultipartForm<NewProjectMultipartForm>,
) -> Result<Json<UploadNewScene3dProjectSuccessResponse>, CommonWebError> {
let media_file_token = save_new_project(SaveNewProjectArgs {
http_request: &http_request,
server_state: &server_state,
config: &SCENE_3D_PROJECT_CONFIG,
form,
}).await?;
Ok(Json(UploadNewScene3dProjectSuccessResponse {
success: true,
media_file_token,
}))
}
@@ -0,0 +1,55 @@
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_config::VIDEO_TIMELINE_PROJECT_CONFIG;
use crate::http_server::endpoints::media_files::upload::project::save_new_project::{save_new_project, NewProjectMultipartForm, SaveNewProjectArgs};
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<Arc<ServerState>>,
MultipartForm(form): MultipartForm<NewProjectMultipartForm>,
) -> Result<Json<UploadNewVideoTimelineProjectSuccessResponse>, CommonWebError> {
let media_file_token = save_new_project(SaveNewProjectArgs {
http_request: &http_request,
server_state: &server_state,
config: &VIDEO_TIMELINE_PROJECT_CONFIG,
form,
}).await?;
Ok(Json(UploadNewVideoTimelineProjectSuccessResponse {
success: true,
media_file_token,
}))
}
@@ -0,0 +1,57 @@
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::upload_updated_editor_2d_project::{UploadUpdatedEditor2dProjectPathInfo, UploadUpdatedEditor2dProjectSuccessResponse};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::media_files::upload::project::project_upload_config::EDITOR_2D_PROJECT_CONFIG;
use crate::http_server::endpoints::media_files::upload::project::update_project::{update_project, UpdateProjectArgs, UpdateProjectMultipartForm};
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 = UploadUpdatedEditor2dProjectSuccessResponse),
(status = 400, description = "Bad input"),
(status = 401, description = "Not authorized"),
(status = 404, description = "Not found"),
(status = 500, description = "Server error"),
),
params(
("path" = UploadUpdatedEditor2dProjectPathInfo, 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 upload_updated_editor_2d_project_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
path: Path<UploadUpdatedEditor2dProjectPathInfo>,
MultipartForm(form): MultipartForm<UpdateProjectMultipartForm>,
) -> Result<Json<UploadUpdatedEditor2dProjectSuccessResponse>, CommonWebError> {
update_project(UpdateProjectArgs {
http_request: &http_request,
server_state: &server_state,
config: &EDITOR_2D_PROJECT_CONFIG,
media_file_token: &path.token,
form,
}).await?;
Ok(Json(UploadUpdatedEditor2dProjectSuccessResponse {
success: true,
media_file_token: path.into_inner().token,
}))
}
@@ -0,0 +1,57 @@
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::upload_updated_mood_board_project::{UploadUpdatedMoodBoardProjectPathInfo, UploadUpdatedMoodBoardProjectSuccessResponse};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::media_files::upload::project::project_upload_config::MOOD_BOARD_PROJECT_CONFIG;
use crate::http_server::endpoints::media_files::upload::project::update_project::{update_project, UpdateProjectArgs, UpdateProjectMultipartForm};
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 = UploadUpdatedMoodBoardProjectSuccessResponse),
(status = 400, description = "Bad input"),
(status = 401, description = "Not authorized"),
(status = 404, description = "Not found"),
(status = 500, description = "Server error"),
),
params(
("path" = UploadUpdatedMoodBoardProjectPathInfo, 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 upload_updated_mood_board_project_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
path: Path<UploadUpdatedMoodBoardProjectPathInfo>,
MultipartForm(form): MultipartForm<UpdateProjectMultipartForm>,
) -> Result<Json<UploadUpdatedMoodBoardProjectSuccessResponse>, CommonWebError> {
update_project(UpdateProjectArgs {
http_request: &http_request,
server_state: &server_state,
config: &MOOD_BOARD_PROJECT_CONFIG,
media_file_token: &path.token,
form,
}).await?;
Ok(Json(UploadUpdatedMoodBoardProjectSuccessResponse {
success: true,
media_file_token: path.into_inner().token,
}))
}
@@ -0,0 +1,57 @@
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::upload_updated_scene_3d_project::{UploadUpdatedScene3dProjectPathInfo, UploadUpdatedScene3dProjectSuccessResponse};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::media_files::upload::project::project_upload_config::SCENE_3D_PROJECT_CONFIG;
use crate::http_server::endpoints::media_files::upload::project::update_project::{update_project, UpdateProjectArgs, UpdateProjectMultipartForm};
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 = UploadUpdatedScene3dProjectSuccessResponse),
(status = 400, description = "Bad input"),
(status = 401, description = "Not authorized"),
(status = 404, description = "Not found"),
(status = 500, description = "Server error"),
),
params(
("path" = UploadUpdatedScene3dProjectPathInfo, 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 upload_updated_scene_3d_project_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
path: Path<UploadUpdatedScene3dProjectPathInfo>,
MultipartForm(form): MultipartForm<UpdateProjectMultipartForm>,
) -> Result<Json<UploadUpdatedScene3dProjectSuccessResponse>, CommonWebError> {
update_project(UpdateProjectArgs {
http_request: &http_request,
server_state: &server_state,
config: &SCENE_3D_PROJECT_CONFIG,
media_file_token: &path.token,
form,
}).await?;
Ok(Json(UploadUpdatedScene3dProjectSuccessResponse {
success: true,
media_file_token: path.into_inner().token,
}))
}
@@ -0,0 +1,57 @@
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::upload_updated_video_timeline_project::{UploadUpdatedVideoTimelineProjectPathInfo, UploadUpdatedVideoTimelineProjectSuccessResponse};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::media_files::upload::project::project_upload_config::VIDEO_TIMELINE_PROJECT_CONFIG;
use crate::http_server::endpoints::media_files::upload::project::update_project::{update_project, UpdateProjectArgs, UpdateProjectMultipartForm};
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 = UploadUpdatedVideoTimelineProjectSuccessResponse),
(status = 400, description = "Bad input"),
(status = 401, description = "Not authorized"),
(status = 404, description = "Not found"),
(status = 500, description = "Server error"),
),
params(
("path" = UploadUpdatedVideoTimelineProjectPathInfo, 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 upload_updated_video_timeline_project_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
path: Path<UploadUpdatedVideoTimelineProjectPathInfo>,
MultipartForm(form): MultipartForm<UpdateProjectMultipartForm>,
) -> Result<Json<UploadUpdatedVideoTimelineProjectSuccessResponse>, CommonWebError> {
update_project(UpdateProjectArgs {
http_request: &http_request,
server_state: &server_state,
config: &VIDEO_TIMELINE_PROJECT_CONFIG,
media_file_token: &path.token,
form,
}).await?;
Ok(Json(UploadUpdatedVideoTimelineProjectSuccessResponse {
success: true,
media_file_token: path.into_inner().token,
}))
}
@@ -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::upload_updated_mood_board_project_handler::upload_updated_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::upload_updated_video_timeline_project_handler::upload_updated_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::upload_updated_editor_2d_project_handler::upload_updated_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::upload_updated_scene_3d_project_handler::upload_updated_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<T, B> (app: App<T>) -> App<T>
.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(upload_updated_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(upload_updated_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(upload_updated_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(upload_updated_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()))