mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
fal webhook fixes 2 (#1347)
This commit is contained in:
@@ -85,7 +85,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn gpt_image_1p5_content_policy_violation() {
|
||||
let webhook = load_test_webhook("gpt_image_1p5_fail_content_policy.json");
|
||||
let webhook = load_test_webhook("failure/gpt_image_1p5_fail_content_policy.json");
|
||||
let result = hydrate_webhook_contents(&webhook);
|
||||
|
||||
match result {
|
||||
@@ -105,7 +105,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn gpt_image_invalid_api_key() {
|
||||
let webhook = load_test_webhook("gpt_image_fail_invalid_api_key.json");
|
||||
let webhook = load_test_webhook("failure/gpt_image_fail_invalid_api_key.json");
|
||||
let result = hydrate_webhook_contents(&webhook);
|
||||
|
||||
match result {
|
||||
@@ -125,7 +125,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn kling_1p6_pro_file_too_large() {
|
||||
let webhook = load_test_webhook("kling_1p6_pro_file_too_large_error.json");
|
||||
let webhook = load_test_webhook("failure/kling_1p6_pro_file_too_large_error.json");
|
||||
let result = hydrate_webhook_contents(&webhook);
|
||||
|
||||
match result {
|
||||
@@ -145,7 +145,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn kling_3p0_pro_content_policy_violation() {
|
||||
let webhook = load_test_webhook("kling_3p0_pro_fail_content_policy.json");
|
||||
let webhook = load_test_webhook("failure/kling_3p0_pro_fail_content_policy.json");
|
||||
let result = hydrate_webhook_contents(&webhook);
|
||||
|
||||
match result {
|
||||
@@ -165,7 +165,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn nano_banana_pro_no_media_generated() {
|
||||
let webhook = load_test_webhook("nano_banana_pro_fail_no_media_generated.json");
|
||||
let webhook = load_test_webhook("failure/nano_banana_pro_fail_no_media_generated.json");
|
||||
let result = hydrate_webhook_contents(&webhook);
|
||||
|
||||
match result {
|
||||
@@ -224,6 +224,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seedream_partner_validation_failed() {
|
||||
let webhook = load_test_webhook("failure/seedream_partner_validation_failed.json");
|
||||
let result = hydrate_webhook_contents(&webhook);
|
||||
|
||||
match result {
|
||||
HydratedWebhookContents::Error(data) => {
|
||||
assert_eq!(
|
||||
data.error_type,
|
||||
Some(WebhookErrorType::ContentPolicyViolation),
|
||||
);
|
||||
assert_eq!(
|
||||
data.message.as_deref(),
|
||||
Some("The content could not be processed because it contained material flagged by a content checker."),
|
||||
);
|
||||
}
|
||||
other => panic!("Expected HydratedWebhookContents::Error, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_no_payload() {
|
||||
let webhook = RawWebhookPayload {
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"error": "Unexpected status code: 422",
|
||||
"gateway_request_id": "019d8079-baa1-75c0-a2da-7347ee642909",
|
||||
"payload": {
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {
|
||||
"extra_info": {
|
||||
"reason": "partner_validation_failed"
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"enable_safety_checker": false,
|
||||
"image_size": "landscape_16_9",
|
||||
"image_urls": [
|
||||
"https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/w/k/5/k/8/wk5k8p47fwsw4hvgyj6gsb7nczcqeh58/snapshot_wk5k8p47fwsw4hvgyj6gsb7nczcqeh58.png"
|
||||
],
|
||||
"max_images": 1,
|
||||
"num_images": 4,
|
||||
"prompt": "Add a giant monster in the distance",
|
||||
"sync_mode": false
|
||||
},
|
||||
"loc": [
|
||||
"body",
|
||||
"image"
|
||||
],
|
||||
"msg": "The content could not be processed because it contained material flagged by a content checker.",
|
||||
"type": "content_policy_violation",
|
||||
"url": "https://docs.fal.ai/errors#content_policy_violation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"request_id": "019d8079-baa1-75c0-a2da-7347ee642909",
|
||||
"status": "ERROR"
|
||||
}
|
||||
+1
@@ -4,3 +4,4 @@ mod process_images_payload;
|
||||
mod process_model_glb_payload;
|
||||
mod process_model_mesh_payload;
|
||||
mod process_video_payload;
|
||||
mod resolve_file_metadata;
|
||||
|
||||
+93
-42
@@ -1,18 +1,17 @@
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::webhooks::process_success::resolve_file_metadata::resolve_file_metadata;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use anyhow::anyhow;
|
||||
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_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ImageData;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use images::encoding::webp_bytes_to_png_bytes::webp_bytes_to_png_bytes;
|
||||
use images::image_info::image_info::ImageInfo;
|
||||
use log::info;
|
||||
use log::{info, warn};
|
||||
use mimetypes::mimetype_info::file_extension::FileExtension;
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -23,44 +22,78 @@ pub async fn process_image_payload(
|
||||
image_data: &ImageData,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
) -> Result<MediaFileToken, AdvancedCommonWebError> {
|
||||
let image_url = image_data.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in image payload"))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("No `url` in image payload");
|
||||
AdvancedCommonWebError::server_error_with_message("no `url` in image payload")
|
||||
})?;
|
||||
|
||||
//let mime_type = image.content_type
|
||||
// .as_deref()
|
||||
// .ok_or_else(|| anyhow!("no `content_type` in image payload"))?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(image_url)
|
||||
// Download with a retry if the first attempt returns suspiciously few bytes.
|
||||
let mut file_bytes = http_download_url_to_bytes(image_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download image: {:?}", e))?;
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download image from {}: {:?}", image_url, err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
if file_bytes.len() <= 10 {
|
||||
warn!(
|
||||
"Downloaded only {} bytes from {} — retrying once",
|
||||
file_bytes.len(),
|
||||
image_url,
|
||||
);
|
||||
file_bytes = http_download_url_to_bytes(image_url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download image on retry from {}: {:?}", image_url, err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
info!("File type: {}, extension: {:?}",
|
||||
mimetype_info.mime_type(),
|
||||
mimetype_info.file_extension());
|
||||
// Resolve mime type: magic bytes first, fal content_type as fallback.
|
||||
let metadata = resolve_file_metadata(&file_bytes, image_data.content_type.as_deref())
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"Could not determine file type for image (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(),
|
||||
image_data.content_type,
|
||||
);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Could not determine file type for image (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(), image_data.content_type))
|
||||
})?;
|
||||
|
||||
match mimetype_info.file_extension() {
|
||||
Some(FileExtension::Webp) => {
|
||||
info!("File type: {}, extension: {:?}, source: {:?}",
|
||||
metadata.mime_type, metadata.file_extension, metadata.source);
|
||||
|
||||
match metadata.file_extension {
|
||||
FileExtension::Webp => {
|
||||
info!("Artcraft can't handle WebP images yet; converting to PNG...");
|
||||
|
||||
let file_bytes = webp_bytes_to_png_bytes(&file_bytes)?;
|
||||
let converted = webp_bytes_to_png_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to convert WebP to PNG: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
let converted_metadata = resolve_file_metadata(&converted, Some("image/png"))
|
||||
.ok_or_else(|| {
|
||||
warn!("Failed to determine file type after WebP→PNG conversion");
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
"Failed to determine file type after WebP→PNG conversion")
|
||||
})?;
|
||||
|
||||
info!("Updated file type: {}, extension: {:?}",
|
||||
mimetype_info.mime_type(),
|
||||
mimetype_info.file_extension());
|
||||
info!("Converted file type: {}, extension: {:?}",
|
||||
converted_metadata.mime_type, converted_metadata.file_extension);
|
||||
|
||||
upload_single_image_bytes(
|
||||
job,
|
||||
server_state,
|
||||
&file_bytes,
|
||||
mimetype_info,
|
||||
&converted,
|
||||
&converted_metadata.mime_type,
|
||||
converted_metadata.file_extension,
|
||||
).await
|
||||
}
|
||||
_ => {
|
||||
@@ -68,7 +101,8 @@ pub async fn process_image_payload(
|
||||
job,
|
||||
server_state,
|
||||
&file_bytes,
|
||||
mimetype_info,
|
||||
&metadata.mime_type,
|
||||
metadata.file_extension,
|
||||
).await
|
||||
}
|
||||
}
|
||||
@@ -78,21 +112,30 @@ async fn upload_single_image_bytes(
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
file_bytes: &[u8],
|
||||
mimetype_info: MimetypeInfo,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
mime_type: &str,
|
||||
file_extension: FileExtension,
|
||||
) -> Result<MediaFileToken, AdvancedCommonWebError> {
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("Unsupported media file type: {}", mime_type);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Unsupported media file type: {}", mime_type))
|
||||
})?;
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
let extension_with_period = file_extension.extension_with_period();
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
let image_info = ImageInfo::decode_image_from_bytes(&file_bytes)?;
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to hash image bytes: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
let image_info = ImageInfo::decode_image_from_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to decode image info: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
@@ -101,8 +144,12 @@ async fn upload_single_image_bytes(
|
||||
server_state.public_bucket_client.upload_file_with_content_type_process(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
mime_type)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to upload image to bucket: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
@@ -120,8 +167,12 @@ async fn upload_single_image_bytes(
|
||||
.frame_height(image_info.height())
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to insert image media file record: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
info!("Image media file uploaded with token: {}", media_token);
|
||||
|
||||
Ok(media_token)
|
||||
|
||||
+169
-41
@@ -1,18 +1,17 @@
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::webhooks::process_success::resolve_file_metadata::resolve_file_metadata;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use anyhow::anyhow;
|
||||
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_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ImagesData;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use images::encoding::webp_bytes_to_png_bytes::webp_bytes_to_png_bytes;
|
||||
use images::image_info::image_info::ImageInfo;
|
||||
use log::info;
|
||||
use log::{info, warn};
|
||||
use mimetypes::mimetype_info::file_extension::FileExtension;
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
@@ -24,7 +23,7 @@ pub async fn process_images_payload(
|
||||
images_data: &[ImagesData],
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<(Option<MediaFileToken>, Option<BatchGenerationToken>)> {
|
||||
) -> Result<(Option<MediaFileToken>, Option<BatchGenerationToken>), AdvancedCommonWebError> {
|
||||
|
||||
let mut maybe_media_token = None;
|
||||
|
||||
@@ -52,41 +51,80 @@ async fn upload_image(
|
||||
server_state: &ServerState,
|
||||
image: &ImagesData,
|
||||
maybe_batch_token: Option<&BatchGenerationToken>,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
) -> Result<MediaFileToken, AdvancedCommonWebError> {
|
||||
let image_url = image.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in image payload"))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("No `url` in image payload");
|
||||
AdvancedCommonWebError::server_error_with_message("no `url` in image payload")
|
||||
})?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(image_url)
|
||||
// Download with a retry if the first attempt returns suspiciously few bytes.
|
||||
let mut file_bytes = http_download_url_to_bytes(image_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download image: {:?}", e))?;
|
||||
.map_err(|e| {
|
||||
warn!("Failed to download image from {}: {:?}", image_url, e);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Failed to download image: {:?}", e))
|
||||
})?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info (1)"))?;
|
||||
if file_bytes.len() <= 10 {
|
||||
warn!(
|
||||
"Downloaded only {} bytes from {} — retrying once",
|
||||
file_bytes.len(),
|
||||
image_url,
|
||||
);
|
||||
file_bytes = http_download_url_to_bytes(image_url)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to download image on retry from {}: {:?}", image_url, e);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Failed to download image on retry: {:?}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
info!("File type: {}, extension: {:?}",
|
||||
mimetype_info.mime_type(),
|
||||
mimetype_info.file_extension());
|
||||
// Resolve mime type: magic bytes first, fal content_type as fallback.
|
||||
let metadata = resolve_file_metadata(&file_bytes, image.content_type.as_deref())
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"Could not determine file type for image (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(),
|
||||
image.content_type,
|
||||
);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Could not determine file type for image (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(), image.content_type))
|
||||
})?;
|
||||
|
||||
match mimetype_info.file_extension() {
|
||||
Some(FileExtension::Webp) => {
|
||||
info!("File type: {}, extension: {:?}, source: {:?}",
|
||||
metadata.mime_type, metadata.file_extension, metadata.source);
|
||||
|
||||
match metadata.file_extension {
|
||||
FileExtension::Webp => {
|
||||
info!("Artcraft can't handle WebP images yet; converting to PNG...");
|
||||
|
||||
let file_bytes = webp_bytes_to_png_bytes(&file_bytes)?;
|
||||
let converted = webp_bytes_to_png_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to convert WebP to PNG: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info (2)"))?;
|
||||
let converted_metadata = resolve_file_metadata(&converted, Some("image/png"))
|
||||
.ok_or_else(|| {
|
||||
warn!("Failed to determine file type after WebP→PNG conversion");
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
"Failed to determine file type after WebP→PNG conversion")
|
||||
})?;
|
||||
|
||||
info!("Updated file type: {}, extension: {:?}",
|
||||
mimetype_info.mime_type(),
|
||||
mimetype_info.file_extension());
|
||||
info!("Converted file type: {}, extension: {:?}",
|
||||
converted_metadata.mime_type, converted_metadata.file_extension);
|
||||
|
||||
upload_image_bytes(
|
||||
job,
|
||||
server_state,
|
||||
image,
|
||||
&file_bytes,
|
||||
mimetype_info,
|
||||
&converted,
|
||||
&converted_metadata.mime_type,
|
||||
converted_metadata.file_extension,
|
||||
maybe_batch_token,
|
||||
).await
|
||||
}
|
||||
@@ -94,9 +132,9 @@ async fn upload_image(
|
||||
upload_image_bytes(
|
||||
job,
|
||||
server_state,
|
||||
image,
|
||||
&file_bytes,
|
||||
mimetype_info,
|
||||
&metadata.mime_type,
|
||||
metadata.file_extension,
|
||||
maybe_batch_token,
|
||||
).await
|
||||
}
|
||||
@@ -106,26 +144,34 @@ async fn upload_image(
|
||||
async fn upload_image_bytes(
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
image: &ImagesData,
|
||||
file_bytes: &[u8],
|
||||
mimetype_info: MimetypeInfo,
|
||||
mime_type: &str,
|
||||
file_extension: FileExtension,
|
||||
maybe_batch_token: Option<&BatchGenerationToken>,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
) -> Result<MediaFileToken, AdvancedCommonWebError> {
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("Unsupported media file type: {}", mime_type);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Unsupported media file type: {}", mime_type))
|
||||
})?;
|
||||
|
||||
info!("MediaFileType: {:?}", media_file_type);
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
let extension_with_period = file_extension.extension_with_period();
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
let image_info = ImageInfo::decode_image_from_bytes(&file_bytes)?;
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to hash image bytes: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
let image_info = ImageInfo::decode_image_from_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to decode image info: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
@@ -134,8 +180,12 @@ async fn upload_image_bytes(
|
||||
server_state.public_bucket_client.upload_file_with_content_type_process(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
mime_type)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to upload image to bucket: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let mut query_builder = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
@@ -159,10 +209,88 @@ async fn upload_image_bytes(
|
||||
|
||||
let media_token = query_builder
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to insert image media file record: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
info!("Image media file uploaded with token: {} ; possible batch token: {:?}",
|
||||
media_token, maybe_batch_token);
|
||||
|
||||
Ok(media_token)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
|
||||
/// Diagnostic test: download real fal-hosted images and check if MimetypeInfo
|
||||
/// can detect them. This test is #[ignore]'d because it makes real HTTP
|
||||
/// requests.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn diagnose_fal_image_mimetype_detection() {
|
||||
let urls = [
|
||||
"https://v3b.fal.media/files/b/[REDACT].png",
|
||||
"https://v3b.fal.media/files/b/[REDACT].png",
|
||||
"https://v3b.fal.media/files/b/[REDACT].png",
|
||||
"https://v3b.fal.media/files/b/[REDACT].png",
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
println!("\n--- Downloading: {} ---", url);
|
||||
let bytes = http_download_url_to_bytes(url).await
|
||||
.unwrap_or_else(|e| panic!("Failed to download {}: {:?}", url, e));
|
||||
|
||||
println!(" Downloaded {} bytes", bytes.len());
|
||||
|
||||
// Print first 32 bytes as hex for magic number inspection.
|
||||
let head: Vec<u8> = bytes.iter().take(32).cloned().collect();
|
||||
println!(" First 32 bytes (hex): {:02x?}", head);
|
||||
println!(" First 16 bytes (ascii): {:?}", String::from_utf8_lossy(&head[..16.min(head.len())]));
|
||||
|
||||
let mimetype = MimetypeInfo::get_for_bytes(&bytes);
|
||||
match mimetype {
|
||||
Some(info) => {
|
||||
println!(" Detected mime type: {}", info.mime_type());
|
||||
println!(" Detected extension: {:?}", info.file_extension());
|
||||
}
|
||||
None => {
|
||||
println!(" ERROR: MimetypeInfo::get_for_bytes returned None!");
|
||||
println!(" This is the bug — infer could not detect the file type.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Diagnostic test: download a real fal-hosted video and check mimetype.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn diagnose_fal_video_mimetype_detection() {
|
||||
let url = "https://v3b.fal.media/files/b/[REDACT].mp4";
|
||||
|
||||
println!("\n--- Downloading: {} ---", url);
|
||||
let bytes = http_download_url_to_bytes(url).await
|
||||
.unwrap_or_else(|e| panic!("Failed to download {}: {:?}", url, e));
|
||||
|
||||
println!(" Downloaded {} bytes", bytes.len());
|
||||
|
||||
let head: Vec<u8> = bytes.iter().take(32).cloned().collect();
|
||||
println!(" First 32 bytes (hex): {:02x?}", head);
|
||||
println!(" First 16 bytes (ascii): {:?}", String::from_utf8_lossy(&head[..16.min(head.len())]));
|
||||
|
||||
let mimetype = MimetypeInfo::get_for_bytes(&bytes);
|
||||
match mimetype {
|
||||
Some(info) => {
|
||||
println!(" Detected mime type: {}", info.mime_type());
|
||||
println!(" Detected extension: {:?}", info.file_extension());
|
||||
}
|
||||
None => {
|
||||
println!(" ERROR: MimetypeInfo::get_for_bytes returned None!");
|
||||
println!(" This is the bug — infer could not detect the file type.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+142
-42
@@ -1,16 +1,15 @@
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::webhooks::process_success::resolve_file_metadata::resolve_file_metadata;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use anyhow::anyhow;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::webhook_api::hydrated::hydrated_webhook_contents::{ModelGlbData, ThumbnailData};
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use log::{info, warn};
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use mysql_queries::queries::media_files::edit::set_media_file_cover_image::{set_media_file_cover_image, UpdateArgs};
|
||||
@@ -20,7 +19,7 @@ const PREFIX : Option<&str> = Some("artcraft_");
|
||||
|
||||
/*
|
||||
Example payload, from Hunyuan 3d 3.0,
|
||||
|
||||
|
||||
{
|
||||
"model_glb": {
|
||||
"url": "https://v3b.fal.media/files/b/0a8afeff/8uc6VVLTpmVrupLqxxwOg_model.glb",
|
||||
@@ -52,7 +51,7 @@ const PREFIX : Option<&str> = Some("artcraft_");
|
||||
},
|
||||
"seed": null
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
|
||||
pub async fn process_model_glb_payload(
|
||||
@@ -60,31 +59,69 @@ pub async fn process_model_glb_payload(
|
||||
maybe_thumbnail_data: Option<&ThumbnailData>,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
) -> Result<MediaFileToken, AdvancedCommonWebError> {
|
||||
let mesh_url = model_glb_data.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in model glb payload"))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("No `url` in model glb payload");
|
||||
AdvancedCommonWebError::server_error_with_message("no `url` in model glb payload")
|
||||
})?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
// Download with a retry if the first attempt returns suspiciously few bytes.
|
||||
let mut file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download mesh: {:?}", e))?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
|
||||
info!("Mime type of mesh: {}", mime_type);
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download mesh from {}: {:?}", mesh_url, err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
if file_bytes.len() <= 10 {
|
||||
warn!(
|
||||
"Downloaded only {} bytes from {} — retrying once",
|
||||
file_bytes.len(),
|
||||
mesh_url,
|
||||
);
|
||||
file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to download mesh on retry from {}: {:?}", mesh_url, e);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Failed to download mesh on retry: {:?}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Resolve mime type: magic bytes first, fal content_type as fallback.
|
||||
let metadata = resolve_file_metadata(&file_bytes, model_glb_data.content_type.as_deref())
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"Could not determine file type for mesh (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(),
|
||||
model_glb_data.content_type,
|
||||
);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Could not determine file type for mesh (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(), model_glb_data.content_type))
|
||||
})?;
|
||||
|
||||
let mime_type = metadata.mime_type.as_str();
|
||||
|
||||
info!("Mime type of mesh: {}, source: {:?}", mime_type, metadata.source);
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("Unsupported media file type: {}", mime_type);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Unsupported media file type: {}", mime_type))
|
||||
})?;
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
let extension_with_period = metadata.file_extension.extension_with_period();
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to hash mesh bytes: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
@@ -94,7 +131,11 @@ pub async fn process_model_glb_payload(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to upload mesh to bucket: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
@@ -111,17 +152,21 @@ pub async fn process_model_glb_payload(
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to insert glb media file record: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
info!("Glb media file uploaded with token: {}", media_token);
|
||||
|
||||
|
||||
let result = try_to_attach_thumbnail(
|
||||
maybe_thumbnail_data,
|
||||
job,
|
||||
server_state,
|
||||
&media_token,
|
||||
).await;
|
||||
|
||||
|
||||
// NB: Fail open
|
||||
if let Err(err) = result {
|
||||
warn!("Could not attach thumbnail as cover image to media file: {:?}", err);
|
||||
@@ -135,36 +180,79 @@ async fn try_to_attach_thumbnail(
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
glb_media_token: &MediaFileToken,
|
||||
) -> AnyhowResult<()> {
|
||||
) -> Result<(), AdvancedCommonWebError> {
|
||||
let thumbnail_data = maybe_thumbnail_data
|
||||
.ok_or_else(|| anyhow!("no thumbnail data in extracted contents"))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("No thumbnail data in extracted contents");
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
"no thumbnail data in extracted contents")
|
||||
})?;
|
||||
|
||||
info!("Fal Thumbnail Data: {:?}", thumbnail_data);
|
||||
|
||||
let thumbnail_url = thumbnail_data.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in thumbnail payload"))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("No `url` in thumbnail payload");
|
||||
AdvancedCommonWebError::server_error_with_message("no `url` in thumbnail payload")
|
||||
})?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(thumbnail_url)
|
||||
// Download with a retry if the first attempt returns suspiciously few bytes.
|
||||
let mut file_bytes = http_download_url_to_bytes(thumbnail_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download thumbnail image: {:?}", e))?;
|
||||
.map_err(|e| {
|
||||
warn!("Failed to download thumbnail image from {}: {:?}", thumbnail_url, e);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Failed to download thumbnail image: {:?}", e))
|
||||
})?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
if file_bytes.len() <= 10 {
|
||||
warn!(
|
||||
"Downloaded only {} bytes from {} — retrying once",
|
||||
file_bytes.len(),
|
||||
thumbnail_url,
|
||||
);
|
||||
file_bytes = http_download_url_to_bytes(thumbnail_url)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to download thumbnail on retry from {}: {:?}", thumbnail_url, e);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Failed to download thumbnail on retry: {:?}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
// Resolve mime type: magic bytes first, fal content_type as fallback.
|
||||
let metadata = resolve_file_metadata(&file_bytes, thumbnail_data.content_type.as_deref())
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"Could not determine file type for thumbnail (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(),
|
||||
thumbnail_data.content_type,
|
||||
);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Could not determine file type for thumbnail (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(), thumbnail_data.content_type))
|
||||
})?;
|
||||
|
||||
info!("Mime type of thumbnail image: {}", mime_type);
|
||||
let mime_type = metadata.mime_type.as_str();
|
||||
|
||||
info!("Mime type of thumbnail image: {}, source: {:?}", mime_type, metadata.source);
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("Unsupported thumbnail media file type: {}", mime_type);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Unsupported media file type: {}", mime_type))
|
||||
})?;
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
let extension_with_period = metadata.file_extension.extension_with_period();
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to hash thumbnail bytes: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
@@ -174,7 +262,11 @@ async fn try_to_attach_thumbnail(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to upload thumbnail to bucket: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let thumbnail_media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
@@ -192,7 +284,11 @@ async fn try_to_attach_thumbnail(
|
||||
.is_intermediate_system_file(true)
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to insert thumbnail media file record: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let query_result = set_media_file_cover_image(UpdateArgs {
|
||||
media_file_token: glb_media_token,
|
||||
@@ -200,5 +296,9 @@ async fn try_to_attach_thumbnail(
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await;
|
||||
|
||||
if let Err(err) = query_result {
|
||||
warn!("Failed to set cover image on media file {}: {:?}", glb_media_token, err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+67
-23
@@ -1,16 +1,15 @@
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::webhooks::process_success::resolve_file_metadata::resolve_file_metadata;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use anyhow::anyhow;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ModelMeshData;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use log::info;
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use log::{info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
@@ -21,31 +20,68 @@ pub async fn process_model_mesh_payload(
|
||||
model_mesh_data: &ModelMeshData,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
) -> Result<MediaFileToken, AdvancedCommonWebError> {
|
||||
let mesh_url = model_mesh_data.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in model mesh payload"))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("No `url` in model mesh payload");
|
||||
AdvancedCommonWebError::server_error_with_message("no `url` in model mesh payload")
|
||||
})?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
// Download with a retry if the first attempt returns suspiciously few bytes.
|
||||
let mut file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download mesh: {:?}", e))?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
|
||||
info!("Mime type of mesh: {}", mime_type);
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download mesh from {}: {:?}", mesh_url, err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
if file_bytes.len() <= 10 {
|
||||
warn!(
|
||||
"Downloaded only {} bytes from {} — retrying once",
|
||||
file_bytes.len(),
|
||||
mesh_url,
|
||||
);
|
||||
file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download mesh on retry from {}: {:?}", mesh_url, err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Resolve mime type: magic bytes first, fal content_type as fallback.
|
||||
let metadata = resolve_file_metadata(&file_bytes, model_mesh_data.content_type.as_deref())
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"Could not determine file type for mesh (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(),
|
||||
model_mesh_data.content_type,
|
||||
);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Could not determine file type for mesh (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(), model_mesh_data.content_type))
|
||||
})?;
|
||||
|
||||
let mime_type = metadata.mime_type.as_str();
|
||||
|
||||
info!("Mime type of mesh: {}, source: {:?}", mime_type, metadata.source);
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("Unsupported media file type: {}", mime_type);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Unsupported media file type: {}", mime_type))
|
||||
})?;
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
let extension_with_period = metadata.file_extension.extension_with_period();
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to hash mesh bytes: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
@@ -55,7 +91,11 @@ pub async fn process_model_mesh_payload(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to upload mesh to bucket: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
@@ -72,8 +112,12 @@ pub async fn process_model_mesh_payload(
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to insert mesh media file record: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
info!("Mesh media file uploaded with token: {}", media_token);
|
||||
|
||||
Ok(media_token)
|
||||
|
||||
+72
-25
@@ -1,16 +1,15 @@
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::webhooks::process_success::resolve_file_metadata::resolve_file_metadata;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use anyhow::anyhow;
|
||||
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_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use errors::AnyhowResult;
|
||||
use fal_client::webhook_api::hydrated::hydrated_webhook_contents::VideoData;
|
||||
use filesys::path_to_string::path_to_string;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use log::{error, info};
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use std::io::Write;
|
||||
@@ -25,43 +24,83 @@ pub async fn process_video_payload(
|
||||
video_data: &VideoData,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> AnyhowResult<MediaFileToken> {
|
||||
) -> Result<MediaFileToken, AdvancedCommonWebError> {
|
||||
let video_url = video_data.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no `url` in video payload"))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("No `url` in video payload");
|
||||
AdvancedCommonWebError::server_error_with_message("no `url` in video payload")
|
||||
})?;
|
||||
|
||||
let file_bytes = http_download_url_to_bytes(video_url)
|
||||
// Download with a retry if the first attempt returns suspiciously few bytes.
|
||||
let mut file_bytes = http_download_url_to_bytes(video_url)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to download image: {:?}", e))?;
|
||||
|
||||
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
|
||||
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
|
||||
|
||||
let mime_type = mimetype_info.mime_type();
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download video from {}: {:?}", video_url, err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
if file_bytes.len() <= 10 {
|
||||
warn!(
|
||||
"Downloaded only {} bytes from {} — retrying once",
|
||||
file_bytes.len(),
|
||||
video_url,
|
||||
);
|
||||
file_bytes = http_download_url_to_bytes(video_url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download video on retry from {}: {:?}", video_url, err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Resolve mime type: magic bytes first, fal content_type as fallback.
|
||||
let metadata = resolve_file_metadata(&file_bytes, video_data.content_type.as_deref())
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"Could not determine file type for video (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(),
|
||||
video_data.content_type,
|
||||
);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Could not determine file type for video (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(), video_data.content_type))
|
||||
})?;
|
||||
|
||||
info!("File type: {}, extension: {:?}, source: {:?}",
|
||||
metadata.mime_type, metadata.file_extension, metadata.source);
|
||||
|
||||
let mime_type = metadata.mime_type.as_str();
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
|
||||
.ok_or_else(|| {
|
||||
warn!("Unsupported media file type: {}", mime_type);
|
||||
AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Unsupported media file type: {}", mime_type))
|
||||
})?;
|
||||
|
||||
let extension_with_period = mimetype_info.file_extension()
|
||||
.map(|ext| ext.extension_with_period())
|
||||
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
|
||||
let extension_with_period = metadata.file_extension.extension_with_period();
|
||||
|
||||
let file_size_bytes = file_bytes.len();
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)?;
|
||||
let file_hash = sha256_hash_bytes(&file_bytes)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to hash video bytes: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let mut maybe_duration_millis = None;
|
||||
let mut maybe_frame_width = None;
|
||||
let mut maybe_frame_height = None;
|
||||
|
||||
|
||||
if let Ok(mut file) = NamedTempFile::new() {
|
||||
// NB: Fallible
|
||||
// NB: Fallible
|
||||
let _r = file.write_all(&file_bytes);
|
||||
|
||||
|
||||
if let Ok(video_info) = ffprobe_get_info(&file.path()) {
|
||||
maybe_duration_millis = video_info.duration
|
||||
.as_ref()
|
||||
.map(|duration| duration.millis as u64);
|
||||
|
||||
|
||||
maybe_frame_width = video_info.dimensions
|
||||
.as_ref()
|
||||
.map(|ref dims| dims.width as u32);
|
||||
@@ -80,7 +119,11 @@ pub async fn process_video_payload(
|
||||
public_upload_path.get_full_object_path_str(),
|
||||
file_bytes.as_ref(),
|
||||
&mime_type)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to upload video to bucket: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
@@ -99,8 +142,12 @@ pub async fn process_video_payload(
|
||||
.checksum_sha2(&file_hash)
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await?;
|
||||
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to insert video media file record: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
info!("Video media file uploaded with token: {}", media_token);
|
||||
|
||||
let thumbnail_task_result =
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
//! Resolves file metadata (mime type, file extension) from downloaded bytes,
|
||||
//! falling back to fal webhook content_type when magic-byte detection fails.
|
||||
|
||||
use log::warn;
|
||||
use mimetypes::mimetype_info::file_extension::FileExtension;
|
||||
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
|
||||
|
||||
/// Resolved metadata about a downloaded file: its mime type and extension.
|
||||
#[derive(Debug)]
|
||||
pub struct ResolvedFileMetadata {
|
||||
pub mime_type: String,
|
||||
pub file_extension: FileExtension,
|
||||
/// Where we got the mime type from.
|
||||
pub source: MetadataSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum MetadataSource {
|
||||
/// Detected from the file's magic bytes.
|
||||
MagicBytes,
|
||||
/// Fell back to the content_type reported by the fal webhook.
|
||||
FalContentType,
|
||||
}
|
||||
|
||||
/// Try to resolve the mime type and extension for `file_bytes`.
|
||||
///
|
||||
/// 1. First tries `MimetypeInfo::get_for_bytes` (magic-byte detection).
|
||||
/// 2. If that fails, falls back to `fal_content_type` (the `content_type`
|
||||
/// field from the fal webhook payload).
|
||||
/// 3. If neither works, returns `None`.
|
||||
pub fn resolve_file_metadata(
|
||||
file_bytes: &[u8],
|
||||
fal_content_type: Option<&str>,
|
||||
) -> Option<ResolvedFileMetadata> {
|
||||
// Primary: magic-byte detection.
|
||||
if let Some(info) = MimetypeInfo::get_for_bytes(file_bytes) {
|
||||
if let Some(ext) = info.file_extension() {
|
||||
return Some(ResolvedFileMetadata {
|
||||
mime_type: info.mime_type().to_string(),
|
||||
file_extension: ext,
|
||||
source: MetadataSource::MagicBytes,
|
||||
});
|
||||
}
|
||||
// Magic bytes gave a mime type but no known extension — still try fal.
|
||||
warn!(
|
||||
"Magic bytes detected mime type '{}' but no known file extension; trying fal content_type",
|
||||
info.mime_type(),
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: fal webhook content_type.
|
||||
if let Some(content_type) = fal_content_type {
|
||||
if let Some(ext) = FileExtension::from_mimetype(content_type) {
|
||||
warn!(
|
||||
"Using fal content_type '{}' as fallback (magic-byte detection failed or had no extension)",
|
||||
content_type,
|
||||
);
|
||||
return Some(ResolvedFileMetadata {
|
||||
mime_type: content_type.to_string(),
|
||||
file_extension: ext,
|
||||
source: MetadataSource::FalContentType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn png_magic_bytes_detected() {
|
||||
// PNG magic: 89 50 4e 47 0d 0a 1a 0a
|
||||
let png_bytes: Vec<u8> = vec![137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82];
|
||||
let result = resolve_file_metadata(&png_bytes, Some("image/png"));
|
||||
|
||||
let meta = result.expect("should resolve");
|
||||
assert_eq!(meta.mime_type, "image/png");
|
||||
assert_eq!(meta.file_extension, FileExtension::Png);
|
||||
assert_eq!(meta.source, MetadataSource::MagicBytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mp4_magic_bytes_detected() {
|
||||
// ftypisom MP4 magic
|
||||
let mp4_bytes: Vec<u8> = vec![
|
||||
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70,
|
||||
0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00,
|
||||
];
|
||||
let result = resolve_file_metadata(&mp4_bytes, None);
|
||||
|
||||
let meta = result.expect("should resolve");
|
||||
assert_eq!(meta.mime_type, "video/mp4");
|
||||
assert_eq!(meta.file_extension, FileExtension::Mp4);
|
||||
assert_eq!(meta.source, MetadataSource::MagicBytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_magic_bytes_falls_back_to_fal_content_type() {
|
||||
// Garbage bytes that infer won't recognize.
|
||||
let garbage: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
|
||||
let result = resolve_file_metadata(&garbage, Some("image/png"));
|
||||
|
||||
let meta = result.expect("should resolve via fallback");
|
||||
assert_eq!(meta.mime_type, "image/png");
|
||||
assert_eq!(meta.file_extension, FileExtension::Png);
|
||||
assert_eq!(meta.source, MetadataSource::FalContentType);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_magic_bytes_and_no_fal_content_type_returns_none() {
|
||||
let garbage: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
|
||||
let result = resolve_file_metadata(&garbage, None);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_magic_bytes_and_unknown_fal_content_type_returns_none() {
|
||||
let garbage: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
|
||||
let result = resolve_file_metadata(&garbage, Some("application/octet-stream"));
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_bytes_falls_back_to_fal_content_type() {
|
||||
let result = resolve_file_metadata(&[], Some("video/mp4"));
|
||||
|
||||
let meta = result.expect("should resolve via fallback");
|
||||
assert_eq!(meta.mime_type, "video/mp4");
|
||||
assert_eq!(meta.file_extension, FileExtension::Mp4);
|
||||
assert_eq!(meta.source, MetadataSource::FalContentType);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_bytes_falls_back_to_fal_content_type() {
|
||||
let result = resolve_file_metadata(&[0x89], Some("image/jpeg"));
|
||||
|
||||
let meta = result.expect("should resolve via fallback");
|
||||
assert_eq!(meta.mime_type, "image/jpeg");
|
||||
assert_eq!(meta.file_extension, FileExtension::Jpg);
|
||||
assert_eq!(meta.source, MetadataSource::FalContentType);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user