diff --git a/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_3_firefox/r5_upload_complete_response.json b/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_3_firefox/r5_upload_complete_response.json new file mode 100644 index 0000000000..3f80a034de --- /dev/null +++ b/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_3_firefox/r5_upload_complete_response.json @@ -0,0 +1,12 @@ +{ + "context": null, + "id": "cf326bd0-b7eb-454c-b433-54f36eb4d5a6", + "display_name": "uploaded_image_7e2fb65f-819a-486b-9609-f782ad4d244f.jpeg", + "owner_id": "sVgAQDyH3BfYOBfF6epWTjLHygX2", + "status": "READY", + "created_at": 1765325556, + "updated_at": 1765325558, + "object_uri": "https://cdn.marble.worldlabs.ai/object/cf326bd0-b7eb-454c-b433-54f36eb4d5a6/asset.jpg", + "mime_type": "image/jpeg", + "metadata": null +} diff --git a/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_3_firefox/r6_upload_complete_response.txt b/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_3_firefox/r6_upload_complete_response.txt deleted file mode 100644 index a6f6ac0c8e..0000000000 --- a/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_3_firefox/r6_upload_complete_response.txt +++ /dev/null @@ -1 +0,0 @@ -{"context":null,"id":"cf326bd0-b7eb-454c-b433-54f36eb4d5a6","display_name":"uploaded_image_7e2fb65f-819a-486b-9609-f782ad4d244f.jpeg","owner_id":"sVgAQDyH3BfYOBfF6epWTjLHygX2","status":"READY","created_at":1765325556,"updated_at":1765325558,"object_uri":"https://cdn.marble.worldlabs.ai/object/cf326bd0-b7eb-454c-b433-54f36eb4d5a6/asset.jpg","mime_type":"image/jpeg","metadata":null} \ No newline at end of file diff --git a/crates/api_clients/world_labs_client/src/api/requests/google_upload/google_upload_image.rs b/crates/api_clients/world_labs_client/src/api/requests/google_upload/google_upload_image.rs new file mode 100644 index 0000000000..acdc06991f --- /dev/null +++ b/crates/api_clients/world_labs_client/src/api/requests/google_upload/google_upload_image.rs @@ -0,0 +1,102 @@ +use crate::api::api_types::upload_mime_type::UploadMimeType; +use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; +use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; +use crate::credentials::world_labs_cookies::WorldLabsCookies; +use crate::error::world_labs_client_error::WorldLabsClientError; +use crate::error::world_labs_error::WorldLabsError; +use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; +use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; +use http_headers::values::accept::ACCEPT_ALL; +use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; +use http_headers::values::connection::CONNECTION_KEEP_ALIVE; +use http_headers::values::pragma::PRAGMA_NO_CACHE; +use http_headers::values::priority::{PRIORITY_6}; +use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; +use http_headers::values::te::TE_TRAILERS; +use log::error; +use serde::{Serialize}; +use std::time::Duration; +use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; +use wreq::Client; +use wreq_util::Emulation; + +pub struct GoogleUploadImageArgs<'a> { + pub cookies: &'a WorldLabsCookies, + pub bearer_token: &'a WorldLabsBearerToken, + pub upload_url: &'a str, + pub upload_mime_type: UploadMimeType, + pub file_bytes: Vec, + pub request_timeout: Option, +} + +/// Marble Image-to-World +/// Upload the binary to GCP using the signed URL +/// Request #4 (of ~10) +pub async fn google_upload_image(args: GoogleUploadImageArgs<'_>) -> Result<(), WorldLabsError> { + let client = Client::builder() + .emulation(Emulation::Firefox143) + .build() + .map_err(|err| WorldLabsClientError::WreqClientError(err))?; + + let content_type = args.upload_mime_type.content_type(); + + let mut request_builder = client.post(args.upload_url) + .header(ACCEPT, ACCEPT_ALL) + .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") + .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") + .header(REFERER, REFERER_VALUE) + .header(CONTENT_TYPE, content_type) + .header("x-goog-content-length-range", "0,1048576000") + .header(AUTHORIZATION, args.bearer_token.to_bearer_token_string()) + .header(ORIGIN, ORIGIN_VALUE) + .header(SEC_GPC, "1") + .header(CONNECTION, CONNECTION_KEEP_ALIVE) + .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) + .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) + .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) + .header(PRIORITY, PRIORITY_6) + .header(PRAGMA, PRAGMA_NO_CACHE) + .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) + .header(CONTENT_LENGTH, 0) + .header(TE, TE_TRAILERS); + + if let Some(timeout) = args.request_timeout { + request_builder = request_builder.timeout(timeout); + } + + let http_request = request_builder + .body(args.file_bytes) + .build() + .map_err(|err| { + error!("Error building request: {:?}", err); + WorldLabsClientError::WreqClientError(err) + })?; + + let response = client.execute(http_request) + .await + .map_err(|err| { + error!("Error during request execution: {:?}", err); + WorldLabsGenericApiError::WreqError(err) + })?; + + let status = response.status(); + + let response_body = response.text() + .await + .map_err(|err| { + error!("Error reading response body: {:?}", err); + WorldLabsGenericApiError::WreqError(err) + })?; + + // TODO: Handle errors (Cloudflare, Grok, etc.) + if !status.is_success() { + error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); + //return Err(classify_general_http_status_code_and_body(status, response_body)); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { +} diff --git a/crates/api_clients/world_labs_client/src/api/requests/google_upload/mod.rs b/crates/api_clients/world_labs_client/src/api/requests/google_upload/mod.rs new file mode 100644 index 0000000000..66f712bf42 --- /dev/null +++ b/crates/api_clients/world_labs_client/src/api/requests/google_upload/mod.rs @@ -0,0 +1 @@ +pub mod google_upload_image; \ No newline at end of file diff --git a/crates/api_clients/world_labs_client/src/api/requests/mod.rs b/crates/api_clients/world_labs_client/src/api/requests/mod.rs index 53f08a2b70..6d6867b200 100644 --- a/crates/api_clients/world_labs_client/src/api/requests/mod.rs +++ b/crates/api_clients/world_labs_client/src/api/requests/mod.rs @@ -1 +1,2 @@ -pub mod objects; \ No newline at end of file +pub mod objects; +pub mod google_upload; \ No newline at end of file diff --git a/crates/api_clients/world_labs_client/src/api/requests/objects/finalize_object_image_upload.rs b/crates/api_clients/world_labs_client/src/api/requests/objects/finalize_object_image_upload.rs new file mode 100644 index 0000000000..ec628f2cea --- /dev/null +++ b/crates/api_clients/world_labs_client/src/api/requests/objects/finalize_object_image_upload.rs @@ -0,0 +1,132 @@ +use crate::api::api_types::object_id::ObjectId; +use crate::api::api_types::upload_id::UploadId; +use crate::api::api_types::upload_mime_type::UploadMimeType; +use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; +use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; +use crate::credentials::world_labs_cookies::WorldLabsCookies; +use crate::error::world_labs_client_error::WorldLabsClientError; +use crate::error::world_labs_error::WorldLabsError; +use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; +use chrono::Utc; +use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; +use http_headers::values::accept::ACCEPT_ALL; +use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; +use http_headers::values::connection::CONNECTION_KEEP_ALIVE; +use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; +use http_headers::values::pragma::PRAGMA_NO_CACHE; +use http_headers::values::priority::PRIORITY_4; +use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; +use http_headers::values::te::TE_TRAILERS; +use log::error; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use uuid::Uuid; +use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; +use wreq::Client; +use wreq_util::Emulation; + +const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects"; + +fn get_url(object_id: &ObjectId) -> String { + format!("{}/{}:complete", BASE_URL, object_id.0) +} + +pub struct BeginObjectImageUploadArgs<'a> { + pub cookies: &'a WorldLabsCookies, + pub bearer_token: &'a WorldLabsBearerToken, + pub object_id: &'a ObjectId, + pub request_timeout: Option, +} + +pub struct BeginObjectImageUploadResponse { + pub object_url: String, +} + +/// Marble Image-to-World +/// Mark the upload as complete. +/// Request #5 (of ~10) +pub async fn finalize_object_image_upload(args: BeginObjectImageUploadArgs<'_>) -> Result { + let client = Client::builder() + .emulation(Emulation::Firefox143) + .build() + .map_err(|err| WorldLabsClientError::WreqClientError(err))?; + + let url = get_url(args.object_id); + + let mut request_builder = client.post(url) + .header(ACCEPT, ACCEPT_ALL) + .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") + .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") + .header(REFERER, REFERER_VALUE) + .header(AUTHORIZATION, args.bearer_token.to_bearer_token_string()) + .header(ORIGIN, ORIGIN_VALUE) + .header(SEC_GPC, "1") + .header(CONNECTION, CONNECTION_KEEP_ALIVE) + .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) + .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) + .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) + .header(PRIORITY, PRIORITY_4) + .header(PRAGMA, PRAGMA_NO_CACHE) + .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) + .header(CONTENT_LENGTH, 0) + .header(TE, TE_TRAILERS); + + if let Some(timeout) = args.request_timeout { + request_builder = request_builder.timeout(timeout); + } + + let http_request = request_builder + .build() + .map_err(|err| { + error!("Error building request: {:?}", err); + WorldLabsClientError::WreqClientError(err) + })?; + + let response = client.execute(http_request) + .await + .map_err(|err| { + error!("Error during request execution: {:?}", err); + WorldLabsGenericApiError::WreqError(err) + })?; + + let status = response.status(); + + let response_body = response.text() + .await + .map_err(|err| { + error!("Error reading response body: {:?}", err); + WorldLabsGenericApiError::WreqError(err) + })?; + + // TODO: Handle errors (Cloudflare, Grok, etc.) + if !status.is_success() { + error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); + //return Err(classify_general_http_status_code_and_body(status, response_body)); + } + + let response : RawResponse = serde_json::from_str(&response_body) + .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; + + Ok(BeginObjectImageUploadResponse { + object_url: response.object_uri, + }) +} + +#[derive(Deserialize)] +struct RawResponse { + /// eg. "https://cdn.marble.worldlabs.ai/object/foo-bar-baz-bin/asset.jpg" + pub object_uri: String, +} + +#[cfg(test)] +mod tests { + use crate::api::api_types::object_id::ObjectId; + use crate::api::requests::objects::finalize_object_image_upload::get_url; + + #[test] + fn test_get_url() { + let object_id = ObjectId("foo-bar-baz-bin".to_string()); + let expected = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects/foo-bar-baz-bin:complete"; + assert_eq!(get_url(&object_id), expected); + } +} diff --git a/crates/api_clients/world_labs_client/src/api/requests/objects/mod.rs b/crates/api_clients/world_labs_client/src/api/requests/objects/mod.rs index 9532225d29..980fce9d8e 100644 --- a/crates/api_clients/world_labs_client/src/api/requests/objects/mod.rs +++ b/crates/api_clients/world_labs_client/src/api/requests/objects/mod.rs @@ -1,3 +1,4 @@ pub mod create_object; pub mod begin_object_image_attachment; -pub mod begin_object_image_upload; \ No newline at end of file +pub mod begin_object_image_upload; +pub mod finalize_object_image_upload; \ No newline at end of file diff --git a/crates/lib/http_headers/src/values/priority.rs b/crates/lib/http_headers/src/values/priority.rs index 9d8ddf06bd..197c90cc80 100644 --- a/crates/lib/http_headers/src/values/priority.rs +++ b/crates/lib/http_headers/src/values/priority.rs @@ -4,3 +4,7 @@ pub const PRIORITY_HIGHEST : &str = "u=0"; /// eg. -H 'Priority: u=4' /// "The default priority value for requests is 3." pub const PRIORITY_4: &str = "u=4"; + +/// eg. -H 'Priority: u=6' +/// "The default priority value for requests is 3." +pub const PRIORITY_6: &str = "u=6";