1use facet::Facet;
9use serde::{Deserialize, Serialize};
10
11use crate::http::HttpOutcome;
12
13#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
16#[repr(C)]
17pub enum TransferEvent {
18 Progress { transferred: u64, total: Option<u64> },
20 Done { outcome: HttpOutcome, handle: Option<String> },
25}
26
27impl TransferEvent {
28 pub fn encode(&self) -> Vec<u8> {
32 use crux_core::bridge::{BincodeFfiFormat, FfiFormat};
33 let mut buffer = Vec::new();
34 BincodeFfiFormat::serialize(&mut buffer, self).expect("encode TransferEvent");
35 buffer
36 }
37
38 pub fn decode(bytes: &[u8]) -> Result<Self, String> {
40 use crux_core::bridge::{BincodeFfiFormat, FfiFormat};
41 BincodeFfiFormat::deserialize(bytes).map_err(|e| e.to_string())
42 }
43}
44
45use crate::{Cx, HttpHeader, PluginResponse};
46
47#[derive(Serialize)]
50struct TransferReq {
51 url: String,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 source: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 dest: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 method: Option<String>,
58 headers: Vec<HttpHeader>,
59}
60
61pub struct TransferBuilder<'a, E> {
64 cx: &'a mut Cx<E>,
65 op: &'static str, req: TransferReq,
67}
68
69impl<'a, E> TransferBuilder<'a, E> {
70 pub(crate) fn upload(cx: &'a mut Cx<E>, url: String, source: String) -> Self {
71 Self {
72 cx,
73 op: "upload",
74 req: TransferReq { url, source: Some(source), dest: None, method: Some("PUT".into()), headers: Vec::new() },
75 }
76 }
77
78 pub(crate) fn download(cx: &'a mut Cx<E>, url: String, dest: String) -> Self {
79 Self {
80 cx,
81 op: "download",
82 req: TransferReq { url, source: None, dest: Some(dest), method: None, headers: Vec::new() },
83 }
84 }
85
86 #[must_use]
88 pub fn method(mut self, m: impl Into<String>) -> Self {
89 if self.op == "upload" {
90 self.req.method = Some(m.into());
91 }
92 self
93 }
94
95 #[must_use]
97 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
98 self.req.headers.push(HttpHeader { name: name.into(), value: value.into() });
99 self
100 }
101
102 #[must_use]
104 pub fn bearer(self, token: impl AsRef<str>) -> Self {
105 self.header("Authorization", format!("Bearer {}", token.as_ref()))
106 }
107
108 pub fn start(self, key: impl Into<String>, on_event: impl Fn(TransferEvent) -> E + Send + 'static) -> String {
111 let key = key.into();
112 let input = serde_json::to_string(&self.req).expect("serialize transfer request");
113 self.cx.subscribe(key.clone(), "transfer", self.op, input, move |r: PluginResponse| {
114 on_event(decode_event(&r))
115 });
116 key
117 }
118}
119
120fn decode_event(r: &PluginResponse) -> TransferEvent {
123 TransferEvent::decode(&r.output).unwrap_or_else(|e| TransferEvent::Done {
124 outcome: HttpOutcome::TransportError { message: format!("malformed transfer event: {e}") },
125 handle: None,
126 })
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::http::{HttpHeader, HttpOutcome};
133
134 #[test]
135 fn round_trips_progress_with_and_without_total() {
136 for ev in [
137 TransferEvent::Progress { transferred: 0, total: Some(1024) },
138 TransferEvent::Progress { transferred: 999_999_999, total: None },
139 ] {
140 assert_eq!(TransferEvent::decode(&ev.encode()).unwrap(), ev);
141 }
142 }
143
144 #[test]
145 fn round_trips_done_upload_and_download() {
146 let up = TransferEvent::Done {
147 outcome: HttpOutcome::Response { status: 200, headers: vec![], body: vec![] },
148 handle: None,
149 };
150 let down = TransferEvent::Done {
151 outcome: HttpOutcome::Response {
152 status: 200,
153 headers: vec![HttpHeader { name: "Content-Length".into(), value: "5".into() }],
154 body: vec![],
155 },
156 handle: Some("blob:abc".into()),
157 };
158 for ev in [up, down] {
159 assert_eq!(TransferEvent::decode(&ev.encode()).unwrap(), ev);
160 }
161 }
162
163 #[test]
164 fn decode_rejects_garbage_without_panicking() {
165 assert!(TransferEvent::decode(&[0xff, 0xff, 0xff]).is_err());
166 }
167}