mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 03:39:28 -04:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54eb88eddf | ||
|
|
ba50a266ba | ||
|
|
ff4187b67a | ||
|
|
f985a97d45 | ||
|
|
5b64a4f322 | ||
|
|
7d7e95fc22 |
-157
@@ -1,157 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_type,\n mf.media_class,\n mf.maybe_mime_type,\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n mf.maybe_duration_millis,\n mf.maybe_title\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY fmf.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "membership_id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "added_to_folder_at: DateTime<Utc>",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "media_file_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "media_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "media_class",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_mime_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_frame_width",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_frame_height",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_duration_millis",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "maybe_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "491980e0a555a900354e60525947d77548a6cb8e4305d13886c8ad7e6787476c"
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY fmf.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "membership_id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "added_to_folder_at: DateTime<Utc>",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "media_file_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "media_class: MediaFileClass",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "media_type: MediaFileType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_batch_token: BatchGenerationToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_public_bucket_directory_hash?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_cover_public_bucket_prefix?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_cover_public_bucket_extension?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "creator_set_visibility: Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "is_user_upload: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_intermediate_system_file: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "maybe_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "maybe_prompt_token: PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "maybe_origin_filename",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "maybe_duration_millis",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "maybe_frame_width",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "maybe_frame_height",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "88673127b54626dc38d28693e709dfaeced9a05300a26b3f3c52a17f28171c9a"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE folders\nSET\n maybe_last_media_file_token_1 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 0\n ),\n maybe_last_media_file_token_2 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 1\n ),\n maybe_last_media_file_token_3 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 2\n ),\n maybe_last_media_file_token_4 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 3\n )\nWHERE token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ae417955113b385031181772b29f234ab9297cf583583f74fe287bf9c24b1e6a"
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND fmf.id < ?\nORDER BY fmf.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "membership_id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "added_to_folder_at: DateTime<Utc>",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "media_file_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "media_class: MediaFileClass",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "media_type: MediaFileType",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_batch_token: BatchGenerationToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_cover_public_bucket_directory_hash?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_cover_public_bucket_prefix?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_cover_public_bucket_extension?",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "creator_set_visibility: Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "is_user_upload: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_intermediate_system_file: bool",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY",
|
||||
"char_set": 63,
|
||||
"max_size": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "maybe_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "maybe_prompt_token: PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "maybe_origin_filename",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "maybe_duration_millis",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "maybe_frame_width",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "maybe_frame_height",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d2a406bcf08f656fd602aa6ba1124a77d04ad874b15abafca300e8b381c7b211"
|
||||
}
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_type,\n mf.media_class,\n mf.maybe_mime_type,\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n mf.maybe_duration_millis,\n mf.maybe_title\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND fmf.id < ?\nORDER BY fmf.id DESC\nLIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "membership_id: u64",
|
||||
"type_info": {
|
||||
"type": "LongLong",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
|
||||
"char_set": 63,
|
||||
"max_size": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "added_to_folder_at: DateTime<Utc>",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
"flags": "NOT_NULL | BINARY | TIMESTAMP",
|
||||
"char_set": 63,
|
||||
"max_size": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "media_file_token: MediaFileToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "media_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "media_class",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "maybe_mime_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "maybe_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "maybe_frame_width",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "maybe_frame_height",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "maybe_duration_millis",
|
||||
"type_info": {
|
||||
"type": "Long",
|
||||
"flags": "",
|
||||
"char_set": 63,
|
||||
"max_size": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "maybe_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 1020
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ea5f5e5b3caf990e9c25a3e8fe2e9625c12d6a36dd5dd91fc4615a45acdd33b8"
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
// NB: The "folder media file" list-item shape lives in storyteller_web
|
||||
// (see `endpoints/folders/media_files/list_folder_media_files_handler.rs`)
|
||||
// because it embeds `MediaFileCoverImageDetails` and other domain types
|
||||
// that depend on the request's `MediaDomain` + `ServerEnvironment`.
|
||||
|
||||
/// Canonical wire shape for a folder. Used by single-folder GETs, create
|
||||
/// responses, list rows, and subfolder list rows.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
@@ -15,12 +23,16 @@ pub struct FolderInfo {
|
||||
pub owner_user_token: UserToken,
|
||||
pub maybe_parent_folder_token: Option<FolderToken>,
|
||||
|
||||
pub maybe_last_media_file_token_1: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_2: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_3: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_4: Option<MediaFileToken>,
|
||||
/// The four most-recent media files added to the folder, in slot order
|
||||
/// (the schema stores them as `maybe_last_media_file_token_1..4`).
|
||||
/// Slots whose media file has been deleted are skipped, so the vec
|
||||
/// may contain 0-4 entries.
|
||||
pub last_media_thumbnails: Vec<FolderThumbnail>,
|
||||
|
||||
pub maybe_cover_image_custom_media_token: Option<MediaFileToken>,
|
||||
/// User-chosen cover image. When present, the frontend should use this
|
||||
/// as the folder's primary thumbnail instead of the
|
||||
/// `last_media_thumbnails` grid.
|
||||
pub maybe_custom_cover_thumbnail: Option<FolderThumbnail>,
|
||||
|
||||
pub maybe_color_code: Option<String>,
|
||||
pub has_star: bool,
|
||||
@@ -33,22 +45,25 @@ pub struct FolderInfo {
|
||||
pub is_orphaned: bool,
|
||||
}
|
||||
|
||||
/// Canonical wire shape for a media file inside a folder. Lean enough for
|
||||
/// thumbnail rendering; callers that need richer fields should hit the
|
||||
/// media-file batch-get endpoint.
|
||||
/// A compact thumbnail descriptor for a media file referenced by a
|
||||
/// folder (either one of the `last_media_*` slots or the custom cover).
|
||||
///
|
||||
/// Intentionally narrower than `MediaLinks` / `CoverImageLinks` — folders
|
||||
/// only need the bare minimum to render a thumbnail card. The CDN URL is
|
||||
/// the direct asset link; `maybe_thumbnail_template` carries the resizing
|
||||
/// template URL (with `{{width}}` / `{{height}}` placeholders) when the
|
||||
/// media class supports it.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct FolderMediaFileInfo {
|
||||
pub media_file_token: MediaFileToken,
|
||||
pub added_to_folder_at: DateTime<Utc>,
|
||||
pub struct FolderThumbnail {
|
||||
pub token: MediaFileToken,
|
||||
pub media_class: MediaFileClass,
|
||||
pub media_type: MediaFileType,
|
||||
|
||||
pub media_type: String,
|
||||
pub media_class: String,
|
||||
pub maybe_mime_type: Option<String>,
|
||||
pub public_bucket_directory_hash: String,
|
||||
pub maybe_public_bucket_prefix: Option<String>,
|
||||
pub maybe_public_bucket_extension: Option<String>,
|
||||
pub maybe_frame_width: Option<i32>,
|
||||
pub maybe_frame_height: Option<i32>,
|
||||
pub maybe_duration_millis: Option<i32>,
|
||||
pub maybe_title: Option<String>,
|
||||
/// Direct CDN URL to the underlying media asset.
|
||||
pub cdn_url: Url,
|
||||
|
||||
/// Resizing template URL (with `{{width}}` / `{{height}}` placeholders)
|
||||
/// when the media class supports it. `None` for media classes without
|
||||
/// a thumbnail variant.
|
||||
pub maybe_thumbnail_template: Option<String>,
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ use utoipa::{IntoParams, ToSchema};
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::folders::common::FolderMediaFileInfo;
|
||||
|
||||
// ── GET /v1/folders/media_files/{folder_token} ──
|
||||
//
|
||||
// NB: `ListFolderMediaFilesSuccessResponse` and `FolderMediaFileListItem`
|
||||
// live in `storyteller_web`'s handler because the wire shape embeds
|
||||
// `MediaFileCoverImageDetails` and `MediaLinks` constructors that depend
|
||||
// on the request's `MediaDomain` + `ServerEnvironment`.
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct FolderMediaFilesPathInfo {
|
||||
@@ -19,13 +22,6 @@ pub struct ListFolderMediaFilesQueryParams {
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListFolderMediaFilesSuccessResponse {
|
||||
pub success: bool,
|
||||
pub media_files: Vec<FolderMediaFileInfo>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/media_files/{folder_token}/bulk_add ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
|
||||
+119
-28
@@ -3,29 +3,57 @@ use std::marker::PhantomData;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
|
||||
/// One folder-media-file row + the joined media-file fields the list
|
||||
/// endpoint needs to materialize MediaLinks, MediaFileCoverImageDetails,
|
||||
/// and the rest of the typed response shape.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FolderMediaFileRow {
|
||||
/// The `folder_media_files.id` of the membership row. Use this as the
|
||||
/// cursor for the next page.
|
||||
/// `folder_media_files.id` — used as the pagination cursor.
|
||||
pub membership_id: u64,
|
||||
|
||||
/// When the media file was added to the folder.
|
||||
pub added_to_folder_at: DateTime<Utc>,
|
||||
|
||||
pub media_file_token: MediaFileToken,
|
||||
pub media_type: String,
|
||||
pub media_class: String,
|
||||
pub maybe_mime_type: Option<String>,
|
||||
pub media_class: MediaFileClass,
|
||||
pub media_type: MediaFileType,
|
||||
|
||||
pub maybe_batch_token: Option<BatchGenerationToken>,
|
||||
|
||||
// The media file's own bucket fields (caller uses these to build MediaLinks).
|
||||
pub public_bucket_directory_hash: String,
|
||||
pub maybe_public_bucket_prefix: Option<String>,
|
||||
pub maybe_public_bucket_extension: Option<String>,
|
||||
|
||||
// Cover image's bucket fields, from a LEFT JOIN on
|
||||
// mf.maybe_cover_image_media_file_token. All three are Some-or-None
|
||||
// together (matched / unmatched join row).
|
||||
pub maybe_cover_public_bucket_directory_hash: Option<String>,
|
||||
pub maybe_cover_public_bucket_prefix: Option<String>,
|
||||
pub maybe_cover_public_bucket_extension: Option<String>,
|
||||
|
||||
pub creator_set_visibility: Visibility,
|
||||
pub is_user_upload: bool,
|
||||
pub is_intermediate_system_file: bool,
|
||||
|
||||
pub maybe_title: Option<String>,
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
pub maybe_origin_filename: Option<String>,
|
||||
|
||||
pub maybe_duration_millis: Option<i32>,
|
||||
pub maybe_frame_width: Option<i32>,
|
||||
pub maybe_frame_height: Option<i32>,
|
||||
pub maybe_duration_millis: Option<i32>,
|
||||
pub maybe_title: Option<String>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub struct ListFolderMediaFilesArgs<'e, 'c, E>
|
||||
@@ -40,9 +68,12 @@ where
|
||||
}
|
||||
|
||||
/// Paginated list of media files in the given folder. Joins `media_files`
|
||||
/// and filters out rows that are soft-deleted on the media-file side.
|
||||
/// Most-recently-added first. The caller is expected to have already
|
||||
/// authorized the folder access.
|
||||
/// for the file's own metadata and LEFT JOINs `media_files` again on
|
||||
/// `maybe_cover_image_media_file_token` so the cover-image bucket fields
|
||||
/// come back in a single round-trip. Filters out media files that are
|
||||
/// soft-deleted on the media-file side. Most-recently-added first.
|
||||
///
|
||||
/// The caller is expected to have already authorized the folder access.
|
||||
pub async fn list_folder_media_files<'e, 'c: 'e, E>(
|
||||
args: ListFolderMediaFilesArgs<'e, 'c, E>,
|
||||
) -> Result<Vec<FolderMediaFileRow>, sqlx::Error>
|
||||
@@ -58,20 +89,40 @@ where
|
||||
SELECT
|
||||
fmf.id as `membership_id: u64`,
|
||||
fmf.created_at as `added_to_folder_at: DateTime<Utc>`,
|
||||
|
||||
mf.token as `media_file_token: MediaFileToken`,
|
||||
mf.media_type,
|
||||
mf.media_class,
|
||||
mf.maybe_mime_type,
|
||||
mf.media_class as `media_class: MediaFileClass`,
|
||||
mf.media_type as `media_type: MediaFileType`,
|
||||
|
||||
mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,
|
||||
|
||||
mf.public_bucket_directory_hash,
|
||||
mf.maybe_public_bucket_prefix,
|
||||
mf.maybe_public_bucket_extension,
|
||||
|
||||
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
|
||||
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
|
||||
cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,
|
||||
|
||||
mf.creator_set_visibility as `creator_set_visibility: Visibility`,
|
||||
mf.is_user_upload as `is_user_upload: bool`,
|
||||
mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,
|
||||
|
||||
mf.maybe_title,
|
||||
mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,
|
||||
mf.maybe_origin_filename,
|
||||
|
||||
mf.maybe_duration_millis,
|
||||
mf.maybe_frame_width,
|
||||
mf.maybe_frame_height,
|
||||
mf.maybe_duration_millis,
|
||||
mf.maybe_title
|
||||
|
||||
mf.created_at,
|
||||
mf.updated_at
|
||||
FROM folder_media_files fmf
|
||||
JOIN media_files mf
|
||||
ON mf.token = fmf.media_file_token
|
||||
LEFT JOIN media_files cover
|
||||
ON cover.token = mf.maybe_cover_image_media_file_token
|
||||
WHERE fmf.folder_token = ?
|
||||
AND mf.user_deleted_at IS NULL
|
||||
AND mf.mod_deleted_at IS NULL
|
||||
@@ -90,16 +141,26 @@ LIMIT ?
|
||||
membership_id: r.membership_id,
|
||||
added_to_folder_at: r.added_to_folder_at,
|
||||
media_file_token: r.media_file_token,
|
||||
media_type: r.media_type,
|
||||
media_class: r.media_class,
|
||||
maybe_mime_type: r.maybe_mime_type,
|
||||
media_type: r.media_type,
|
||||
maybe_batch_token: r.maybe_batch_token,
|
||||
public_bucket_directory_hash: r.public_bucket_directory_hash,
|
||||
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
|
||||
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
|
||||
maybe_cover_public_bucket_directory_hash: r.maybe_cover_public_bucket_directory_hash,
|
||||
maybe_cover_public_bucket_prefix: r.maybe_cover_public_bucket_prefix,
|
||||
maybe_cover_public_bucket_extension: r.maybe_cover_public_bucket_extension,
|
||||
creator_set_visibility: r.creator_set_visibility,
|
||||
is_user_upload: r.is_user_upload,
|
||||
is_intermediate_system_file: r.is_intermediate_system_file,
|
||||
maybe_title: r.maybe_title,
|
||||
maybe_prompt_token: r.maybe_prompt_token,
|
||||
maybe_origin_filename: r.maybe_origin_filename,
|
||||
maybe_duration_millis: r.maybe_duration_millis,
|
||||
maybe_frame_width: r.maybe_frame_width,
|
||||
maybe_frame_height: r.maybe_frame_height,
|
||||
maybe_duration_millis: r.maybe_duration_millis,
|
||||
maybe_title: r.maybe_title,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
@@ -109,20 +170,40 @@ LIMIT ?
|
||||
SELECT
|
||||
fmf.id as `membership_id: u64`,
|
||||
fmf.created_at as `added_to_folder_at: DateTime<Utc>`,
|
||||
|
||||
mf.token as `media_file_token: MediaFileToken`,
|
||||
mf.media_type,
|
||||
mf.media_class,
|
||||
mf.maybe_mime_type,
|
||||
mf.media_class as `media_class: MediaFileClass`,
|
||||
mf.media_type as `media_type: MediaFileType`,
|
||||
|
||||
mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,
|
||||
|
||||
mf.public_bucket_directory_hash,
|
||||
mf.maybe_public_bucket_prefix,
|
||||
mf.maybe_public_bucket_extension,
|
||||
|
||||
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
|
||||
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
|
||||
cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,
|
||||
|
||||
mf.creator_set_visibility as `creator_set_visibility: Visibility`,
|
||||
mf.is_user_upload as `is_user_upload: bool`,
|
||||
mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,
|
||||
|
||||
mf.maybe_title,
|
||||
mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,
|
||||
mf.maybe_origin_filename,
|
||||
|
||||
mf.maybe_duration_millis,
|
||||
mf.maybe_frame_width,
|
||||
mf.maybe_frame_height,
|
||||
mf.maybe_duration_millis,
|
||||
mf.maybe_title
|
||||
|
||||
mf.created_at,
|
||||
mf.updated_at
|
||||
FROM folder_media_files fmf
|
||||
JOIN media_files mf
|
||||
ON mf.token = fmf.media_file_token
|
||||
LEFT JOIN media_files cover
|
||||
ON cover.token = mf.maybe_cover_image_media_file_token
|
||||
WHERE fmf.folder_token = ?
|
||||
AND mf.user_deleted_at IS NULL
|
||||
AND mf.mod_deleted_at IS NULL
|
||||
@@ -139,16 +220,26 @@ LIMIT ?
|
||||
membership_id: r.membership_id,
|
||||
added_to_folder_at: r.added_to_folder_at,
|
||||
media_file_token: r.media_file_token,
|
||||
media_type: r.media_type,
|
||||
media_class: r.media_class,
|
||||
maybe_mime_type: r.maybe_mime_type,
|
||||
media_type: r.media_type,
|
||||
maybe_batch_token: r.maybe_batch_token,
|
||||
public_bucket_directory_hash: r.public_bucket_directory_hash,
|
||||
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
|
||||
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
|
||||
maybe_cover_public_bucket_directory_hash: r.maybe_cover_public_bucket_directory_hash,
|
||||
maybe_cover_public_bucket_prefix: r.maybe_cover_public_bucket_prefix,
|
||||
maybe_cover_public_bucket_extension: r.maybe_cover_public_bucket_extension,
|
||||
creator_set_visibility: r.creator_set_visibility,
|
||||
is_user_upload: r.is_user_upload,
|
||||
is_intermediate_system_file: r.is_intermediate_system_file,
|
||||
maybe_title: r.maybe_title,
|
||||
maybe_prompt_token: r.maybe_prompt_token,
|
||||
maybe_origin_filename: r.maybe_origin_filename,
|
||||
maybe_duration_millis: r.maybe_duration_millis,
|
||||
maybe_frame_width: r.maybe_frame_width,
|
||||
maybe_frame_height: r.maybe_frame_height,
|
||||
maybe_duration_millis: r.maybe_duration_millis,
|
||||
maybe_title: r.maybe_title,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ pub mod bulk_delete_folder_media_files;
|
||||
pub mod bulk_insert_folder_media_files;
|
||||
pub mod filter_existing_media_file_tokens;
|
||||
pub mod list_folder_media_files;
|
||||
pub mod recompute_folder_last_media_files;
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
pub struct RecomputeFolderLastMediaFilesArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
pub folder_token: &'e FolderToken,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
/// Recompute the four denormalized `maybe_last_media_file_token_1..4`
|
||||
/// columns on `folders` for the given folder by scanning the current
|
||||
/// (non-soft-deleted) members of `folder_media_files` and grabbing the
|
||||
/// four with the largest `folder_media_files.id` (most-recently-added).
|
||||
///
|
||||
/// Single-statement: four correlated `LIMIT 1 OFFSET k` scalar subqueries
|
||||
/// in the SET clause. Each subquery walks the `index_folder_token` index
|
||||
/// on `folder_media_files`, joins `media_files` by primary key, and
|
||||
/// stops at the (offset+1)-th matching row. Folders with fewer than four
|
||||
/// live members get NULL automatically for the empty slots — that's the
|
||||
/// scalar-subquery / no-rows-found semantics.
|
||||
///
|
||||
/// Intended to run inside the same transaction as the bulk add / remove /
|
||||
/// move that necessitated the recompute, so the cached columns and the
|
||||
/// real membership stay consistent under concurrent writers.
|
||||
pub async fn recompute_folder_last_media_files<'e, 'c: 'e, E>(
|
||||
args: RecomputeFolderLastMediaFilesArgs<'e, 'c, E>,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let folder_token_str = args.folder_token.as_str();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE folders
|
||||
SET
|
||||
maybe_last_media_file_token_1 = (
|
||||
SELECT mf.token
|
||||
FROM folder_media_files fmf
|
||||
JOIN media_files mf
|
||||
ON mf.token = fmf.media_file_token
|
||||
WHERE fmf.folder_token = ?
|
||||
AND mf.user_deleted_at IS NULL
|
||||
AND mf.mod_deleted_at IS NULL
|
||||
ORDER BY fmf.id DESC
|
||||
LIMIT 1 OFFSET 0
|
||||
),
|
||||
maybe_last_media_file_token_2 = (
|
||||
SELECT mf.token
|
||||
FROM folder_media_files fmf
|
||||
JOIN media_files mf
|
||||
ON mf.token = fmf.media_file_token
|
||||
WHERE fmf.folder_token = ?
|
||||
AND mf.user_deleted_at IS NULL
|
||||
AND mf.mod_deleted_at IS NULL
|
||||
ORDER BY fmf.id DESC
|
||||
LIMIT 1 OFFSET 1
|
||||
),
|
||||
maybe_last_media_file_token_3 = (
|
||||
SELECT mf.token
|
||||
FROM folder_media_files fmf
|
||||
JOIN media_files mf
|
||||
ON mf.token = fmf.media_file_token
|
||||
WHERE fmf.folder_token = ?
|
||||
AND mf.user_deleted_at IS NULL
|
||||
AND mf.mod_deleted_at IS NULL
|
||||
ORDER BY fmf.id DESC
|
||||
LIMIT 1 OFFSET 2
|
||||
),
|
||||
maybe_last_media_file_token_4 = (
|
||||
SELECT mf.token
|
||||
FROM folder_media_files fmf
|
||||
JOIN media_files mf
|
||||
ON mf.token = fmf.media_file_token
|
||||
WHERE fmf.folder_token = ?
|
||||
AND mf.user_deleted_at IS NULL
|
||||
AND mf.mod_deleted_at IS NULL
|
||||
ORDER BY fmf.id DESC
|
||||
LIMIT 1 OFFSET 3
|
||||
)
|
||||
WHERE token = ?
|
||||
"#,
|
||||
folder_token_str,
|
||||
folder_token_str,
|
||||
folder_token_str,
|
||||
folder_token_str,
|
||||
folder_token_str,
|
||||
)
|
||||
.execute(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use sqlx::{Executor, MySql, QueryBuilder};
|
||||
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Minimal columns the folder-thumbnail builder needs from `media_files`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaFileThumbnailRow {
|
||||
pub token: MediaFileToken,
|
||||
pub media_class: MediaFileClass,
|
||||
pub media_type: MediaFileType,
|
||||
pub public_bucket_directory_hash: String,
|
||||
pub maybe_public_bucket_prefix: Option<String>,
|
||||
pub maybe_public_bucket_extension: Option<String>,
|
||||
}
|
||||
|
||||
pub struct BatchGetMediaFileThumbnailsByTokensArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
pub candidate_tokens: &'e [MediaFileToken],
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
/// Look up the bucket-path / class / type for each of the given media
|
||||
/// file tokens. Silently drops rows that don't exist or are soft-deleted.
|
||||
/// Caller is responsible for de-duplicating the input set if needed; the
|
||||
/// SQL doesn't care about duplicates.
|
||||
///
|
||||
/// Uses runtime `QueryBuilder` because the IN-list size is dynamic.
|
||||
pub async fn batch_get_media_file_thumbnails_by_tokens<'e, 'c: 'e, E>(
|
||||
args: BatchGetMediaFileThumbnailsByTokensArgs<'e, 'c, E>,
|
||||
) -> Result<Vec<MediaFileThumbnailRow>, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
if args.candidate_tokens.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut builder = QueryBuilder::<MySql>::new(
|
||||
"SELECT \
|
||||
token, \
|
||||
media_class, \
|
||||
media_type, \
|
||||
public_bucket_directory_hash, \
|
||||
maybe_public_bucket_prefix, \
|
||||
maybe_public_bucket_extension \
|
||||
FROM media_files \
|
||||
WHERE user_deleted_at IS NULL \
|
||||
AND mod_deleted_at IS NULL \
|
||||
AND token IN (",
|
||||
);
|
||||
|
||||
let mut separated = builder.separated(", ");
|
||||
for token in args.candidate_tokens {
|
||||
separated.push_bind(token.as_str());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
|
||||
let rows = builder
|
||||
.build_query_as::<RawRow>()
|
||||
.fetch_all(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(|r| MediaFileThumbnailRow {
|
||||
token: MediaFileToken::new(r.token),
|
||||
media_class: MediaFileClass::from_str(&r.media_class).unwrap_or(MediaFileClass::Image),
|
||||
media_type: MediaFileType::from_str(&r.media_type).unwrap_or(MediaFileType::Image),
|
||||
public_bucket_directory_hash: r.public_bucket_directory_hash,
|
||||
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
|
||||
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
|
||||
}).collect())
|
||||
}
|
||||
|
||||
/// Internal row shape used by the dynamic `QueryBuilder` call. We hand-roll
|
||||
/// the deserialization here because `QueryBuilder` can't ride the
|
||||
/// compile-time type checks of `sqlx::query!`/`query_as!`.
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct RawRow {
|
||||
token: String,
|
||||
media_class: String,
|
||||
media_type: String,
|
||||
public_bucket_directory_hash: String,
|
||||
maybe_public_bucket_prefix: Option<String>,
|
||||
maybe_public_bucket_extension: Option<String>,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod batch_get_media_file_thumbnails_by_tokens;
|
||||
pub mod batch_get_media_files;
|
||||
pub mod batch_get_media_files_by_tokens;
|
||||
pub mod get_media_file;
|
||||
|
||||
@@ -1246,7 +1246,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
|
||||
// Folders
|
||||
FolderInfo,
|
||||
FolderMediaFileInfo,
|
||||
FolderThumbnail,
|
||||
FolderMediaFileListItem,
|
||||
FolderPathInfo,
|
||||
FolderMediaFilesPathInfo,
|
||||
SubfolderPathInfo,
|
||||
|
||||
+18
-2
@@ -13,7 +13,10 @@ use mysql_queries::queries::folders::folder::get_folder_for_owner::{
|
||||
use mysql_queries::queries::folders::folder::insert_folder::{insert_folder, InsertFolderArgs};
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::folder_row_to_info;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
|
||||
build_folder_thumbnails_lookup, folder_row_to_info,
|
||||
};
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -101,7 +104,20 @@ pub async fn create_folder_handler(
|
||||
})?
|
||||
.ok_or_else(|| CommonWebError::server_error_with_message("folder vanished after insert"))?;
|
||||
|
||||
let folder: FolderInfo = folder_row_to_info(row);
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
let server_environment = server_state.server_environment;
|
||||
|
||||
let thumbnails = build_folder_thumbnails_lookup(
|
||||
std::slice::from_ref(&row),
|
||||
&mut *conn,
|
||||
media_domain,
|
||||
server_environment,
|
||||
).await.map_err(|err| {
|
||||
warn!("Folder thumbnail lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let folder: FolderInfo = folder_row_to_info(row, &thumbnails);
|
||||
|
||||
Ok(Json(CreateFolderSuccessResponse {
|
||||
success: true,
|
||||
|
||||
+123
-21
@@ -1,18 +1,87 @@
|
||||
use artcraft_api_defs::folders::common::{FolderInfo, FolderMediaFileInfo};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use artcraft_api_defs::folders::common::{FolderInfo, FolderThumbnail};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use mysql_queries::queries::folders::folder::folder_row::FolderRow;
|
||||
use mysql_queries::queries::folders::media_files::list_folder_media_files::FolderMediaFileRow;
|
||||
use mysql_queries::queries::media_files::get::batch_get_media_file_thumbnails_by_tokens::{
|
||||
batch_get_media_file_thumbnails_by_tokens, BatchGetMediaFileThumbnailsByTokensArgs,
|
||||
MediaFileThumbnailRow,
|
||||
};
|
||||
use server_environment::ServerEnvironment;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
|
||||
/// Gather every media-file token referenced by the given folder rows
|
||||
/// (the four `last_media_*` slots + the custom cover) and batch-fetch
|
||||
/// the thumbnail descriptors so we can build [`FolderInfo`]s in one go.
|
||||
///
|
||||
/// Returns a token → thumbnail map. Tokens for media files that don't
|
||||
/// exist or are soft-deleted are silently absent from the map; the
|
||||
/// caller treats them as "slot skipped".
|
||||
pub async fn build_folder_thumbnails_lookup<'e, 'c: 'e, E>(
|
||||
folder_rows: &[FolderRow],
|
||||
mysql_executor: E,
|
||||
media_domain: MediaDomain,
|
||||
server_environment: ServerEnvironment,
|
||||
) -> Result<HashMap<MediaFileToken, FolderThumbnail>, sqlx::Error>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql> + 'c,
|
||||
{
|
||||
let candidate_tokens = collect_candidate_thumbnail_tokens(folder_rows);
|
||||
|
||||
if candidate_tokens.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let thumbnail_rows = batch_get_media_file_thumbnails_by_tokens(
|
||||
BatchGetMediaFileThumbnailsByTokensArgs {
|
||||
candidate_tokens: &candidate_tokens,
|
||||
mysql_executor,
|
||||
phantom: PhantomData,
|
||||
},
|
||||
).await?;
|
||||
|
||||
Ok(thumbnail_rows.into_iter()
|
||||
.map(|row| {
|
||||
let token = row.token.clone();
|
||||
(token, build_folder_thumbnail(row, media_domain, server_environment))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn folder_row_to_info(
|
||||
row: FolderRow,
|
||||
thumbnails_by_token: &HashMap<MediaFileToken, FolderThumbnail>,
|
||||
) -> FolderInfo {
|
||||
// Vec rather than array so `.into_iter()` yields owned `Option`s on
|
||||
// every edition.
|
||||
let last_media_thumbnails: Vec<FolderThumbnail> = vec![
|
||||
row.maybe_last_media_file_token_1,
|
||||
row.maybe_last_media_file_token_2,
|
||||
row.maybe_last_media_file_token_3,
|
||||
row.maybe_last_media_file_token_4,
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|maybe_token| {
|
||||
maybe_token.and_then(|t| thumbnails_by_token.get(&t).cloned())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let maybe_custom_cover_thumbnail = row.maybe_cover_image_custom_media_token
|
||||
.and_then(|t| thumbnails_by_token.get(&t).cloned());
|
||||
|
||||
pub fn folder_row_to_info(row: FolderRow) -> FolderInfo {
|
||||
FolderInfo {
|
||||
token: row.token,
|
||||
name: row.name,
|
||||
owner_user_token: row.owner_user_token,
|
||||
maybe_parent_folder_token: row.maybe_parent_folder_token,
|
||||
maybe_last_media_file_token_1: row.maybe_last_media_file_token_1,
|
||||
maybe_last_media_file_token_2: row.maybe_last_media_file_token_2,
|
||||
maybe_last_media_file_token_3: row.maybe_last_media_file_token_3,
|
||||
maybe_last_media_file_token_4: row.maybe_last_media_file_token_4,
|
||||
maybe_cover_image_custom_media_token: row.maybe_cover_image_custom_media_token,
|
||||
last_media_thumbnails,
|
||||
maybe_custom_cover_thumbnail,
|
||||
maybe_color_code: row.maybe_color_code,
|
||||
has_star: row.has_star,
|
||||
created_at: row.created_at,
|
||||
@@ -21,19 +90,52 @@ pub fn folder_row_to_info(row: FolderRow) -> FolderInfo {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn folder_media_file_row_to_info(row: FolderMediaFileRow) -> FolderMediaFileInfo {
|
||||
FolderMediaFileInfo {
|
||||
media_file_token: row.media_file_token,
|
||||
added_to_folder_at: row.added_to_folder_at,
|
||||
media_type: row.media_type,
|
||||
fn collect_candidate_thumbnail_tokens(rows: &[FolderRow]) -> Vec<MediaFileToken> {
|
||||
// Dedup with a `HashSet` so a folder that reuses the same token across
|
||||
// multiple slots (or two folders that share a thumbnail) doesn't pad
|
||||
// the IN-list bound parameter list.
|
||||
let mut seen: HashSet<MediaFileToken> = HashSet::with_capacity(rows.len() * 5);
|
||||
for row in rows {
|
||||
for maybe_token in [
|
||||
row.maybe_last_media_file_token_1.as_ref(),
|
||||
row.maybe_last_media_file_token_2.as_ref(),
|
||||
row.maybe_last_media_file_token_3.as_ref(),
|
||||
row.maybe_last_media_file_token_4.as_ref(),
|
||||
row.maybe_cover_image_custom_media_token.as_ref(),
|
||||
] {
|
||||
if let Some(token) = maybe_token {
|
||||
seen.insert(token.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
seen.into_iter().collect()
|
||||
}
|
||||
|
||||
fn build_folder_thumbnail(
|
||||
row: MediaFileThumbnailRow,
|
||||
media_domain: MediaDomain,
|
||||
server_environment: ServerEnvironment,
|
||||
) -> FolderThumbnail {
|
||||
let bucket_path = MediaFileBucketPath::from_object_hash(
|
||||
&row.public_bucket_directory_hash,
|
||||
row.maybe_public_bucket_prefix.as_deref(),
|
||||
row.maybe_public_bucket_extension.as_deref(),
|
||||
);
|
||||
|
||||
// Reuse `MediaLinksBuilder` to produce the CDN URL + thumbnail
|
||||
// template, then pull just those two fields onto the compact
|
||||
// FolderThumbnail wire shape.
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_environment,
|
||||
&bucket_path,
|
||||
);
|
||||
|
||||
FolderThumbnail {
|
||||
token: row.token,
|
||||
media_class: row.media_class,
|
||||
maybe_mime_type: row.maybe_mime_type,
|
||||
public_bucket_directory_hash: row.public_bucket_directory_hash,
|
||||
maybe_public_bucket_prefix: row.maybe_public_bucket_prefix,
|
||||
maybe_public_bucket_extension: row.maybe_public_bucket_extension,
|
||||
maybe_frame_width: row.maybe_frame_width,
|
||||
maybe_frame_height: row.maybe_frame_height,
|
||||
maybe_duration_millis: row.maybe_duration_millis,
|
||||
maybe_title: row.maybe_title,
|
||||
media_type: row.media_type,
|
||||
cdn_url: media_links.cdn_url,
|
||||
maybe_thumbnail_template: media_links.maybe_thumbnail_template,
|
||||
}
|
||||
}
|
||||
|
||||
+18
-3
@@ -12,7 +12,10 @@ use mysql_queries::queries::folders::folder::get_folder_for_owner::{
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::folder_row_to_info;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
|
||||
build_folder_thumbnails_lookup, folder_row_to_info,
|
||||
};
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -43,7 +46,6 @@ pub async fn get_folder_handler(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
|
||||
let row = get_folder_for_owner(GetFolderForOwnerArgs {
|
||||
folder_token: &path.folder_token,
|
||||
owner_user_token: &user_session.user_token,
|
||||
@@ -55,8 +57,21 @@ pub async fn get_folder_handler(
|
||||
})?
|
||||
.ok_or(CommonWebError::NotFound)?;
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
let server_environment = server_state.server_environment;
|
||||
|
||||
let thumbnails = build_folder_thumbnails_lookup(
|
||||
std::slice::from_ref(&row),
|
||||
&mut *conn,
|
||||
media_domain,
|
||||
server_environment,
|
||||
).await.map_err(|err| {
|
||||
warn!("Folder thumbnail lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(Json(GetFolderSuccessResponse {
|
||||
success: true,
|
||||
folder: folder_row_to_info(row),
|
||||
folder: folder_row_to_info(row, &thumbnails),
|
||||
}))
|
||||
}
|
||||
|
||||
+20
-2
@@ -13,7 +13,10 @@ use mysql_queries::queries::folders::folder::list_folders_for_user::{
|
||||
};
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::folder_row_to_info;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
|
||||
build_folder_thumbnails_lookup, folder_row_to_info,
|
||||
};
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -82,7 +85,22 @@ pub async fn list_folders_handler(
|
||||
CommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let folders = rows.into_iter().map(folder_row_to_info).collect();
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
let server_environment = server_state.server_environment;
|
||||
|
||||
let thumbnails = build_folder_thumbnails_lookup(
|
||||
&rows,
|
||||
&mut *conn,
|
||||
media_domain,
|
||||
server_environment,
|
||||
).await.map_err(|err| {
|
||||
warn!("Folder thumbnail lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let folders = rows.into_iter()
|
||||
.map(|row| folder_row_to_info(row, &thumbnails))
|
||||
.collect();
|
||||
|
||||
Ok(Json(ListFoldersSuccessResponse {
|
||||
success: true,
|
||||
|
||||
+75
-10
@@ -3,7 +3,10 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
use log::{error, warn};
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Acquire, MySql, Transaction};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use artcraft_api_defs::folders::media_files::{
|
||||
BulkAddFolderMediaFilesRequest, BulkAddFolderMediaFilesSuccessResponse,
|
||||
@@ -18,6 +21,9 @@ use mysql_queries::queries::folders::media_files::bulk_insert_folder_media_files
|
||||
use mysql_queries::queries::folders::media_files::filter_existing_media_file_tokens::{
|
||||
filter_existing_media_file_tokens, FilterExistingMediaFileTokensArgs,
|
||||
};
|
||||
use mysql_queries::queries::folders::media_files::recompute_folder_last_media_files::{
|
||||
recompute_folder_last_media_files, RecomputeFolderLastMediaFilesArgs,
|
||||
};
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
@@ -88,16 +94,10 @@ pub async fn bulk_add_folder_media_files_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// Empty `accepted` is a no-op — no membership change means no
|
||||
// recompute needed and no transaction to open.
|
||||
if !accepted.is_empty() {
|
||||
bulk_insert_folder_media_files(BulkInsertFolderMediaFilesArgs {
|
||||
folder_token: &path.folder_token,
|
||||
media_file_tokens: &accepted,
|
||||
mysql_executor: &mut *conn,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("bulk_insert_folder_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
perform_atomic_add(&mut conn, &path.folder_token, &accepted).await?;
|
||||
}
|
||||
|
||||
Ok(Json(BulkAddFolderMediaFilesSuccessResponse {
|
||||
@@ -105,3 +105,68 @@ pub async fn bulk_add_folder_media_files_handler(
|
||||
accepted_media_file_tokens: accepted,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Open a transaction, run the insert + recompute as one unit, and
|
||||
/// commit on success. On any failure inside [`perform_add_work`] the
|
||||
/// transaction is explicitly rolled back before the original error is
|
||||
/// re-raised — sqlx would roll back on drop too, but doing it explicitly
|
||||
/// makes the failure path obvious and surfaces any rollback error in
|
||||
/// the log.
|
||||
async fn perform_atomic_add(
|
||||
conn: &mut PoolConnection<MySql>,
|
||||
folder_token: &FolderToken,
|
||||
media_file_tokens: &[MediaFileToken],
|
||||
) -> Result<(), CommonWebError> {
|
||||
let mut tx = conn.begin().await.map_err(|err| {
|
||||
warn!("Failed to begin transaction: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let work_result = perform_add_work(&mut tx, folder_token, media_file_tokens).await;
|
||||
|
||||
match work_result {
|
||||
Ok(()) => {
|
||||
tx.commit().await.map_err(|err| {
|
||||
warn!("Failed to commit bulk_add transaction: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
if let Err(rollback_err) = tx.rollback().await {
|
||||
error!(
|
||||
"Rollback after bulk_add failure also failed: {:?} (original error: {:?})",
|
||||
rollback_err, err,
|
||||
);
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_add_work(
|
||||
tx: &mut Transaction<'_, MySql>,
|
||||
folder_token: &FolderToken,
|
||||
media_file_tokens: &[MediaFileToken],
|
||||
) -> Result<(), CommonWebError> {
|
||||
bulk_insert_folder_media_files(BulkInsertFolderMediaFilesArgs {
|
||||
folder_token,
|
||||
media_file_tokens,
|
||||
mysql_executor: &mut **tx,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("bulk_insert_folder_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
recompute_folder_last_media_files(RecomputeFolderLastMediaFilesArgs {
|
||||
folder_token,
|
||||
mysql_executor: &mut **tx,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("recompute_folder_last_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+25
@@ -24,6 +24,9 @@ use mysql_queries::queries::folders::media_files::bulk_insert_folder_media_files
|
||||
use mysql_queries::queries::folders::media_files::filter_existing_media_file_tokens::{
|
||||
filter_existing_media_file_tokens, FilterExistingMediaFileTokensArgs,
|
||||
};
|
||||
use mysql_queries::queries::folders::media_files::recompute_folder_last_media_files::{
|
||||
recompute_folder_last_media_files, RecomputeFolderLastMediaFilesArgs,
|
||||
};
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
@@ -229,6 +232,28 @@ async fn perform_move_work(
|
||||
})?
|
||||
};
|
||||
|
||||
// Recompute the denormalized `maybe_last_media_file_token_1..4` on
|
||||
// BOTH folders inside the same transaction. The source needs it because
|
||||
// we may have removed one of its cached recent items; the destination
|
||||
// needs it because we just added new recent items.
|
||||
recompute_folder_last_media_files(RecomputeFolderLastMediaFilesArgs {
|
||||
folder_token: source_folder_token,
|
||||
mysql_executor: &mut **tx,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("recompute_folder_last_media_files (source) failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
recompute_folder_last_media_files(RecomputeFolderLastMediaFilesArgs {
|
||||
folder_token: destination_folder_token,
|
||||
mysql_executor: &mut **tx,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("recompute_folder_last_media_files (destination) failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(MoveOutcome {
|
||||
removed_from_source_count,
|
||||
added_to_destination_count,
|
||||
|
||||
+85
-10
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
use log::{error, warn};
|
||||
|
||||
use artcraft_api_defs::folders::media_files::{
|
||||
BulkRemoveFolderMediaFilesRequest, BulkRemoveFolderMediaFilesSuccessResponse,
|
||||
@@ -15,7 +15,13 @@ use mysql_queries::queries::folders::folder::get_folder_for_owner::{
|
||||
use mysql_queries::queries::folders::media_files::bulk_delete_folder_media_files::{
|
||||
bulk_delete_folder_media_files, BulkDeleteFolderMediaFilesArgs,
|
||||
};
|
||||
use mysql_queries::queries::folders::media_files::recompute_folder_last_media_files::{
|
||||
recompute_folder_last_media_files, RecomputeFolderLastMediaFilesArgs,
|
||||
};
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{Acquire, MySql, Transaction};
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
@@ -74,18 +80,87 @@ pub async fn bulk_remove_folder_media_files_handler(
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
let removed_count = bulk_delete_folder_media_files(BulkDeleteFolderMediaFilesArgs {
|
||||
folder_token: &path.folder_token,
|
||||
media_file_tokens: &request.media_file_tokens,
|
||||
mysql_executor: &mut *conn,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("bulk_delete_folder_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
// Empty input short-circuits with zero work — no transaction needed.
|
||||
if request.media_file_tokens.is_empty() {
|
||||
return Ok(Json(BulkRemoveFolderMediaFilesSuccessResponse {
|
||||
success: true,
|
||||
removed_count: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
let removed_count = perform_atomic_remove(
|
||||
&mut conn,
|
||||
&path.folder_token,
|
||||
&request.media_file_tokens,
|
||||
).await?;
|
||||
|
||||
Ok(Json(BulkRemoveFolderMediaFilesSuccessResponse {
|
||||
success: true,
|
||||
removed_count,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Open a transaction, run the delete + recompute as one unit, and
|
||||
/// commit on success. On any failure inside [`perform_remove_work`] the
|
||||
/// transaction is explicitly rolled back before the original error is
|
||||
/// re-raised — sqlx would roll back on drop too, but doing it explicitly
|
||||
/// makes the failure path obvious and surfaces any rollback error in
|
||||
/// the log.
|
||||
async fn perform_atomic_remove(
|
||||
conn: &mut PoolConnection<MySql>,
|
||||
folder_token: &FolderToken,
|
||||
media_file_tokens: &[MediaFileToken],
|
||||
) -> Result<u64, CommonWebError> {
|
||||
let mut tx = conn.begin().await.map_err(|err| {
|
||||
warn!("Failed to begin transaction: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let work_result = perform_remove_work(&mut tx, folder_token, media_file_tokens).await;
|
||||
|
||||
match work_result {
|
||||
Ok(removed_count) => {
|
||||
tx.commit().await.map_err(|err| {
|
||||
warn!("Failed to commit bulk_remove transaction: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
Ok(removed_count)
|
||||
}
|
||||
Err(err) => {
|
||||
if let Err(rollback_err) = tx.rollback().await {
|
||||
error!(
|
||||
"Rollback after bulk_remove failure also failed: {:?} (original error: {:?})",
|
||||
rollback_err, err,
|
||||
);
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_remove_work(
|
||||
tx: &mut Transaction<'_, MySql>,
|
||||
folder_token: &FolderToken,
|
||||
media_file_tokens: &[MediaFileToken],
|
||||
) -> Result<u64, CommonWebError> {
|
||||
let removed_count = bulk_delete_folder_media_files(BulkDeleteFolderMediaFilesArgs {
|
||||
folder_token,
|
||||
media_file_tokens,
|
||||
mysql_executor: &mut **tx,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("bulk_delete_folder_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
recompute_folder_last_media_files(RecomputeFolderLastMediaFilesArgs {
|
||||
folder_token,
|
||||
mysql_executor: &mut **tx,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("recompute_folder_last_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(removed_count)
|
||||
}
|
||||
|
||||
+134
-5
@@ -3,21 +3,35 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::warn;
|
||||
use serde_derive::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use artcraft_api_defs::folders::media_files::{
|
||||
FolderMediaFilesPathInfo, ListFolderMediaFilesQueryParams, ListFolderMediaFilesSuccessResponse,
|
||||
FolderMediaFilesPathInfo, ListFolderMediaFilesQueryParams,
|
||||
};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use mysql_queries::queries::folders::folder::get_folder_for_owner::{
|
||||
get_folder_for_owner, GetFolderForOwnerArgs,
|
||||
};
|
||||
use mysql_queries::queries::folders::media_files::list_folder_media_files::{
|
||||
list_folder_media_files, ListFolderMediaFilesArgs,
|
||||
list_folder_media_files, FolderMediaFileRow, ListFolderMediaFilesArgs,
|
||||
};
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::folder_media_file_row_to_info;
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -25,6 +39,67 @@ const CURSOR_NAME: &str = "folder_mf";
|
||||
const DEFAULT_LIMIT: u32 = 100;
|
||||
const MAX_LIMIT: u32 = 1000;
|
||||
|
||||
// ── Response shape ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListFolderMediaFilesSuccessResponse {
|
||||
pub success: bool,
|
||||
pub media_files: Vec<FolderMediaFileListItem>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
/// One media file as it appears inside a folder. Lean version of the
|
||||
/// "by batch" / "by user" list-item shape — keeps the fields that matter
|
||||
/// for folder browsing and drops the bucket-path noise.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct FolderMediaFileListItem {
|
||||
pub token: MediaFileToken,
|
||||
|
||||
/// Coarse-grained class (image / video / audio / dimensional).
|
||||
pub media_class: MediaFileClass,
|
||||
|
||||
/// Specific format (jpg, png, mp4, etc.) — closer to a MIME type.
|
||||
pub media_type: MediaFileType,
|
||||
|
||||
/// Link to the prompt
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
/// If this file was generated as part of a batch, the batch token —
|
||||
/// useful for showing "siblings" in the UI.
|
||||
pub maybe_batch_token: Option<BatchGenerationToken>,
|
||||
|
||||
/// Rich CDN links to the media itself (full URL, thumbnail template,
|
||||
/// video previews when applicable).
|
||||
pub media_links: MediaLinks,
|
||||
|
||||
/// Cover image details. For files that don't carry their own cover
|
||||
/// image, this still gives the deterministic default-cover spec
|
||||
/// (image_index + color_index) the frontend uses for placeholders.
|
||||
pub cover_image: MediaFileCoverImageDetails,
|
||||
|
||||
pub maybe_title: Option<String>,
|
||||
pub maybe_original_filename: Option<String>,
|
||||
|
||||
/// Original pixel width / height for image and video files when known.
|
||||
pub maybe_frame_width: Option<i32>,
|
||||
pub maybe_frame_height: Option<i32>,
|
||||
|
||||
/// Duration for audio and video files, if available. Milliseconds.
|
||||
pub maybe_duration_millis: Option<u64>,
|
||||
|
||||
pub creator_set_visibility: Visibility,
|
||||
pub is_user_upload: bool,
|
||||
pub is_intermediate_system_file: bool,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
/// When the media file was added to the folder.
|
||||
pub added_to_folder_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// ── Handler ──
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Folders (Media File Management)",
|
||||
@@ -65,7 +140,7 @@ pub async fn list_folder_media_files_handler(
|
||||
warn!("Folder lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
|
||||
if folder.is_none() {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
@@ -103,7 +178,12 @@ pub async fn list_folder_media_files_handler(
|
||||
CommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let media_files = rows.into_iter().map(folder_media_file_row_to_info).collect();
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
let server_environment = server_state.server_environment;
|
||||
|
||||
let media_files = rows.into_iter()
|
||||
.map(|row| folder_media_file_row_to_list_item(row, media_domain, server_environment))
|
||||
.collect();
|
||||
|
||||
Ok(Json(ListFolderMediaFilesSuccessResponse {
|
||||
success: true,
|
||||
@@ -111,3 +191,52 @@ pub async fn list_folder_media_files_handler(
|
||||
maybe_cursor,
|
||||
}))
|
||||
}
|
||||
|
||||
fn folder_media_file_row_to_list_item(
|
||||
row: FolderMediaFileRow,
|
||||
media_domain: MediaDomain,
|
||||
server_environment: server_environment::ServerEnvironment,
|
||||
) -> FolderMediaFileListItem {
|
||||
let bucket_path = MediaFileBucketPath::from_object_hash(
|
||||
&row.public_bucket_directory_hash,
|
||||
row.maybe_public_bucket_prefix.as_deref(),
|
||||
row.maybe_public_bucket_extension.as_deref(),
|
||||
);
|
||||
|
||||
let media_links = MediaLinksBuilder::from_media_path_and_env(
|
||||
media_domain,
|
||||
server_environment,
|
||||
&bucket_path,
|
||||
);
|
||||
|
||||
let cover_image = MediaFileCoverImageDetails::from_optional_db_fields(
|
||||
&row.media_file_token,
|
||||
media_domain,
|
||||
server_environment,
|
||||
row.maybe_cover_public_bucket_directory_hash.as_deref(),
|
||||
row.maybe_cover_public_bucket_prefix.as_deref(),
|
||||
row.maybe_cover_public_bucket_extension.as_deref(),
|
||||
);
|
||||
|
||||
FolderMediaFileListItem {
|
||||
token: row.media_file_token,
|
||||
added_to_folder_at: row.added_to_folder_at,
|
||||
media_class: row.media_class,
|
||||
media_type: row.media_type,
|
||||
maybe_batch_token: row.maybe_batch_token,
|
||||
media_links,
|
||||
cover_image,
|
||||
creator_set_visibility: row.creator_set_visibility,
|
||||
is_user_upload: row.is_user_upload,
|
||||
is_intermediate_system_file: row.is_intermediate_system_file,
|
||||
maybe_title: row.maybe_title,
|
||||
maybe_prompt_token: row.maybe_prompt_token,
|
||||
maybe_original_filename: row.maybe_origin_filename,
|
||||
// Schema stores `INT(10)`; widen to u64 for the wire shape.
|
||||
maybe_duration_millis: row.maybe_duration_millis.map(|n| n as u64),
|
||||
maybe_frame_width: row.maybe_frame_width,
|
||||
maybe_frame_height: row.maybe_frame_height,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
+20
-2
@@ -17,7 +17,10 @@ use mysql_queries::queries::folders::subfolder::list_subfolders::{
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::folder_row_to_info;
|
||||
use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
|
||||
build_folder_thumbnails_lookup, folder_row_to_info,
|
||||
};
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -105,7 +108,22 @@ pub async fn list_subfolders_handler(
|
||||
CommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let subfolders = rows.into_iter().map(folder_row_to_info).collect();
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
let server_environment = server_state.server_environment;
|
||||
|
||||
let thumbnails = build_folder_thumbnails_lookup(
|
||||
&rows,
|
||||
&mut *conn,
|
||||
media_domain,
|
||||
server_environment,
|
||||
).await.map_err(|err| {
|
||||
warn!("Folder thumbnail lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let subfolders = rows.into_iter()
|
||||
.map(|row| folder_row_to_info(row, &thumbnails))
|
||||
.collect();
|
||||
|
||||
Ok(Json(ListSubfoldersSuccessResponse {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user