fal webhook fixes 2 (#1347)

This commit is contained in:
Brandon Thomas
2026-04-12 05:32:16 -04:00
committed by GitHub
parent d64944b1c3
commit 834f0b48a9
14 changed files with 749 additions and 178 deletions
@@ -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 {
@@ -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"
}
@@ -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;
@@ -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)
@@ -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.");
}
}
}
}
@@ -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(())
}
@@ -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)
@@ -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 =
@@ -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);
}
}