use facet::Facet;
use serde::{Deserialize, Serialize};
use crate::http::HttpOutcome;
#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum TransferEvent {
Progress { transferred: u64, total: Option<u64> },
Done { outcome: HttpOutcome, handle: Option<String> },
}
impl TransferEvent {
pub fn encode(&self) -> Vec<u8> {
use crux_core::bridge::{BincodeFfiFormat, FfiFormat};
let mut buffer = Vec::new();
BincodeFfiFormat::serialize(&mut buffer, self).expect("encode TransferEvent");
buffer
}
pub fn decode(bytes: &[u8]) -> Result<Self, String> {
use crux_core::bridge::{BincodeFfiFormat, FfiFormat};
BincodeFfiFormat::deserialize(bytes).map_err(|e| e.to_string())
}
}
use crate::{Cx, HttpHeader, PluginResponse};
#[derive(Serialize)]
struct TransferReq {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
dest: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
method: Option<String>,
headers: Vec<HttpHeader>,
}
pub struct TransferBuilder<'a, E> {
cx: &'a mut Cx<E>,
op: &'static str, req: TransferReq,
}
impl<'a, E> TransferBuilder<'a, E> {
pub(crate) fn upload(cx: &'a mut Cx<E>, url: String, source: String) -> Self {
Self {
cx,
op: "upload",
req: TransferReq { url, source: Some(source), dest: None, method: Some("PUT".into()), headers: Vec::new() },
}
}
pub(crate) fn download(cx: &'a mut Cx<E>, url: String, dest: String) -> Self {
Self {
cx,
op: "download",
req: TransferReq { url, source: None, dest: Some(dest), method: None, headers: Vec::new() },
}
}
#[must_use]
pub fn method(mut self, m: impl Into<String>) -> Self {
if self.op == "upload" {
self.req.method = Some(m.into());
}
self
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.req.headers.push(HttpHeader { name: name.into(), value: value.into() });
self
}
#[must_use]
pub fn bearer(self, token: impl AsRef<str>) -> Self {
self.header("Authorization", format!("Bearer {}", token.as_ref()))
}
pub fn start(self, key: impl Into<String>, on_event: impl Fn(TransferEvent) -> E + Send + 'static) -> String {
let key = key.into();
let input = serde_json::to_string(&self.req).expect("serialize transfer request");
self.cx.subscribe(key.clone(), "transfer", self.op, input, move |r: PluginResponse| {
on_event(decode_event(&r))
});
key
}
}
fn decode_event(r: &PluginResponse) -> TransferEvent {
TransferEvent::decode(&r.output).unwrap_or_else(|e| TransferEvent::Done {
outcome: HttpOutcome::TransportError { message: format!("malformed transfer event: {e}") },
handle: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::{HttpHeader, HttpOutcome};
#[test]
fn round_trips_progress_with_and_without_total() {
for ev in [
TransferEvent::Progress { transferred: 0, total: Some(1024) },
TransferEvent::Progress { transferred: 999_999_999, total: None },
] {
assert_eq!(TransferEvent::decode(&ev.encode()).unwrap(), ev);
}
}
#[test]
fn round_trips_done_upload_and_download() {
let up = TransferEvent::Done {
outcome: HttpOutcome::Response { status: 200, headers: vec![], body: vec![] },
handle: None,
};
let down = TransferEvent::Done {
outcome: HttpOutcome::Response {
status: 200,
headers: vec![HttpHeader { name: "Content-Length".into(), value: "5".into() }],
body: vec![],
},
handle: Some("blob:abc".into()),
};
for ev in [up, down] {
assert_eq!(TransferEvent::decode(&ev.encode()).unwrap(), ev);
}
}
#[test]
fn decode_rejects_garbage_without_panicking() {
assert!(TransferEvent::decode(&[0xff, 0xff, 0xff]).is_err());
}
}