update
This commit is contained in:
11
AI_prompt.md
11
AI_prompt.md
@@ -189,3 +189,14 @@ Check the following files:
|
|||||||
I would like to expand this package (NATSBRIDGE) to include Rust support.
|
I would like to expand this package (NATSBRIDGE) to include Rust support.
|
||||||
Now help me update Rust implementation of this package at ./src/natsbridge.rs.
|
Now help me update Rust implementation of this package at ./src/natsbridge.rs.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
I want to build a client-side-rendering Dioxus-based chat webapp.
|
||||||
|
Dioxus version 0.7+ should be great.
|
||||||
|
I already populate the current folder for the project.
|
||||||
|
my server REST API endpoint is sommpanion.yiem.cc/agent-fronent/api/v1/chat but I didn't run the server yet. A message format is JSON string.
|
||||||
|
I just placed my custom package for encode and decode message at ./src/natsbridge.rs. smartsend() is for encoding and smartreceive() is for decoding.
|
||||||
|
you may also check the file /home/ton/docker-apps/sommpanion/NATSBridge/docs/walkthrough.md for more info about my package.
|
||||||
|
You can test whether Dioxus webapp can be build using this command "dx bundle --web --release --debug-symbols=false"
|
||||||
|
|||||||
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -692,6 +692,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -909,6 +919,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -1357,6 +1368,12 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ path = "src/natsbridge.rs"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
reqwest = { version = "0.12", features = ["json", "stream", "multipart"] }
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -246,7 +248,7 @@ impl MsgPayloadV1 {
|
|||||||
encoding,
|
encoding,
|
||||||
size,
|
size,
|
||||||
data: url,
|
data: url,
|
||||||
metadata: Some(HashMap::new()),
|
metadata: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,7 +334,7 @@ pub struct SmartsendOptions {
|
|||||||
/// HTTP file server URL for large payloads
|
/// HTTP file server URL for large payloads
|
||||||
pub fileserver_url: String,
|
pub fileserver_url: String,
|
||||||
/// Custom file server upload handler (optional, uses Plik by default)
|
/// Custom file server upload handler (optional, uses Plik by default)
|
||||||
pub fileserver_upload_handler: Option<Box<dyn FileUploadHandler>>,
|
pub fileserver_upload_handler: Option<Arc<dyn FileUploadHandler>>,
|
||||||
/// Size threshold in bytes for switching from direct to link transport
|
/// Size threshold in bytes for switching from direct to link transport
|
||||||
pub size_threshold: usize,
|
pub size_threshold: usize,
|
||||||
/// Correlation ID for distributed tracing (auto-generated if empty)
|
/// Correlation ID for distributed tracing (auto-generated if empty)
|
||||||
@@ -378,7 +380,7 @@ impl Default for SmartsendOptions {
|
|||||||
/// Options for the `smartreceive` function
|
/// Options for the `smartreceive` function
|
||||||
pub struct SmartreceiveOptions {
|
pub struct SmartreceiveOptions {
|
||||||
/// Custom file server download handler (optional, uses exponential backoff by default)
|
/// Custom file server download handler (optional, uses exponential backoff by default)
|
||||||
pub fileserver_download_handler: Option<Box<dyn FileDownloadHandler>>,
|
pub fileserver_download_handler: Option<Arc<dyn FileDownloadHandler>>,
|
||||||
/// Maximum retry attempts for fetching a URL
|
/// Maximum retry attempts for fetching a URL
|
||||||
pub max_retries: u32,
|
pub max_retries: u32,
|
||||||
/// Initial delay for exponential backoff in milliseconds
|
/// Initial delay for exponential backoff in milliseconds
|
||||||
@@ -502,14 +504,20 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Upload the file
|
// Step 2: Upload the file as multipart/form-data
|
||||||
let upload_url = format!("{}/file/{}", file_server_url, uploadid);
|
let upload_url = format!("{}/file/{}", file_server_url, uploadid);
|
||||||
|
let form = reqwest::multipart::Form::new()
|
||||||
|
.part(
|
||||||
|
"file",
|
||||||
|
reqwest::multipart::Part::bytes(data.to_vec())
|
||||||
|
.file_name(dataname.to_string())
|
||||||
|
.mime_str("application/octet-stream")
|
||||||
|
.map_err(|e| NatSBridgeError::UploadFailed(format!("Invalid MIME type: {}", e)))?,
|
||||||
|
);
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&upload_url)
|
.post(&upload_url)
|
||||||
.header("X-UploadToken", &uploadtoken)
|
.header("X-UploadToken", &uploadtoken)
|
||||||
.body(data.to_vec())
|
.multipart(form)
|
||||||
.header("Content-Type", "application/octet-stream")
|
|
||||||
.header("Filename", dataname)
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| NatSBridgeError::UploadFailed(format!("Upload request failed: {}", e)))?;
|
.map_err(|e| NatSBridgeError::UploadFailed(format!("Upload request failed: {}", e)))?;
|
||||||
@@ -530,8 +538,8 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
|
|||||||
let fileid = upload_json["id"].as_str().unwrap_or("").to_string();
|
let fileid = upload_json["id"].as_str().unwrap_or("").to_string();
|
||||||
|
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/file/{}/{}",
|
"{}/file/{}/{}/{}",
|
||||||
file_server_url, uploadid, dataname
|
file_server_url, uploadid, fileid, dataname
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(UploadResult {
|
Ok(UploadResult {
|
||||||
@@ -762,9 +770,9 @@ pub async fn smartsend(
|
|||||||
|
|
||||||
let mut payloads: Vec<MsgPayloadV1> = Vec::new();
|
let mut payloads: Vec<MsgPayloadV1> = Vec::new();
|
||||||
|
|
||||||
// Determine the upload handler to use
|
// Determine the upload handler to use (custom or default Plik)
|
||||||
let _custom_upload = options.fileserver_upload_handler.is_some();
|
let upload_handler: Arc<dyn FileUploadHandler> = options.fileserver_upload_handler.clone()
|
||||||
let upload_handler: Box<dyn FileUploadHandler> = Box::new(PlikOneshotUploadHandler);
|
.unwrap_or_else(|| Arc::new(PlikOneshotUploadHandler));
|
||||||
|
|
||||||
for (dataname, payload, payload_type) in data {
|
for (dataname, payload, payload_type) in data {
|
||||||
// Use the explicitly provided payload_type from the tuple,
|
// Use the explicitly provided payload_type from the tuple,
|
||||||
@@ -845,6 +853,27 @@ pub async fn smartsend(
|
|||||||
Ok((env, env_json_str))
|
Ok((env, env_json_str))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helper: store deserialized data back into MsgPayloadV1
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Store deserialized Payload data back into a MsgPayloadV1's data field.
|
||||||
|
/// After smartreceive(), payload.data contains the deserialized content as a string
|
||||||
|
/// (decoded text, JSON string, or base64 for binary types).
|
||||||
|
fn store_deserialized_data(payload: &MsgPayloadV1, deserialized: &Payload) -> MsgPayloadV1 {
|
||||||
|
let mut p = payload.clone();
|
||||||
|
match deserialized {
|
||||||
|
Payload::Text(s) => p.data = s.clone(),
|
||||||
|
Payload::Dictionary(v) => p.data = serde_json::to_string(v).unwrap_or_default(),
|
||||||
|
Payload::JsonTable(v) => p.data = serde_json::to_string(v).unwrap_or_default(),
|
||||||
|
Payload::ArrowTable(b) => p.data = BASE64.encode(b),
|
||||||
|
Payload::Image(b) | Payload::Audio(b) | Payload::Video(b) | Payload::Binary(b) => {
|
||||||
|
p.data = BASE64.encode(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Public API: smartreceive
|
// Public API: smartreceive
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -905,12 +934,12 @@ pub async fn smartreceive(
|
|||||||
let correlation_id = env.correlation_id.clone();
|
let correlation_id = env.correlation_id.clone();
|
||||||
log_trace(&correlation_id, "Processing received message");
|
log_trace(&correlation_id, "Processing received message");
|
||||||
|
|
||||||
// Determine the download handler to use
|
// Determine the download handler to use (custom or default backoff)
|
||||||
let _custom_download = options.fileserver_download_handler.is_some();
|
let download_handler: Arc<dyn FileDownloadHandler> = options.fileserver_download_handler.clone()
|
||||||
let download_handler: Box<dyn FileDownloadHandler> = Box::new(BackoffDownloadHandler);
|
.unwrap_or_else(|| Arc::new(BackoffDownloadHandler));
|
||||||
|
|
||||||
// Process each payload
|
// Process each payload
|
||||||
let mut deserialized_payloads: Vec<MsgPayloadV1> = Vec::new();
|
let mut updated_payloads: Vec<MsgPayloadV1> = Vec::new();
|
||||||
|
|
||||||
for payload in &env.payloads {
|
for payload in &env.payloads {
|
||||||
let transport = payload.transport.as_str();
|
let transport = payload.transport.as_str();
|
||||||
@@ -929,15 +958,15 @@ pub async fn smartreceive(
|
|||||||
"Base64 decode failed for '{}': {}", dataname, e
|
"Base64 decode failed for '{}': {}", dataname, e
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
// Deserialize based on type
|
// Deserialize based on type and store result back into payload
|
||||||
let _deserialized = deserialize_data(
|
let deserialized = deserialize_data(
|
||||||
&payload_bytes,
|
&payload_bytes,
|
||||||
&payload_type,
|
&payload_type,
|
||||||
&correlation_id,
|
&correlation_id,
|
||||||
)?;
|
)?;
|
||||||
|
let updated = store_deserialized_data(payload, &deserialized);
|
||||||
|
|
||||||
// Keep the original payload structure (with base64 data)
|
updated_payloads.push(updated);
|
||||||
deserialized_payloads.push(payload.clone());
|
|
||||||
}
|
}
|
||||||
"link" => {
|
"link" => {
|
||||||
let url = payload.data.clone();
|
let url = payload.data.clone();
|
||||||
@@ -956,15 +985,15 @@ pub async fn smartreceive(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Deserialize based on type
|
// Deserialize based on type and store result back into payload
|
||||||
let _deserialized = deserialize_data(
|
let deserialized = deserialize_data(
|
||||||
&downloaded_data,
|
&downloaded_data,
|
||||||
&payload_type,
|
&payload_type,
|
||||||
&correlation_id,
|
&correlation_id,
|
||||||
)?;
|
)?;
|
||||||
|
let updated = store_deserialized_data(payload, &deserialized);
|
||||||
|
|
||||||
// Keep the original payload structure (with URL)
|
updated_payloads.push(updated);
|
||||||
deserialized_payloads.push(payload.clone());
|
|
||||||
}
|
}
|
||||||
unknown => {
|
unknown => {
|
||||||
return Err(NatSBridgeError::UnknownTransport(format!(
|
return Err(NatSBridgeError::UnknownTransport(format!(
|
||||||
@@ -975,7 +1004,7 @@ pub async fn smartreceive(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
env.payloads = deserialized_payloads;
|
env.payloads = updated_payloads;
|
||||||
Ok(env)
|
Ok(env)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1037,6 +1066,52 @@ pub async fn send_binary(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Plik File Upload from Disk
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Upload a file from disk to a Plik server in one-shot mode.
|
||||||
|
///
|
||||||
|
/// Reads the file at `filepath`, extracts its filename, and uploads it
|
||||||
|
/// using the Plik one-shot upload protocol (multipart/form-data).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `file_server_url`: Base URL of the Plik server (e.g., `"http://localhost:8080"`)
|
||||||
|
/// - `filepath`: Full path to the local file to upload
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Result<UploadResult, NatSBridgeError>` with uploadid, fileid, and download URL
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// use natsbridge::plik_upload_file;
|
||||||
|
///
|
||||||
|
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let result = plik_upload_file("http://localhost:8080", "./large_file.zip").await?;
|
||||||
|
/// println!("Uploaded to: {}", result.url);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn plik_upload_file(
|
||||||
|
file_server_url: &str,
|
||||||
|
filepath: &str,
|
||||||
|
) -> Result<UploadResult, NatSBridgeError> {
|
||||||
|
// Read the file from disk
|
||||||
|
let data = tokio::fs::read(filepath).await
|
||||||
|
.map_err(|e| NatSBridgeError::IoError(format!(
|
||||||
|
"Failed to read file '{}': {}", filepath, e
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
// Extract filename from path
|
||||||
|
let dataname = Path::new(filepath)
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Upload using the Plik one-shot handler
|
||||||
|
PlikOneshotUploadHandler.upload(file_server_url, &dataname, &data).await
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Module Exports
|
// Module Exports
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user