mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 15:55:41 -05:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37c446185e | ||
|
|
1dfb62454a |
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n t.token as `token: tokens::tokens::tags::TagToken`,\n t.tag_value\n\nFROM\n tags AS t\nJOIN tag_uses AS u\n ON t.token = u.tag_token\nWHERE\n u.entity_type = ?\n AND u.entity_token = ?\n AND u.maybe_deleted_at IS NULL\nORDER BY t.id DESC\nLIMIT 500\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token: tokens::tokens::tags::TagToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "tag_value",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3929e9fe74e75fad7252681880c6c3bd7e76e96b0bf603f77f6a9b2d4fcda18d"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nDELETE FROM tag_uses\nWHERE entity_type = ?\nAND entity_token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "41b16ccf04ef1ffb8b48bc59edf9389f61f54b871f6361f35529706b98d419ec"
|
||||
}
|
||||
+58
@@ -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"
|
||||
}
|
||||
+12
@@ -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"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO tags\nSET\n token = ?,\n tag_value = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c7e848e4c906a34ac5a4ad8cc73354baa8d5cd81170dd06d66d7f414505d538b"
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
$kind: collection
|
||||
order: 8000
|
||||
@@ -1,15 +0,0 @@
|
||||
$kind: http-request
|
||||
name: Edit Tags
|
||||
url: "{{api_host}}/v1/tags/edit/model_weight/weight_84t9zeafn72wkje1x59f9rm81"
|
||||
method: POST
|
||||
headers:
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: ""
|
||||
body:
|
||||
type: json
|
||||
content: |-
|
||||
{
|
||||
"tags": "video games, legend of zelda"
|
||||
}
|
||||
order: 1000
|
||||
@@ -1,6 +0,0 @@
|
||||
$kind: http-request
|
||||
url: "{{api_host}}/v1/tags/list/model_weight/weight_84t9zeafn72wkje1x59f9rm81"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
order: 2000
|
||||
@@ -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;
|
||||
+16
@@ -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,
|
||||
}
|
||||
+16
@@ -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,
|
||||
}
|
||||
+16
@@ -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,
|
||||
}
|
||||
+16
@@ -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,
|
||||
}
|
||||
+21
@@ -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,
|
||||
}
|
||||
+21
@@ -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,
|
||||
}
|
||||
+21
@@ -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,
|
||||
}
|
||||
+21
@@ -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,
|
||||
}
|
||||
+7
@@ -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;
|
||||
+83
@@ -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(())
|
||||
}
|
||||
+62
@@ -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;
|
||||
@@ -32,7 +32,6 @@ pub mod prompts;
|
||||
pub mod public_event_feed;
|
||||
pub mod staff_audit_logs;
|
||||
pub mod stats;
|
||||
pub mod tags;
|
||||
pub mod trending_model_analytics;
|
||||
pub mod tts;
|
||||
pub mod twitch;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
use crate::errors::database_insert_error::DatabaseInsertError;
|
||||
use crate::utils::transactor::Transactor;
|
||||
use tokens::tokens::tags::TagToken;
|
||||
|
||||
pub async fn create_tag(
|
||||
tag: &str,
|
||||
transactor: Transactor<'_, '_>,
|
||||
) -> Result<TagToken, DatabaseInsertError>
|
||||
{
|
||||
let tag = tag.trim().to_lowercase();
|
||||
let token = TagToken::generate();
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO tags
|
||||
SET
|
||||
token = ?,
|
||||
tag_value = ?
|
||||
"#,
|
||||
token,
|
||||
tag,
|
||||
);
|
||||
|
||||
let _r = transactor.execute(query).await?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::tags::TagToken;
|
||||
|
||||
use crate::utils::transactor::Transactor;
|
||||
|
||||
pub struct EntityTag {
|
||||
pub token: TagToken,
|
||||
pub tag_value: String,
|
||||
}
|
||||
|
||||
pub async fn list_tags_for_entity(
|
||||
tag_entity: TagUseEntity,
|
||||
transactor: Transactor<'_, '_>,
|
||||
) -> AnyhowResult<Vec<EntityTag>> {
|
||||
|
||||
let (entity_type, entity_token) = tag_entity.get_composite_keys();
|
||||
|
||||
let query = sqlx::query_as!(
|
||||
RawTag,
|
||||
r#"
|
||||
SELECT
|
||||
t.token as `token: tokens::tokens::tags::TagToken`,
|
||||
t.tag_value
|
||||
|
||||
FROM
|
||||
tags AS t
|
||||
JOIN tag_uses AS u
|
||||
ON t.token = u.tag_token
|
||||
WHERE
|
||||
u.entity_type = ?
|
||||
AND u.entity_token = ?
|
||||
AND u.maybe_deleted_at IS NULL
|
||||
ORDER BY t.id DESC
|
||||
LIMIT 500
|
||||
"#,
|
||||
entity_type,
|
||||
entity_token
|
||||
);
|
||||
|
||||
let results = match transactor {
|
||||
Transactor::Pool { pool } => {
|
||||
query.fetch_all(pool).await?
|
||||
},
|
||||
Transactor::Connection { connection } => {
|
||||
query.fetch_all(connection).await?
|
||||
},
|
||||
Transactor::Transaction { transaction } => {
|
||||
query.fetch_all(&mut **transaction).await?
|
||||
},
|
||||
};
|
||||
|
||||
Ok(results.into_iter()
|
||||
.map(|record| EntityTag {
|
||||
token: record.token,
|
||||
tag_value: record.tag_value,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
struct RawTag {
|
||||
token: TagToken,
|
||||
tag_value: String,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod create_tag;
|
||||
pub mod list_tags_for_entity;
|
||||
pub mod select_matching_tags;
|
||||
pub mod update_tags_for_entity;
|
||||
@@ -1,78 +0,0 @@
|
||||
use crate::utils::transactor::Transactor;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx::mysql::MySqlRow;
|
||||
use sqlx::{FromRow, MySql, QueryBuilder, Row};
|
||||
use tokens::tokens::tags::TagToken;
|
||||
|
||||
pub struct MatchingTag {
|
||||
pub token: TagToken,
|
||||
pub tag_value: String,
|
||||
}
|
||||
|
||||
pub async fn select_matching_tags(
|
||||
tag_values: &[String],
|
||||
transactor: Transactor<'_, '_>,
|
||||
) -> AnyhowResult<Vec<MatchingTag>> {
|
||||
if tag_values.is_empty() {
|
||||
return Ok(Vec::new()) // This prevents a SQL error when the IN clause is empty.
|
||||
}
|
||||
|
||||
let tag_values = tag_values.iter()
|
||||
.map(|tag| tag.trim().to_lowercase())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
|
||||
r#"
|
||||
SELECT
|
||||
token,
|
||||
tag_value
|
||||
FROM
|
||||
tags
|
||||
WHERE
|
||||
tag_value IN (
|
||||
"#,
|
||||
);
|
||||
|
||||
let mut separated = query_builder.separated(", ");
|
||||
|
||||
for tag_value in tag_values.iter() {
|
||||
separated.push_bind(tag_value);
|
||||
}
|
||||
|
||||
separated.push_unseparated(") ");
|
||||
|
||||
let query = query_builder.build_query_as::<RawTag>();
|
||||
|
||||
let results = match transactor {
|
||||
Transactor::Pool { pool } => {
|
||||
query.fetch_all(pool).await?
|
||||
},
|
||||
Transactor::Connection { connection } => {
|
||||
query.fetch_all(connection).await?
|
||||
},
|
||||
Transactor::Transaction { transaction } => {
|
||||
query.fetch_all(&mut **transaction).await?
|
||||
},
|
||||
};
|
||||
|
||||
Ok(results.into_iter()
|
||||
.map(|record| MatchingTag {
|
||||
token: record.token,
|
||||
tag_value: record.tag_value,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
struct RawTag {
|
||||
token: TagToken,
|
||||
tag_value: String,
|
||||
}
|
||||
|
||||
impl FromRow<'_, MySqlRow> for RawTag {
|
||||
fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
|
||||
Ok(Self {
|
||||
token: TagToken::new(row.try_get("token")?),
|
||||
tag_value: row.try_get("tag_value")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Acquire, Executor, MySql, QueryBuilder, Transaction};
|
||||
use tokens::tokens::tags::TagToken;
|
||||
|
||||
pub async fn update_tags_for_entity(
|
||||
entity: TagUseEntity,
|
||||
tags: &[TagToken],
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> AnyhowResult<()>
|
||||
{
|
||||
if tags.is_empty() {
|
||||
delete_all_query(&entity, mysql_connection).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut transaction = mysql_connection.begin().await?;
|
||||
|
||||
delete_query(&entity, tags, &mut transaction).await?;
|
||||
insert_query(&entity, tags, &mut transaction).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_query(
|
||||
entity: &TagUseEntity,
|
||||
tags: &[TagToken],
|
||||
transaction: &mut Transaction<'_, MySql>,
|
||||
) -> AnyhowResult<()> {
|
||||
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new("DELETE FROM tag_uses ");
|
||||
|
||||
query_builder.push(" WHERE entity_type = ");;
|
||||
query_builder.push_bind(entity.get_entity_type().to_str());
|
||||
|
||||
query_builder.push(" AND entity_token = ");;
|
||||
query_builder.push_bind(entity.get_entity_token_str());
|
||||
|
||||
query_builder.push(" AND tag_token NOT IN ( ");
|
||||
|
||||
let mut separated = query_builder.separated(", ");
|
||||
|
||||
for tag in tags.iter() {
|
||||
separated.push_bind(tag.as_str());
|
||||
}
|
||||
|
||||
separated.push_unseparated(") ");
|
||||
|
||||
let query = query_builder.build();
|
||||
|
||||
transaction.execute(query).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_all_query(
|
||||
entity: &TagUseEntity,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> AnyhowResult<()> {
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM tag_uses
|
||||
WHERE entity_type = ?
|
||||
AND entity_token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
entity.get_entity_type().to_str(),
|
||||
entity.get_entity_token_str(),
|
||||
);
|
||||
|
||||
mysql_connection.execute(query).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_query(
|
||||
entity: &TagUseEntity,
|
||||
tags: &[TagToken],
|
||||
transaction: &mut Transaction<'_, MySql>,
|
||||
) -> AnyhowResult<()> {
|
||||
if tags.is_empty() {
|
||||
return Ok(()) // This prevents a SQL error when the IN clause is empty.
|
||||
}
|
||||
|
||||
// NB: Insert ignore will insert non-duplicate rows and silently ignore duplicates
|
||||
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new("INSERT IGNORE INTO tag_uses ");
|
||||
|
||||
query_builder.push(" (entity_type, entity_token, tag_token) VALUES ");
|
||||
|
||||
for (i, tag) in tags.iter().enumerate() {
|
||||
query_builder.push(" ( ");;
|
||||
query_builder.push_bind(entity.get_entity_type().to_str());
|
||||
query_builder.push(", ");;
|
||||
query_builder.push_bind(entity.get_entity_token_str());
|
||||
query_builder.push(", ");;
|
||||
query_builder.push_bind(tag.as_str());
|
||||
query_builder.push(" ) ");
|
||||
|
||||
if i < tags.len() - 1 {
|
||||
query_builder.push(", ");
|
||||
}
|
||||
}
|
||||
|
||||
let query = query_builder.build();
|
||||
|
||||
transaction.execute(query).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+3
@@ -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()),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod audit_logs;
|
||||
pub mod batch_generations;
|
||||
pub mod featured_items;
|
||||
pub mod tag_uses;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod tag_use_entity;
|
||||
@@ -1,40 +0,0 @@
|
||||
use enums::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
|
||||
// TODO(bt, 2024-01-16): A ref type with <'a> lifetime of inner data instead of ownership?
|
||||
pub enum TagUseEntity {
|
||||
MediaFile(MediaFileToken),
|
||||
ModelWeight(ModelWeightToken),
|
||||
}
|
||||
|
||||
// TODO(bt, 2024-01-16): Codegen? Make traits for these? Maybe overkill.
|
||||
impl TagUseEntity {
|
||||
pub fn from_entity_type_and_token(entity_type: TagUseEntityType, token: &str) -> Self {
|
||||
match entity_type {
|
||||
TagUseEntityType::ModelWeight => Self::ModelWeight(ModelWeightToken::new_from_str(token)),
|
||||
TagUseEntityType::MediaFile => Self::MediaFile(MediaFileToken::new_from_str(token)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_entity_type(&self) -> TagUseEntityType {
|
||||
match self {
|
||||
Self::MediaFile(_) => TagUseEntityType::MediaFile,
|
||||
Self::ModelWeight(_) => TagUseEntityType::ModelWeight,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_entity_token_str(&self) -> &str {
|
||||
match self {
|
||||
Self::MediaFile(token) => token.as_str(),
|
||||
Self::ModelWeight(token) => token.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_composite_keys(&self) -> (TagUseEntityType, &str) {
|
||||
match self {
|
||||
Self::MediaFile(token) => (TagUseEntityType::MediaFile, token.as_str()),
|
||||
Self::ModelWeight(token) => (TagUseEntityType::ModelWeight, token.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -18,7 +18,6 @@ pub mod model_weights;
|
||||
pub mod prompt_context_items;
|
||||
pub mod prompts;
|
||||
pub mod staff_audit_logs;
|
||||
pub mod tag_uses;
|
||||
pub mod trending_model_analytics;
|
||||
pub mod tts_models;
|
||||
pub mod uploaded_video_notes;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod tag_use_entity_type;
|
||||
@@ -1,127 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Used in the `tag_uses` table in a `VARCHAR(32)` field named `entity_type`.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, ToSchema)]
|
||||
pub enum TagUseEntityType {
|
||||
/// Media files
|
||||
#[serde(rename = "media_file")]
|
||||
MediaFile,
|
||||
|
||||
/// Model weights
|
||||
#[serde(rename = "model_weight")]
|
||||
ModelWeight,
|
||||
}
|
||||
|
||||
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
|
||||
impl_enum_display_and_debug_using_to_str!(TagUseEntityType);
|
||||
impl_mysql_enum_coders!(TagUseEntityType);
|
||||
|
||||
/// NB: Legacy API for older code.
|
||||
impl TagUseEntityType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::MediaFile => "media_file",
|
||||
Self::ModelWeight => "model_weight",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"media_file" => Ok(Self::MediaFile),
|
||||
"model_weight" => Ok(Self::ModelWeight),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
Self::MediaFile,
|
||||
Self::ModelWeight,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_helpers::assert_serialization;
|
||||
use crate::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
|
||||
|
||||
mod serde {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(TagUseEntityType::MediaFile, "media_file");
|
||||
assert_serialization(TagUseEntityType::ModelWeight, "model_weight");
|
||||
}
|
||||
}
|
||||
|
||||
mod impl_methods {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_to_str() {
|
||||
assert_eq!(TagUseEntityType::MediaFile.to_str(), "media_file");
|
||||
assert_eq!(TagUseEntityType::ModelWeight.to_str(), "model_weight");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
assert_eq!(TagUseEntityType::from_str("media_file").unwrap(), TagUseEntityType::MediaFile);
|
||||
assert_eq!(TagUseEntityType::from_str("model_weight").unwrap(), TagUseEntityType::ModelWeight);
|
||||
assert!(TagUseEntityType::from_str("foo").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod manual_variant_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = TagUseEntityType::all_variants();
|
||||
assert_eq!(variants.len(), 2);
|
||||
assert_eq!(variants.pop_first(), Some(TagUseEntityType::MediaFile));
|
||||
assert_eq!(variants.pop_first(), Some(TagUseEntityType::ModelWeight));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(TagUseEntityType::all_variants().len(), TagUseEntityType::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in TagUseEntityType::all_variants() {
|
||||
assert_eq!(variant, TagUseEntityType::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, TagUseEntityType::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, TagUseEntityType::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 32;
|
||||
for variant in TagUseEntityType::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use crate::http_server::common_responses::pagination_cursors::PaginationCursors;
|
||||
use crate::http_server::common_responses::pagination_page::PaginationPage;
|
||||
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
|
||||
use crate::http_server::common_responses::simple_response::SimpleResponse;
|
||||
use crate::http_server::common_responses::tag_info::TagInfo;
|
||||
use crate::http_server::common_responses::user_details_lite::{UserDefaultAvatarInfo, UserDetailsLight};
|
||||
use crate::http_server::deprecated_endpoints::conversion::enqueue_fbx_to_gltf_handler::*;
|
||||
use crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_face_fusion_workflow_handler::*;
|
||||
@@ -70,6 +69,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::*;
|
||||
@@ -212,8 +221,6 @@ use billing_component::stripe::http_endpoints::checkout::create::stripe_create_c
|
||||
use crate::http_server::endpoints::billing_fakeyou::list_active_user_subscriptions_handler::*;
|
||||
use crate::http_server::endpoints::service::status_alert_handler::*;
|
||||
use crate::http_server::endpoints::stats::get_unified_queue_stats_handler::*;
|
||||
use crate::http_server::endpoints::tags::list_tags_for_entity_handler::*;
|
||||
use crate::http_server::endpoints::tags::set_tags_for_entity_handler::*;
|
||||
use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::*;
|
||||
use crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::*;
|
||||
use crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::*;
|
||||
@@ -329,6 +336,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 +485,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,
|
||||
@@ -486,8 +509,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::prompts::get_prompt_handler::get_prompt_handler,
|
||||
crate::http_server::endpoints::service::status_alert_handler::status_alert_handler,
|
||||
crate::http_server::endpoints::stats::get_unified_queue_stats_handler::get_unified_queue_stats_handler,
|
||||
crate::http_server::endpoints::tags::list_tags_for_entity_handler::list_tags_for_entity_handler,
|
||||
crate::http_server::endpoints::tags::set_tags_for_entity_handler::set_tags_for_entity_handler,
|
||||
crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::enqueue_infer_tts_handler,
|
||||
crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::batch_get_user_bookmarks_handler,
|
||||
crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::create_user_bookmark_handler,
|
||||
@@ -736,7 +757,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
SimpleEntityStats,
|
||||
SimpleGenericJsonSuccess,
|
||||
SimpleResponse,
|
||||
TagInfo,
|
||||
UserDetailsLight,
|
||||
VideoPreviews,
|
||||
Visibility,
|
||||
@@ -1075,8 +1095,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListSessionRequestDetailsResponse,
|
||||
ListSessionResultDetailsResponse,
|
||||
ListSessionStatusDetailsResponse,
|
||||
ListTagsForEntityPathInfo,
|
||||
ListTagsForEntitySuccessResponse,
|
||||
ListUserBookmarksForEntityError,
|
||||
ListUserBookmarksForEntityPathInfo,
|
||||
ListUserBookmarksForEntitySuccessResponse,
|
||||
@@ -1112,6 +1130,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ModeratorUserEmailChangeItem,
|
||||
ModeratorUserEmailChangeUserSummary,
|
||||
ModernInferenceQueueStats,
|
||||
NewProjectMultipartForm,
|
||||
PinnedMediaFile,
|
||||
PinnedModelWeightForList,
|
||||
ProjectMediaFileInfo,
|
||||
@@ -1141,9 +1160,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
SetModelWeightCoverImagePathInfo,
|
||||
SetModelWeightCoverImageRequest,
|
||||
SetModelWeightCoverImageResponse,
|
||||
SetTagsForEntityPathInfo,
|
||||
SetTagsForEntityRequest,
|
||||
SetTagsForEntitySuccessResponse,
|
||||
SetUserRatingRequest,
|
||||
SetUserRatingResponse,
|
||||
StatusAlertCategory,
|
||||
@@ -1169,6 +1185,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,
|
||||
|
||||
@@ -6,7 +6,6 @@ pub mod pagination_cursors;
|
||||
pub mod pagination_page;
|
||||
pub mod simple_entity_stats;
|
||||
pub mod simple_response;
|
||||
pub mod tag_info;
|
||||
pub mod user_avatars;
|
||||
pub mod user_details_lite;
|
||||
pub mod user_details_lite_builder;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
use tokens::tokens::tags::TagToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct TagInfo {
|
||||
pub token: TagToken,
|
||||
pub value: String,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+12
@@ -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;
|
||||
+42
@@ -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",
|
||||
};
|
||||
+55
@@ -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)
|
||||
}
|
||||
+176
@@ -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)
|
||||
}
|
||||
+203
@@ -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(())
|
||||
}
|
||||
+55
@@ -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,
|
||||
}))
|
||||
}
|
||||
+55
@@ -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,
|
||||
}))
|
||||
}
|
||||
+55
@@ -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,
|
||||
}))
|
||||
}
|
||||
+55
@@ -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,
|
||||
}))
|
||||
}
|
||||
+57
@@ -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,
|
||||
}))
|
||||
}
|
||||
+57
@@ -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,
|
||||
}))
|
||||
}
|
||||
+57
@@ -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,
|
||||
}))
|
||||
}
|
||||
+57
@@ -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,
|
||||
}))
|
||||
}
|
||||
@@ -23,7 +23,6 @@ pub mod prompts;
|
||||
pub mod service;
|
||||
pub mod stats;
|
||||
pub mod subscriptions;
|
||||
pub mod tags;
|
||||
pub mod trending;
|
||||
pub mod tts;
|
||||
pub mod user_bookmarks;
|
||||
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
//// NB: Incrementally getting rid of build warnings...
|
||||
//#![forbid(unused_imports)]
|
||||
//#![forbid(unused_mut)]
|
||||
//#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::tag_info::TagInfo;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
|
||||
use enums::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
|
||||
use log::warn;
|
||||
use mysql_queries::queries::tags::list_tags_for_entity::list_tags_for_entity;
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ListTagsForEntityPathInfo {
|
||||
entity_type: TagUseEntityType,
|
||||
entity_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListTagsForEntitySuccessResponse {
|
||||
pub success: bool,
|
||||
pub tags: Vec<TagInfo>,
|
||||
}
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
/// List the tags for an entity
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Tags",
|
||||
path = "/v1/tags/list/{entity_type}/{entity_token}",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = ListTagsForEntitySuccessResponse),
|
||||
(status = 400, description = "Bad input", body = CommonWebError),
|
||||
(status = 401, description = "Not authorized", body = CommonWebError),
|
||||
(status = 500, description = "Server error", body = CommonWebError),
|
||||
),
|
||||
params(
|
||||
("request" = ListTagsForEntityPathInfo, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn list_tags_for_entity_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<ListTagsForEntityPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListTagsForEntitySuccessResponse>, CommonWebError>
|
||||
{
|
||||
let token = path.entity_token.as_str();
|
||||
let token_prefix_matches = match path.entity_type {
|
||||
TagUseEntityType::MediaFile => token.starts_with(MediaFileToken::token_prefix()),
|
||||
TagUseEntityType::ModelWeight => token.starts_with(ModelWeightToken::token_prefix()),
|
||||
};
|
||||
|
||||
if !token_prefix_matches {
|
||||
warn!("invalid token prefix: {:?} for {:?}", path.entity_token, path.entity_type);
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("invalid token prefix".to_string()));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let entity = TagUseEntity::from_entity_type_and_token(
|
||||
path.entity_type, &path.entity_token);
|
||||
|
||||
let mut tags = list_tags_for_entity(entity, Transactor::for_connection(&mut *mysql_connection))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error listing tags: {:?}", e);
|
||||
CommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
tags.sort_by(|a, b| a.tag_value.cmp(&b.tag_value));
|
||||
|
||||
Ok(Json(ListTagsForEntitySuccessResponse {
|
||||
success: true,
|
||||
tags: tags
|
||||
.into_iter()
|
||||
.map(|tag| TagInfo {
|
||||
token: tag.token,
|
||||
value: tag.tag_value,
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod list_tags_for_entity_handler;
|
||||
pub mod set_tags_for_entity_handler;
|
||||
-245
@@ -1,245 +0,0 @@
|
||||
//// NB: Incrementally getting rid of build warnings...
|
||||
//#![forbid(unused_imports)]
|
||||
//#![forbid(unused_mut)]
|
||||
//#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::tag_info::TagInfo;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::user_lookup::user_session::require_user_session_extended::require_user_session_extended;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Data, Json, Path};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
|
||||
use enums::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
|
||||
use log::warn;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::get_media_file_with_transactor;
|
||||
use mysql_queries::queries::model_weights::get::get_weight::get_weight_by_token_with_transactor;
|
||||
use mysql_queries::queries::tags::create_tag::create_tag;
|
||||
use mysql_queries::queries::tags::select_matching_tags::select_matching_tags;
|
||||
use mysql_queries::queries::tags::update_tags_for_entity::update_tags_for_entity;
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
use sqlx::MySqlConnection;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::tags::TagToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Maximum number of tags allowed per item.
|
||||
const MAX_TAGS : usize = 30;
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetTagsForEntityPathInfo {
|
||||
entity_type: TagUseEntityType,
|
||||
entity_token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetTagsForEntityRequest {
|
||||
/// A comma separated list of tags.
|
||||
tags: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SetTagsForEntitySuccessResponse {
|
||||
pub success: bool,
|
||||
pub tags: Vec<TagInfo>,
|
||||
}
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
/// Edit the tags (in bulk) for an entity. Comma-separated.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Tags",
|
||||
path = "/v1/tags/edit/{entity_type}/{entity_token}",
|
||||
responses(
|
||||
(status = 200, description = "Success", body = SetTagsForEntitySuccessResponse),
|
||||
(status = 400, description = "Bad input", body = CommonWebError),
|
||||
(status = 401, description = "Not authorized", body = CommonWebError),
|
||||
(status = 500, description = "Server error", body = CommonWebError),
|
||||
),
|
||||
params(
|
||||
("request" = SetTagsForEntityRequest, description = "Payload for Request"),
|
||||
)
|
||||
)]
|
||||
pub async fn set_tags_for_entity_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<SetTagsForEntityRequest>,
|
||||
path: Path<SetTagsForEntityPathInfo>,
|
||||
server_state: Data<Arc<ServerState>>,
|
||||
) -> Result<Json<SetTagsForEntitySuccessResponse>, CommonWebError>
|
||||
{
|
||||
let token = path.entity_token.as_str();
|
||||
let token_prefix_matches = match path.entity_type {
|
||||
TagUseEntityType::MediaFile => token.starts_with(MediaFileToken::token_prefix()),
|
||||
TagUseEntityType::ModelWeight => token.starts_with(ModelWeightToken::token_prefix()),
|
||||
};
|
||||
|
||||
if !token_prefix_matches {
|
||||
warn!("invalid token prefix: {:?} for {:?}", path.entity_token, path.entity_type);
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("invalid token prefix".to_string()));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_extended(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut *mysql_connection).await?;
|
||||
|
||||
if user_session.role.is_banned {
|
||||
return Err(CommonWebError::NotAuthorized);
|
||||
}
|
||||
|
||||
let is_mod = user_session.role.can_ban_users;
|
||||
|
||||
let is_creator = get_is_creator(
|
||||
path.entity_type,
|
||||
&path.entity_token,
|
||||
&user_session.user_token_typed,
|
||||
&mut *mysql_connection
|
||||
).await?;
|
||||
|
||||
if !is_creator && !is_mod {
|
||||
warn!("user is not allowed to modify entity: {:?}", user_session.user_token);
|
||||
return Err(CommonWebError::NotAuthorized);
|
||||
}
|
||||
|
||||
let request_tags = to_normalized_tags(&request.tags);
|
||||
|
||||
let matching_tags = select_matching_tags(&request_tags, Transactor::for_connection(&mut *mysql_connection))
|
||||
.await
|
||||
.map(|tags| tags.into_iter()
|
||||
.map(|tag| TagInfo {
|
||||
token: tag.token,
|
||||
value: tag.tag_value.to_string(),
|
||||
})
|
||||
.collect::<Vec<TagInfo>>())
|
||||
.map_err(|e| {
|
||||
warn!("error selecting tags: {:?}", e);
|
||||
CommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let matching_tag_values = matching_tags
|
||||
.iter()
|
||||
.map(|tag| tag.value.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let new_tag_values = request_tags.iter()
|
||||
.filter(|tag| !matching_tag_values.contains(tag))
|
||||
.map(|tag| tag.as_str())
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
let mut new_tags = Vec::new();
|
||||
|
||||
// TODO(bt): Bulk insert instead of insert per tag.
|
||||
|
||||
for new_tag_value in new_tag_values {
|
||||
let token = create_tag(new_tag_value, Transactor::for_connection(&mut *mysql_connection))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error creating tag: {:?}", e);
|
||||
CommonWebError::from_error(e)
|
||||
})?;
|
||||
new_tags.push(TagInfo {
|
||||
token,
|
||||
value: new_tag_value.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let final_tag_set = matching_tags.into_iter()
|
||||
.chain(new_tags.into_iter())
|
||||
.collect::<Vec<TagInfo>>();
|
||||
|
||||
let final_tag_tokens = final_tag_set.iter()
|
||||
.map(|tag_info| tag_info.token.clone())
|
||||
.collect::<Vec<TagToken>>();
|
||||
|
||||
let entity = TagUseEntity::from_entity_type_and_token(
|
||||
path.entity_type, &path.entity_token);
|
||||
|
||||
update_tags_for_entity(
|
||||
entity,
|
||||
&final_tag_tokens,
|
||||
&mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("error updating tags: {:?}", e);
|
||||
CommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
Ok(Json(SetTagsForEntitySuccessResponse {
|
||||
success: true,
|
||||
tags: final_tag_set,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_is_creator(
|
||||
entity_type: TagUseEntityType,
|
||||
entity_token: &str,
|
||||
user_token: &UserToken,
|
||||
mysql_connection: &mut MySqlConnection
|
||||
) -> Result<bool, CommonWebError> {
|
||||
match entity_type {
|
||||
TagUseEntityType::MediaFile => {
|
||||
let entity_token = MediaFileToken::new_from_str(entity_token);
|
||||
let result = get_media_file_with_transactor(
|
||||
&entity_token,
|
||||
false,
|
||||
Transactor::for_connection(mysql_connection),
|
||||
).await;
|
||||
match result {
|
||||
Ok(Some(media_file)) => {
|
||||
Ok(media_file.maybe_creator_user_token
|
||||
.is_some_and(|t| t.as_str() == user_token.as_str()))
|
||||
},
|
||||
Ok(None) => Err(CommonWebError::NotFound),
|
||||
Err(err) => {
|
||||
warn!("Error looking up media_file: {:?}", err);
|
||||
Err(CommonWebError::from_anyhow_error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
TagUseEntityType::ModelWeight => {
|
||||
let entity_token = ModelWeightToken::new_from_str(entity_token);
|
||||
let result = get_weight_by_token_with_transactor(
|
||||
&entity_token,
|
||||
false,
|
||||
Transactor::for_connection(mysql_connection),
|
||||
).await;
|
||||
match result {
|
||||
Ok(Some(model_weight)) => {
|
||||
Ok(model_weight.creator_user_token.as_str() == user_token.as_str())
|
||||
},
|
||||
Ok(None) => Err(CommonWebError::NotFound),
|
||||
Err(err) => {
|
||||
warn!("Error looking up model_weight: {:?}", err);
|
||||
Err(CommonWebError::from_anyhow_error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_normalized_tags(tags: &str) -> Vec<String> {
|
||||
let mut tags : Vec<String> = tags.split(',')
|
||||
.map(|tag| tag.trim().to_lowercase())
|
||||
.filter(|tag| !tag.is_empty())
|
||||
.collect();
|
||||
|
||||
tags.truncate(MAX_TAGS);
|
||||
|
||||
tags
|
||||
}
|
||||
-2
@@ -16,7 +16,6 @@ use crate::http_server::routes::application_routes::omni_gen_routes::add_omni_ge
|
||||
use crate::http_server::routes::application_routes::prompts_routes::add_prompts_routes;
|
||||
use crate::http_server::routes::application_routes::stripe_artcraft_routes::add_stripe_artcraft_routes;
|
||||
use crate::http_server::routes::application_routes::subscription_routes::add_subscription_routes;
|
||||
use crate::http_server::routes::application_routes::tag_routes::add_tag_routes;
|
||||
use crate::http_server::routes::application_routes::user_bookmarks_routes::add_user_bookmarks_routes;
|
||||
use crate::http_server::routes::application_routes::user_rating_routes::add_user_rating_routes;
|
||||
use crate::http_server::routes::application_routes::user_referral_code_routes::add_user_referral_code_routes;
|
||||
@@ -55,7 +54,6 @@ where
|
||||
app = add_webhook_routes(app); // /v1/webhooks/... (fal)
|
||||
|
||||
// Legacy FakeYou surface area that might be useful again one day
|
||||
app = add_tag_routes(app); // /v1/tags
|
||||
app = add_weights_routes(app); // v1/weights
|
||||
|
||||
// Media files routes
|
||||
|
||||
+40
@@ -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()))
|
||||
|
||||
@@ -16,7 +16,6 @@ mod omni_gen_routes;
|
||||
mod prompts_routes;
|
||||
mod stripe_artcraft_routes;
|
||||
mod subscription_routes;
|
||||
mod tag_routes;
|
||||
mod user_bookmarks_routes;
|
||||
mod user_rating_routes;
|
||||
mod user_referral_code_routes;
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
use crate::http_server::endpoints::tags::list_tags_for_entity_handler::list_tags_for_entity_handler;
|
||||
use crate::http_server::endpoints::tags::set_tags_for_entity_handler::set_tags_for_entity_handler;
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
|
||||
pub fn add_tag_routes<T, B>(app: App<T>) -> App<T>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = ()
|
||||
>
|
||||
{
|
||||
app.service(
|
||||
web
|
||||
::scope("/v1/tags")
|
||||
.service(web::resource("/list/{entity_type}/{entity_token}")
|
||||
.route(web::get().to(list_tags_for_entity_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/edit/{entity_type}/{entity_token}")
|
||||
.route(web::post().to(set_tags_for_entity_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user